diff --git a/src/lib/application/handlers/PromptSubmitHandler.ts b/src/lib/application/handlers/PromptSubmitHandler.ts index 99492bc..872a6e5 100644 --- a/src/lib/application/handlers/PromptSubmitHandler.ts +++ b/src/lib/application/handlers/PromptSubmitHandler.ts @@ -73,10 +73,7 @@ export class PromptSubmitHandler implements IRequestHandler { const { @@ -145,7 +144,6 @@ export function createGitIndexingService( try { // Search for existing facts with similar metadata existingFacts = await memory.searchFacts( - [groupId], 'source:code-analysis extractionMethod:heuristic', 100 ); @@ -183,7 +181,7 @@ export function createGitIndexingService( // Save to memory with lifecycle metadata try { - await memory.addFactWithLifecycle(groupId, fact.text, { + await memory.addFactWithLifecycle(fact.text, { lifecycle: 'project', tags, }); diff --git a/src/lib/application/services/MemoryContextLoader.ts b/src/lib/application/services/MemoryContextLoader.ts index 592d02e..5312142 100644 --- a/src/lib/application/services/MemoryContextLoader.ts +++ b/src/lib/application/services/MemoryContextLoader.ts @@ -38,12 +38,7 @@ export class MemoryContextLoader { /** * Load memory context from git-mem. */ - async loadMemory( - hierarchicalGroupIds: readonly string[], - projectAliases: readonly string[], - _branch: string | null, - dateOptions?: IMemoryDateOptions, - ): Promise { + async loadMemory(dateOptions?: IMemoryDateOptions): Promise { const result: IMemoryLoadResult = { facts: [], nodes: [], @@ -53,7 +48,6 @@ export class MemoryContextLoader { }; const TIMEOUT_MS = 5000; - const allGroupIds = [...new Set([...hierarchicalGroupIds, ...projectAliases])]; const cancellableResult = await withCancellation( async (abortSignal) => { @@ -61,7 +55,7 @@ export class MemoryContextLoader { try { checkCancellation(abortSignal, 'Memory load cancelled before init-review'); - const initFacts = await this.memory.searchFacts(allGroupIds, 'init-review', 1); + const initFacts = await this.memory.searchFacts('init-review', 1); checkCancellation(abortSignal, 'Memory load cancelled after init-review fetch'); @@ -77,7 +71,7 @@ export class MemoryContextLoader { try { checkCancellation(abortSignal, 'Memory load cancelled before facts'); - const facts = await this.memory.loadFactsDateOrdered(allGroupIds, 100, dateOptions); + const facts = await this.memory.loadFactsDateOrdered(100, dateOptions); checkCancellation(abortSignal, 'Memory load cancelled after facts fetch'); @@ -90,7 +84,7 @@ export class MemoryContextLoader { try { checkCancellation(abortSignal, 'Memory load cancelled before tasks'); - const loadedTasks = await this.tasks.getTasksSimple(allGroupIds); + const loadedTasks = await this.tasks.getTasksSimple(); checkCancellation(abortSignal, 'Memory load cancelled after tasks fetch'); diff --git a/src/lib/commands/doctor.ts b/src/lib/commands/doctor.ts index 782c92c..52b13a1 100644 --- a/src/lib/commands/doctor.ts +++ b/src/lib/commands/doctor.ts @@ -3,13 +3,15 @@ * * Comprehensive diagnostic tool for Lisa configuration and connectivity. * Supports basic, verbose, and JSON output modes. + * + * Note: Group IDs are no longer used - the git repo itself provides scoping + * via git-mem (git notes in refs/notes/mem). */ import fs from 'fs-extra'; import path from 'path'; import chalk from 'chalk'; import type { ICliServices } from './cli-services'; -import { getCurrentGroupId } from '../skills/shared/utils/group-id'; // ============================================================================ // Types @@ -552,7 +554,8 @@ export async function runDoctor( opts.endpoint || config?.endpoint || (mode === 'zep-cloud' ? ZEP_CLOUD_ENDPOINT : DEFAULT_ENDPOINT); - const group = getCurrentGroupId(cwd); + // Group ID is derived from folder name for backwards compatibility + const group = getProjectName(cwd); const zepApiKey = config?.zepApiKey || process.env.ZEP_API_KEY; // Build config info diff --git a/src/lib/commands/knowledge.ts b/src/lib/commands/knowledge.ts index 49917b2..e961368 100644 --- a/src/lib/commands/knowledge.ts +++ b/src/lib/commands/knowledge.ts @@ -484,14 +484,13 @@ export function registerKnowledgeCommands(program: Command): void { const summarizer = createSummarizationService(memory, guard); - const groupId = opts.group || 'default'; const since = opts.since ? new Date(opts.since) : undefined; if (since !== undefined && isNaN(since.getTime())) { console.log(JSON.stringify({ status: 'error', action: 'summarize', error: `Invalid date: ${opts.since}` }, null, 2)); process.exitCode = 1; return; } - const result = await summarizer.summarize(groupId, { + const result = await summarizer.summarize({ since, topic: opts.topic, style: opts.style === 'detailed' ? 'detailed' : 'concise', diff --git a/src/lib/commands/pr.ts b/src/lib/commands/pr.ts index 2cb6eb9..6416c00 100644 --- a/src/lib/commands/pr.ts +++ b/src/lib/commands/pr.ts @@ -138,10 +138,8 @@ export function registerPrCommands(prCmd: Command, cliLogger: ILogger): void { && opts.watch !== false; if (shouldPoll && result.pr) { - const { getCurrentGroupId } = await import('../skills/common/group-id'); const memoryService = await createPrMemoryService(); - const groupId = getCurrentGroupId(); - const pollHandler = new PrPollHandler(githubClient, prRepository, undefined, memoryService, groupId); + const pollHandler = new PrPollHandler(githubClient, prRepository, undefined, memoryService); const pollOptions: IPrPollOptions = { autoUnwatch: true, logToFile: true, @@ -610,13 +608,11 @@ export function registerPrCommands(prCmd: Command, cliLogger: ILogger): void { try { const { GithubClient } = await import('../infrastructure'); const { PrRememberHandler } = await import('../application/handlers'); - const { getCurrentGroupId } = await import('../skills/common/group-id'); const githubClient = new GithubClient(); const memoryService = await createPrMemoryService(); - const groupId = getCurrentGroupId(); - const handler = new PrRememberHandler(githubClient, memoryService, groupId); + const handler = new PrRememberHandler(githubClient, memoryService); const result = await handler.execute({ prNumber: parsedPrNumber, repo: opts.repo, @@ -779,7 +775,6 @@ export function registerPrCommands(prCmd: Command, cliLogger: ILogger): void { const { GithubClient, Neo4jPullRequestRepository, createNeo4jConnectionManager } = await import('../infrastructure'); const { PrPollHandler } = await import('../application/handlers'); const { NotificationService } = await import('../infrastructure/notifications'); - const { getCurrentGroupId } = await import('../skills/common/group-id'); const githubClient = new GithubClient(); neo4jConnection = createNeo4jConnectionManager(); @@ -814,9 +809,8 @@ export function registerPrCommands(prCmd: Command, cliLogger: ILogger): void { // Create memory service for auto-capture of merged PRs const memoryService = await createPrMemoryService(); - const groupId = getCurrentGroupId(); - const handler = new PrPollHandler(githubClient, prRepository, notificationService, memoryService, groupId); + const handler = new PrPollHandler(githubClient, prRepository, notificationService, memoryService); const pollOptions = { autoUnwatch: opts.autoUnwatch, logToFile: opts.log, diff --git a/src/lib/domain/interfaces/IConsolidationService.ts b/src/lib/domain/interfaces/IConsolidationService.ts index 90854f2..86bcd8f 100644 --- a/src/lib/domain/interfaces/IConsolidationService.ts +++ b/src/lib/domain/interfaces/IConsolidationService.ts @@ -50,6 +50,8 @@ export interface IConsolidationOptions { * Consolidation service interface. * * Provides operations for consolidating duplicate or related facts. + * + * Note: Group IDs are no longer used - the git repo provides scoping via git-mem. */ export interface IConsolidationService { /** @@ -62,7 +64,6 @@ export interface IConsolidationService { * expire the rest, create supersedes relationships * - `keep-all`: No-op, return empty result * - * @param groupId - Group ID the facts belong to * @param factUuids - UUIDs of facts to consolidate (minimum 2) * @param action - Consolidation action to perform * @param options - Additional options (retainUuid, mergedText) @@ -70,7 +71,6 @@ export interface IConsolidationService { * @throws Error if retainUuid is not in the provided UUIDs */ consolidate( - groupId: string, factUuids: readonly string[], action: ConsolidationAction, options?: IConsolidationOptions diff --git a/src/lib/domain/interfaces/ICurationService.ts b/src/lib/domain/interfaces/ICurationService.ts index 82df045..698911c 100644 --- a/src/lib/domain/interfaces/ICurationService.ts +++ b/src/lib/domain/interfaces/ICurationService.ts @@ -74,6 +74,8 @@ export function parseCurationTag(tags: readonly string[]): CurationMark | null { * * Provides operations for curating facts (marking quality) * and computing quality scores for ranking. + * + * Note: Group IDs are no longer used - the git repo provides scoping via git-mem. */ export interface ICurationService { /** @@ -83,12 +85,10 @@ export interface ICurationService { * - `deprecated`: Also expires the fact * - `authoritative`: Promotes confidence to `verified` * - * @param groupId - Group ID the fact belongs to * @param uuid - UUID of the fact to mark * @param mark - The curation mark to apply */ markFact( - groupId: string, uuid: string, mark: CurationMark ): Promise; diff --git a/src/lib/domain/interfaces/IGitIndexingService.ts b/src/lib/domain/interfaces/IGitIndexingService.ts index ecdf2c7..2c84579 100644 --- a/src/lib/domain/interfaces/IGitIndexingService.ts +++ b/src/lib/domain/interfaces/IGitIndexingService.ts @@ -88,6 +88,8 @@ export interface IGitIndexingResult { /** * Service for indexing extracted git facts to memory. + * + * Note: Group IDs are no longer used - the git repo provides scoping via git-mem. */ export interface IGitIndexingService { /** @@ -99,13 +101,11 @@ export interface IGitIndexingService { * - Deduplication against existing memories * * @param facts - Facts extracted from git history - * @param groupId - Memory group ID to save to * @param options - Indexing options * @returns Indexing result with counts */ indexFacts( facts: readonly IHeuristicFact[], - groupId: string, options?: IGitIndexingOptions ): Promise; } diff --git a/src/lib/domain/interfaces/IMemoryService.ts b/src/lib/domain/interfaces/IMemoryService.ts index d7a086d..56e9233 100644 --- a/src/lib/domain/interfaces/IMemoryService.ts +++ b/src/lib/domain/interfaces/IMemoryService.ts @@ -14,97 +14,75 @@ export interface IMemoryDateOptions { /** * Read operations for memory. * Separated for Interface Segregation Principle. + * + * Note: Group IDs are no longer used - the git repo itself provides scoping + * via git-mem (git notes in refs/notes/mem). */ export interface IMemoryReader { /** - * Load memory for a group, querying hierarchically. - * @param groupIds - Hierarchical group IDs to query - * @param aliases - Project aliases for tagging - * @param branch - Current git branch (for tagging) + * Load memory from the repository. * @param timeoutMs - Timeout in milliseconds */ - loadMemory( - groupIds: readonly string[], - aliases: readonly string[], - branch: string | null, - timeoutMs?: number - ): Promise; + loadMemory(timeoutMs?: number): Promise; /** * Load facts with date ordering (newest first). - * Uses DAL router with Neo4j when available for optimal performance. - * @param groupIds - Group IDs to query * @param limit - Maximum number of facts to return * @param options - Optional date filtering options */ loadFactsDateOrdered( - groupIds: readonly string[], limit?: number, options?: IMemoryDateOptions ): Promise; /** * Semantic search for facts. - * Uses DAL router with MCP when available. - * @param groupIds - Group IDs to search * @param query - Search query * @param limit - Maximum number of results */ - searchFacts( - groupIds: readonly string[], - query: string, - limit?: number - ): Promise; + searchFacts(query: string, limit?: number): Promise; } /** * Write operations for memory. * Separated for Interface Segregation Principle. + * + * Note: Group IDs are no longer used - the git repo itself provides scoping. */ export interface IMemoryWriter { /** * Save facts to memory. - * @param groupId - Group ID to save to * @param facts - Facts to save */ - saveMemory(groupId: string, facts: readonly string[]): Promise; + saveMemory(facts: readonly string[]): Promise; /** * Add a single fact to memory. - * @param groupId - Group ID to save to * @param fact - Fact to add * @param tags - Optional tags for the fact */ - addFact(groupId: string, fact: string, tags?: readonly string[]): Promise; + addFact(fact: string, tags?: readonly string[]): Promise; /** * Add a fact with lifecycle metadata. * Enriches tags with lifecycle: tag and delegates to addFact. - * @param groupId - Group ID to save to * @param fact - Fact to add * @param options - Save options including lifecycle and TTL */ - addFactWithLifecycle( - groupId: string, - fact: string, - options: IMemorySaveOptions - ): Promise; + addFactWithLifecycle(fact: string, options: IMemorySaveOptions): Promise; /** * Expire a single fact by UUID. - * Routes to Neo4j repository for direct Cypher expiration. - * @param groupId - Group ID the fact belongs to * @param uuid - UUID of the fact to expire */ - expireFact(groupId: string, uuid: string): Promise; + expireFact(uuid: string): Promise; /** * Clean up expired facts based on lifecycle TTL defaults. * Expires session facts older than 24h and ephemeral facts older than 1h. - * @param groupId - Group ID to clean up * @returns Number of facts expired */ - cleanupExpired(groupId: string): Promise; + cleanupExpired(): Promise; } /** diff --git a/src/lib/domain/interfaces/INlCurationService.ts b/src/lib/domain/interfaces/INlCurationService.ts index de109de..f913a38 100644 --- a/src/lib/domain/interfaces/INlCurationService.ts +++ b/src/lib/domain/interfaces/INlCurationService.ts @@ -68,6 +68,8 @@ export interface INlCurationResult { /** * Natural language curation service. * Translates natural language into structured memory operations. + * + * Note: Group IDs are no longer used - the git repo provides scoping via git-mem. */ export interface INlCurationService { /** @@ -75,17 +77,15 @@ export interface INlCurationService { * Does NOT execute — returns a plan for user review. * * @param input - Natural language command - * @param groupId - Memory group to operate on * @throws LlmDisabledError if LLM is disabled * @throws LlmFeatureDisabledError if curation feature is disabled */ - plan(input: string, groupId: string): Promise; + plan(input: string): Promise; /** * Execute a previously generated plan. * * @param plan - The plan to execute - * @param groupId - Memory group to operate on */ - execute(plan: INlCurationPlan, groupId: string): Promise; + execute(plan: INlCurationPlan): Promise; } diff --git a/src/lib/domain/interfaces/IRecursionService.ts b/src/lib/domain/interfaces/IRecursionService.ts index 424b9e3..1579540 100644 --- a/src/lib/domain/interfaces/IRecursionService.ts +++ b/src/lib/domain/interfaces/IRecursionService.ts @@ -34,6 +34,8 @@ export interface IRecursionConfig { * When a user submits a prompt, this service searches memory for * relevant context: previous decisions, learnings, and tasks. * The search strategy adapts based on the detected task type. + * + * Note: Group IDs are no longer used - the git repo provides scoping via git-mem. */ export interface IRecursionService { /** @@ -41,11 +43,10 @@ export interface IRecursionService { * Searches for relevant decisions, learnings, and tasks. * * @param prompt - The user's prompt text - * @param groupIds - Hierarchical group IDs to search * @param taskType - Optional task type to adjust search strategy * @returns Recursion result with found context */ - run(prompt: string, groupIds: readonly string[], taskType?: TaskType): Promise; + run(prompt: string, taskType?: TaskType): Promise; /** * Check if recursion should run for a given prompt. diff --git a/src/lib/domain/interfaces/ISummarizationService.ts b/src/lib/domain/interfaces/ISummarizationService.ts index fe7ab06..bbcb021 100644 --- a/src/lib/domain/interfaces/ISummarizationService.ts +++ b/src/lib/domain/interfaces/ISummarizationService.ts @@ -37,19 +37,17 @@ export interface ISummarizationOptions { /** * Summarization service. * Produces LLM-generated digests of project memory facts. + * + * Note: Group IDs are no longer used - the git repo provides scoping via git-mem. */ export interface ISummarizationService { /** - * Summarize memory facts for a group. + * Summarize memory facts. * - * @param groupId - Group ID to summarize facts from * @param options - Optional filters and style settings * @throws LlmDisabledError if LLM is disabled * @throws LlmFeatureDisabledError if summarization feature is disabled * @throws LlmBudgetExceededError if monthly budget limit is exceeded */ - summarize( - groupId: string, - options?: ISummarizationOptions - ): Promise; + summarize(options?: ISummarizationOptions): Promise; } diff --git a/src/lib/domain/interfaces/ITaskService.ts b/src/lib/domain/interfaces/ITaskService.ts index 6bb4db9..f208506 100644 --- a/src/lib/domain/interfaces/ITaskService.ts +++ b/src/lib/domain/interfaces/ITaskService.ts @@ -3,54 +3,45 @@ import type { ITask, ITaskInput, ITaskUpdate, ITaskCounts } from './types'; /** * Read operations for tasks. * Separated for Interface Segregation Principle. + * + * Note: Group IDs are no longer used - the git repo provides scoping via git-mem. */ export interface ITaskReader { /** - * Get all tasks for a group. - * @param groupIds - Hierarchical group IDs to query - * @param aliases - Project aliases for tagging - * @param branch - Current git branch (for tagging) + * Get all tasks. */ - getTasks( - groupIds: readonly string[], - aliases: readonly string[], - branch: string | null - ): Promise; + getTasks(): Promise; /** - * Get tasks with simple interface (no aliases/branch). - * Uses DAL router with Neo4j when available for date ordering. - * @param groupIds - Group IDs to query + * Get tasks with simple interface. */ - getTasksSimple(groupIds: readonly string[]): Promise; + getTasksSimple(): Promise; /** * Get task counts by status. - * Uses DAL router with Neo4j when available for efficient aggregation. - * @param groupIds - Group IDs to query */ - getTaskCounts(groupIds: readonly string[]): Promise; + getTaskCounts(): Promise; } /** * Write operations for tasks. * Separated for Interface Segregation Principle. + * + * Note: Group IDs are no longer used - the git repo provides scoping via git-mem. */ export interface ITaskWriter { /** * Create a new task. - * @param groupId - Group ID to create task in * @param task - Task input data */ - createTask(groupId: string, task: ITaskInput): Promise; + createTask(task: ITaskInput): Promise; /** * Update an existing task. - * @param groupId - Group ID containing the task * @param taskId - Task ID to update * @param updates - Fields to update */ - updateTask(groupId: string, taskId: string, updates: ITaskUpdate): Promise; + updateTask(taskId: string, updates: ITaskUpdate): Promise; } /** diff --git a/src/lib/domain/interfaces/events/IMemoryEvent.ts b/src/lib/domain/interfaces/events/IMemoryEvent.ts index 800f209..4f3db6e 100644 --- a/src/lib/domain/interfaces/events/IMemoryEvent.ts +++ b/src/lib/domain/interfaces/events/IMemoryEvent.ts @@ -2,20 +2,22 @@ import type { ISOTimestamp } from '../../types'; /** * Event emitted when memory is loaded. + * + * Note: Group IDs are no longer used - the git repo provides scoping via git-mem. */ export interface IMemoryLoadEvent { readonly type: 'memory:load'; - readonly groupId: string; readonly timestamp: ISOTimestamp; } /** * Event emitted when memory is saved. + * + * Note: Group IDs are no longer used - the git repo provides scoping via git-mem. */ export interface IMemorySaveEvent { readonly type: 'memory:save'; readonly facts: readonly string[]; - readonly groupId: string; readonly timestamp: ISOTimestamp; } @@ -23,12 +25,10 @@ export interface IMemorySaveEvent { * Create a memory load event. */ export function createMemoryLoadEvent( - groupId: string, timestamp: ISOTimestamp ): IMemoryLoadEvent { return { type: 'memory:load', - groupId, timestamp, }; } @@ -38,13 +38,11 @@ export function createMemoryLoadEvent( */ export function createMemorySaveEvent( facts: readonly string[], - groupId: string, timestamp: ISOTimestamp ): IMemorySaveEvent { return { type: 'memory:save', facts, - groupId, timestamp, }; } diff --git a/src/lib/infrastructure/adapters/gitmem/GitMemAdapter.ts b/src/lib/infrastructure/adapters/gitmem/GitMemAdapter.ts new file mode 100644 index 0000000..bb75f38 --- /dev/null +++ b/src/lib/infrastructure/adapters/gitmem/GitMemAdapter.ts @@ -0,0 +1,122 @@ +/** + * GitMemAdapter + * + * Implements Lisa's IMemoryService using git-mem's MemoryService. + * git-mem stores memories as git notes (refs/notes/mem) with AI-* commit trailers. + */ + +import { + MemoryService as GitMemMemoryService, + MemoryRepository, + NotesService, +} from 'git-mem'; +import type { IMemoryEntity, IMemoryQueryResult } from 'git-mem'; +import type { + IMemoryService, + IMemoryDateOptions, + IMemorySaveOptions, + IMemoryItem, + IMemoryResult, +} from '../../../domain'; + +/** + * Create a git-mem MemoryService instance with default wiring. + */ +function createGitMemService(): GitMemMemoryService { + const notesService = new NotesService(); + const repository = new MemoryRepository(notesService); + return new GitMemMemoryService(repository); +} + +/** + * Convert a git-mem IMemoryEntity to Lisa's IMemoryItem. + */ +function toMemoryItem(entity: IMemoryEntity): IMemoryItem { + return { + uuid: entity.id, + name: entity.type, + fact: entity.content, + tags: [...entity.tags], + created_at: entity.createdAt, + }; +} + +export class GitMemAdapter implements IMemoryService { + private readonly gitMem: GitMemMemoryService; + + constructor(gitMem?: GitMemMemoryService) { + this.gitMem = gitMem ?? createGitMemService(); + } + + async loadMemory( + _timeoutMs?: number, + ): Promise { + const result = this.gitMem.recall(undefined, { limit: 100 }); + return this.toMemoryResult(result); + } + + async loadFactsDateOrdered( + limit?: number, + options?: IMemoryDateOptions, + ): Promise { + const result = this.gitMem.recall(undefined, { + limit: limit ?? 100, + since: options?.since?.toISOString(), + }); + return result.memories.map(toMemoryItem); + } + + async searchFacts( + query: string, + limit?: number, + ): Promise { + const result = this.gitMem.recall(query, { limit: limit ?? 20 }); + return result.memories.map(toMemoryItem); + } + + async saveMemory(facts: readonly string[]): Promise { + for (const fact of facts) { + this.gitMem.remember(fact); + } + } + + async addFact(fact: string, tags?: readonly string[]): Promise { + this.gitMem.remember(fact, { + tags: tags ? [...tags] : undefined, + }); + } + + async addFactWithLifecycle( + fact: string, + options: IMemorySaveOptions, + ): Promise { + const tags: string[] = options.tags ? [...options.tags] : []; + if (options.lifecycle) { + tags.push(`lifecycle:${options.lifecycle}`); + } + this.gitMem.remember(fact, { tags }); + } + + async expireFact(uuid: string): Promise { + this.gitMem.delete(uuid); + } + + async cleanupExpired(): Promise { + // git-mem doesn't have TTL-based expiration yet + return 0; + } + + private toMemoryResult(queryResult: IMemoryQueryResult): IMemoryResult { + const items = queryResult.memories.map(toMemoryItem); + const tasks = items.filter(m => m.tags?.some(t => t === 'task' || t.startsWith('status:'))); + const facts = items.filter(m => !tasks.includes(m)); + + return { + facts, + nodes: [], + tasks, + initReview: null, + timedOut: false, + }; + } +} diff --git a/src/lib/infrastructure/adapters/gitmem/GitMemTaskAdapter.ts b/src/lib/infrastructure/adapters/gitmem/GitMemTaskAdapter.ts new file mode 100644 index 0000000..c5b37da --- /dev/null +++ b/src/lib/infrastructure/adapters/gitmem/GitMemTaskAdapter.ts @@ -0,0 +1,134 @@ +/** + * GitMemTaskAdapter + * + * Implements Lisa's ITaskService using git-mem's MemoryService. + * Tasks are stored as memories with 'task' tag and status tags. + */ + +import { + MemoryService as GitMemMemoryService, + MemoryRepository, + NotesService, +} from 'git-mem'; +import type { IMemoryEntity } from 'git-mem'; +import type { + ITaskService, + ITask, + ITaskInput, + ITaskUpdate, + ITaskCounts, + TaskStatus, +} from '../../../domain'; +import { emptyTaskCounts } from '../../../domain'; + +/** + * Create a git-mem MemoryService instance with default wiring. + */ +function createGitMemService(): GitMemMemoryService { + const notesService = new NotesService(); + const repository = new MemoryRepository(notesService); + return new GitMemMemoryService(repository); +} + +/** + * Extract task status from tags. + */ +function getStatus(tags: readonly string[]): TaskStatus { + for (const tag of tags) { + if (tag.startsWith('status:')) { + const s = tag.replace('status:', ''); + const valid: TaskStatus[] = ['ready', 'in-progress', 'blocked', 'done', 'closed']; + if (valid.includes(s as TaskStatus)) return s as TaskStatus; + } + } + return 'unknown'; +} + +/** + * Convert a git-mem memory entity to a Lisa ITask. + */ +function toTask(entity: IMemoryEntity): ITask { + const tags = [...entity.tags]; + return { + key: entity.id, + status: getStatus(tags), + title: entity.content, + blocked: tags + .filter(t => t.startsWith('blocked_by:')) + .map(t => t.replace('blocked_by:', '')), + created_at: entity.createdAt, + }; +} + +export class GitMemTaskAdapter implements ITaskService { + private readonly gitMem: GitMemMemoryService; + + constructor(gitMem?: GitMemMemoryService) { + this.gitMem = gitMem ?? createGitMemService(); + } + + async getTasks(): Promise { + return this.loadTasks(); + } + + async getTasksSimple(): Promise { + return this.loadTasks(); + } + + async getTaskCounts(): Promise { + const tasks = await this.loadTasks(); + const counts = { ...emptyTaskCounts() }; + for (const task of tasks) { + const key = task.status in counts ? task.status : 'unknown'; + (counts as Record)[key] += 1; + } + return counts; + } + + async createTask(task: ITaskInput): Promise { + const tags = ['task', `status:${task.status ?? 'ready'}`]; + if (task.blocked) { + for (const b of task.blocked) { + tags.push(`blocked_by:${b}`); + } + } + const entity = this.gitMem.remember(task.title, { tags }); + return toTask(entity); + } + + async updateTask(taskId: string, updates: ITaskUpdate): Promise { + const existing = this.gitMem.get(taskId); + if (!existing) { + throw new Error(`Task not found: ${taskId}`); + } + + // Delete old, create new with updated fields + this.gitMem.delete(taskId); + + const title = updates.title ?? existing.content; + const status = updates.status ?? getStatus(existing.tags); + const blocked = updates.blocked ?? existing.tags + .filter(t => t.startsWith('blocked_by:')) + .map(t => t.replace('blocked_by:', '')); + + const tags = ['task', `status:${status}`]; + for (const b of blocked) { + tags.push(`blocked_by:${b}`); + } + + // Preserve non-task-specific tags from original + for (const tag of existing.tags) { + if (tag !== 'task' && !tag.startsWith('status:') && !tag.startsWith('blocked_by:')) { + tags.push(tag); + } + } + + const entity = this.gitMem.remember(title, { tags }); + return toTask(entity); + } + + private loadTasks(): ITask[] { + const result = this.gitMem.recall(undefined, { tag: 'task', limit: 200 }); + return result.memories.map(toTask); + } +} diff --git a/src/lib/infrastructure/adapters/gitmem/index.ts b/src/lib/infrastructure/adapters/gitmem/index.ts new file mode 100644 index 0000000..90fb552 --- /dev/null +++ b/src/lib/infrastructure/adapters/gitmem/index.ts @@ -0,0 +1,2 @@ +export { GitMemAdapter } from './GitMemAdapter'; +export { GitMemTaskAdapter } from './GitMemTaskAdapter'; diff --git a/src/lib/infrastructure/services/CurationService.ts b/src/lib/infrastructure/services/CurationService.ts index 6ab092b..a037f86 100644 --- a/src/lib/infrastructure/services/CurationService.ts +++ b/src/lib/infrastructure/services/CurationService.ts @@ -85,24 +85,23 @@ export function createCurationService( ): ICurationService { return { async markFact( - groupId: string, uuid: string, mark: CurationMark ): Promise { const curationTag = resolveCurationTag(mark); // Add the curation tag to the fact - await memoryWriter.addFact(groupId, `__curate:${uuid}`, [curationTag]); + await memoryWriter.addFact(`__curate:${uuid}`, [curationTag]); // Side effects by mark if (mark === 'deprecated') { - await memoryWriter.expireFact(groupId, uuid); + await memoryWriter.expireFact(uuid); } if (mark === 'authoritative') { // Promote confidence to verified const confidenceTag = resolveConfidenceTag('verified'); - await memoryWriter.addFact(groupId, `__promote:${uuid}`, [confidenceTag]); + await memoryWriter.addFact(`__promote:${uuid}`, [confidenceTag]); } }, diff --git a/src/lib/infrastructure/services/GitMemMemoryService.ts b/src/lib/infrastructure/services/GitMemMemoryService.ts index 30562db..ae4bd30 100644 --- a/src/lib/infrastructure/services/GitMemMemoryService.ts +++ b/src/lib/infrastructure/services/GitMemMemoryService.ts @@ -32,14 +32,14 @@ function toMemoryItem(entity: IMemoryEntity): IMemoryItem { } /** - * Build the tags array for git-mem from groupId + optional Lisa tags/options. + * Build the tags array for git-mem from optional Lisa tags/options. + * Note: Group IDs are no longer used - the git repo itself provides scoping. */ function buildTags( - groupId: string, tags?: readonly string[], options?: IMemorySaveOptions ): string[] { - const result: string[] = [`group:${groupId}`]; + const result: string[] = []; if (tags) { result.push(...tags); @@ -72,19 +72,12 @@ export class GitMemMemoryService implements IMemoryService { constructor(private readonly gitMem: IGitMemMemoryService) {} async loadMemory( - groupIds: readonly string[], - _aliases: readonly string[], - _branch: string | null, _timeoutMs?: number ): Promise { const { memories } = this.gitMem.recall(undefined, { limit: 100 }); - const groupTags = groupIds.map(g => `group:${g}`); - const filtered = memories.filter(m => - groupTags.length === 0 || m.tags.some(t => groupTags.includes(t)) - ); - - const facts = filtered.map(toMemoryItem); + // No group filtering - git repo provides scoping + const facts = memories.map(toMemoryItem); // Separate init-review from facts const initReviewFact = facts.find(f => @@ -101,16 +94,13 @@ export class GitMemMemoryService implements IMemoryService { } async loadFactsDateOrdered( - groupIds: readonly string[], limit?: number, options?: IMemoryDateOptions ): Promise { const { memories } = this.gitMem.recall(undefined, { limit: limit || 50 }); - const groupTags = groupIds.map(g => `group:${g}`); - let filtered = memories.filter(m => - groupTags.length === 0 || m.tags.some(t => groupTags.includes(t)) - ); + // No group filtering - git repo provides scoping + let filtered = memories; if (options?.since) { const sinceTime = options.since.getTime(); @@ -126,7 +116,6 @@ export class GitMemMemoryService implements IMemoryService { } async searchFacts( - _groupIds: readonly string[], query: string, limit?: number ): Promise { @@ -134,43 +123,39 @@ export class GitMemMemoryService implements IMemoryService { return memories.map(toMemoryItem); } - async saveMemory(groupId: string, facts: readonly string[]): Promise { + async saveMemory(facts: readonly string[]): Promise { for (const fact of facts) { - this.gitMem.remember(fact, { - tags: [`group:${groupId}`], - }); + this.gitMem.remember(fact, {}); } } async addFact( - groupId: string, fact: string, tags?: readonly string[] ): Promise { this.gitMem.remember(fact, { - tags: buildTags(groupId, tags), + tags: buildTags(tags), }); } async addFactWithLifecycle( - groupId: string, fact: string, options: IMemorySaveOptions ): Promise { const confidence = options.confidence as ConfidenceLevel | undefined; this.gitMem.remember(fact, { - tags: buildTags(groupId, undefined, options), + tags: buildTags(undefined, options), lifecycle: options.lifecycle, confidence, }); } - async expireFact(_groupId: string, uuid: string): Promise { + async expireFact(uuid: string): Promise { this.gitMem.delete(uuid); } - async cleanupExpired(_groupId: string): Promise { + async cleanupExpired(): Promise { // git-mem doesn't support TTL-based cleanup yet return 0; } diff --git a/src/lib/infrastructure/services/GitMemTaskService.ts b/src/lib/infrastructure/services/GitMemTaskService.ts index 72302cf..ff59c67 100644 --- a/src/lib/infrastructure/services/GitMemTaskService.ts +++ b/src/lib/infrastructure/services/GitMemTaskService.ts @@ -6,7 +6,8 @@ * - 'task' tag to identify task memories * - 'task_id:' for unique identification * - 'status:' for current status - * - 'group:' for scoping + * + * Note: Group IDs are no longer used - the git repo provides scoping via git-mem. */ import type { IMemoryService as IGitMemMemoryService, IMemoryEntity } from 'git-mem/dist/index'; @@ -66,12 +67,11 @@ function toTask(entity: IMemoryEntity): ITask { } function buildTaskTags( - groupId: string, key: string, status: TaskStatus, blocked?: readonly string[] ): string[] { - const tags = [TASK_TAG, `task_id:${key}`, `status:${status}`, `group:${groupId}`]; + const tags = [TASK_TAG, `task_id:${key}`, `status:${status}`]; if (blocked) { for (const b of blocked) { tags.push(`blocked_by:${b}`); @@ -98,31 +98,22 @@ function deduplicateByTaskId(tasks: ITask[]): ITask[] { export class GitMemTaskService implements ITaskService { constructor(private readonly gitMem: IGitMemMemoryService) {} - async getTasks( - groupIds: readonly string[], - _aliases: readonly string[], - _branch: string | null - ): Promise { - return this.getTasksSimple(groupIds); + async getTasks(): Promise { + return this.getTasksSimple(); } - async getTasksSimple(groupIds: readonly string[]): Promise { + async getTasksSimple(): Promise { const { memories } = this.gitMem.recall(undefined, { tag: TASK_TAG, limit: 200, }); - const groupTags = groupIds.map(g => `group:${g}`); - const filtered = memories.filter(m => - groupTags.length === 0 || m.tags.some(t => groupTags.includes(t)) - ); - - const tasks = filtered.map(toTask); + const tasks = memories.map(toTask); return deduplicateByTaskId(tasks); } - async getTaskCounts(groupIds: readonly string[]): Promise { - const tasks = await this.getTasksSimple(groupIds); + async getTaskCounts(): Promise { + const tasks = await this.getTasksSimple(); const counts = emptyTaskCounts(); const mutable = counts as unknown as Record; @@ -137,12 +128,12 @@ export class GitMemTaskService implements ITaskService { return counts; } - async createTask(groupId: string, task: ITaskInput): Promise { + async createTask(task: ITaskInput): Promise { const key = `task-${Date.now()}`; const status = task.status || 'ready'; this.gitMem.remember(`TASK: ${task.title}`, { - tags: buildTaskTags(groupId, key, status, task.blocked), + tags: buildTaskTags(key, status, task.blocked), }); return { @@ -154,13 +145,9 @@ export class GitMemTaskService implements ITaskService { }; } - async updateTask( - groupId: string, - taskId: string, - updates: ITaskUpdate - ): Promise { + async updateTask(taskId: string, updates: ITaskUpdate): Promise { // Find existing task - const tasks = await this.getTasksSimple([groupId]); + const tasks = await this.getTasksSimple(); const existing = tasks.find(t => t.key === taskId); const title = updates.title ?? existing?.title ?? 'Unknown task'; @@ -169,7 +156,7 @@ export class GitMemTaskService implements ITaskService { // Create updated task memory (new memory with same task_id, latest wins) this.gitMem.remember(`TASK: ${title}`, { - tags: buildTaskTags(groupId, taskId, status, blocked), + tags: buildTaskTags(taskId, status, blocked), }); return { diff --git a/src/lib/infrastructure/services/NlCurationService.ts b/src/lib/infrastructure/services/NlCurationService.ts index a5017bf..8a38991 100644 --- a/src/lib/infrastructure/services/NlCurationService.ts +++ b/src/lib/infrastructure/services/NlCurationService.ts @@ -45,7 +45,7 @@ export function createNlCurationService( logger?: ILogger ): INlCurationService { return { - async plan(input: string, _groupId: string): Promise { + async plan(input: string): Promise { const prompt = buildCurationPrompt(input); const response = await llmGuard.complete(prompt.user, 'curation', { @@ -65,14 +65,13 @@ export function createNlCurationService( }; }, - async execute(plan: INlCurationPlan, groupId: string): Promise { + async execute(plan: INlCurationPlan): Promise { const outputParts: string[] = []; for (const operation of plan.operations) { try { const result = await executeOperation( operation, - groupId, memoryService, curationService, consolidationService, @@ -214,7 +213,6 @@ function fallbackPlan(): { */ async function executeOperation( operation: INlOperation, - groupId: string, memoryService: IMemoryService, curationService: ICurationService, consolidationService: IConsolidationService, @@ -230,7 +228,7 @@ async function executeOperation( ? operation.params.limit : 10; - const facts = await memoryService.searchFacts([groupId], query, limit); + const facts = await memoryService.searchFacts(query, limit); if (facts.length === 0) { return `No facts found matching "${query}".`; @@ -254,7 +252,7 @@ async function executeOperation( return `Invalid curation mark: "${mark}". Valid marks: authoritative, draft, deprecated, needs-review.`; } - const facts = await memoryService.searchFacts([groupId], query, 5); + const facts = await memoryService.searchFacts(query, 5); if (facts.length === 0) { return `No facts found matching "${query}" to mark as ${mark}.`; @@ -264,7 +262,7 @@ async function executeOperation( for (const fact of facts) { if (!fact.uuid) continue; try { - await curationService.markFact(groupId, fact.uuid, mark as CurationMark); + await curationService.markFact(fact.uuid, mark as CurationMark); marked++; } catch (error) { logger?.debug('Failed to mark fact', { @@ -281,7 +279,7 @@ async function executeOperation( ? operation.params.query : ''; - const facts = await memoryService.searchFacts([groupId], query, 20); + const facts = await memoryService.searchFacts(query, 20); if (facts.length === 0) { return `No facts found matching "${query}" to expire.`; @@ -291,7 +289,7 @@ async function executeOperation( for (const fact of facts) { if (!fact.uuid) continue; try { - await memoryService.expireFact(groupId, fact.uuid); + await memoryService.expireFact(fact.uuid); expired++; } catch (error) { logger?.debug('Failed to expire fact', { @@ -309,7 +307,7 @@ async function executeOperation( : undefined; const style = operation.params.style === 'detailed' ? 'detailed' : 'concise'; - const result = await summarizationService.summarize(groupId, { + const result = await summarizationService.summarize({ topic, style, }); @@ -322,7 +320,7 @@ async function executeOperation( ? operation.params.query : ''; - const facts = await memoryService.searchFacts([groupId], query, 10); + const facts = await memoryService.searchFacts(query, 10); const uuids = facts.filter(f => f.uuid).map(f => f.uuid!); if (uuids.length < 2) { @@ -330,7 +328,6 @@ async function executeOperation( } const result = await consolidationService.consolidate( - groupId, uuids, 'archive-duplicates' ); diff --git a/src/lib/infrastructure/services/RecursionService.ts b/src/lib/infrastructure/services/RecursionService.ts index 927d313..dd6951b 100644 --- a/src/lib/infrastructure/services/RecursionService.ts +++ b/src/lib/infrastructure/services/RecursionService.ts @@ -98,7 +98,7 @@ export class RecursionService implements IRecursionService { /** * Run memory recursion for a plan mode prompt. */ - async run(prompt: string, groupIds: readonly string[]): Promise { + async run(prompt: string): Promise { // Extract topics from prompt const topics = this.extractTopics(prompt); if (topics.length === 0) { @@ -114,9 +114,9 @@ export class RecursionService implements IRecursionService { }); const searchPromise = Promise.all([ - this.searchByType(groupIds, query, 'decision'), - this.searchByType(groupIds, query, 'retrospective'), - this.searchTasks(groupIds, query), + this.searchByType(query, 'decision'), + this.searchByType(query, 'retrospective'), + this.searchTasks(query), ]); let decisions: IMemoryItem[] = []; @@ -181,14 +181,12 @@ export class RecursionService implements IRecursionService { * Search memory for facts of a specific type. */ private async searchByType( - groupIds: readonly string[], query: string, type: string ): Promise { try { // Search with the query - the memory service will filter by tags if supported const results = await this.memory.searchFacts( - groupIds, `${type} ${query}`, this.config.maxResultsPerType * 2 ); @@ -207,14 +205,11 @@ export class RecursionService implements IRecursionService { /** * Search for related tasks. */ - private async searchTasks( - groupIds: readonly string[], - query: string - ): Promise { + private async searchTasks(query: string): Promise { try { // Use task service to get tasks - const allTasks = await this.tasks.getTasksSimple(groupIds); - + const allTasks = await this.tasks.getTasksSimple(); + // Filter tasks by query relevance (simple keyword matching) const queryWords = query.toLowerCase().split(/\s+/); const relevant = allTasks diff --git a/src/lib/infrastructure/services/SkillMemoryService.ts b/src/lib/infrastructure/services/SkillMemoryService.ts index f3cf058..0ce8002 100644 --- a/src/lib/infrastructure/services/SkillMemoryService.ts +++ b/src/lib/infrastructure/services/SkillMemoryService.ts @@ -4,6 +4,9 @@ * * This is the "rich" memory service with full CLI capabilities: * load, add, expire, cleanup, conflicts, dedupe, curate, consolidate. + * + * Note: Group IDs are no longer used - the git repo itself provides scoping + * via git-mem (git notes in refs/notes/mem). */ import type { IMemoryService as IGitMemMemoryService, IMemoryEntity } from 'git-mem/dist/index'; @@ -64,12 +67,11 @@ function resolveTag(text: string, options: IMemoryAddOptions): string | undefine /** * Map a git-mem IMemoryEntity to the skills IFact. */ -function toFact(entity: IMemoryEntity, groupId: string): IFact { +function toFact(entity: IMemoryEntity): IFact { return { uuid: entity.id, name: entity.content.slice(0, 80), fact: entity.content, - group_id: groupId, created_at: entity.createdAt, }; } @@ -92,7 +94,6 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): return { async load( - groupIds: string[], query: string, limit: number, options?: IMemoryLoadOptions @@ -100,11 +101,8 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): const searchQuery = (query && query !== '*') ? query : undefined; const { memories } = gitMem.recall(searchQuery, { limit }); - // Client-side group filtering via tags - const groupTags = groupIds.map(g => `group:${g}`); - let filtered = memories.filter(m => - groupTags.length === 0 || m.tags.some(t => groupTags.includes(t)) - ); + // No group filtering - git repo provides scoping + let filtered = memories; // Client-side date filtering if (options?.since) { @@ -116,13 +114,11 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): filtered = filtered.filter(m => new Date(m.createdAt).getTime() <= untilTime); } - const facts: IFact[] = filtered.map(m => toFact(m, groupIds[0] || '')); + const facts: IFact[] = filtered.map(m => toFact(m)); return { status: 'ok', action: 'load', - group: groupIds[0] || '', - groups: groupIds, query: query || '', facts, mode: 'git-mem', @@ -131,12 +127,11 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): async add( text: string, - groupId: string, options: IMemoryAddOptions ): Promise { const tag = resolveTag(text, options); - const tags: string[] = [`group:${groupId}`]; + const tags: string[] = []; if (tag) { // Tags containing ':' are namespaced, store as-is. // Simple tags get 'type:' prefix. @@ -149,7 +144,6 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): return { status: 'ok', action: 'add', - group: groupId, text, tag, mode: 'git-mem', @@ -157,7 +151,6 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): }, async expire( - groupId: string, uuid: string ): Promise { const found = gitMem.delete(uuid); @@ -165,7 +158,6 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): return { status: 'ok', action: 'expire', - group: groupId, uuid, found, mode: 'git-mem', @@ -173,14 +165,12 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): }, async cleanup( - groupId: string, _dryRun: boolean ): Promise { // git-mem doesn't support TTL-based cleanup yet return { status: 'ok', action: 'cleanup', - group: groupId, expiredCount: 0, dryRun: _dryRun, mode: 'git-mem', @@ -188,25 +178,20 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): }, async conflicts( - groupIds: string[], topic?: string ): Promise { const { memories } = gitMem.recall(undefined, { limit: 200 }); - // Filter by group - const groupTags = groupIds.map(g => `group:${g}`); - const filtered = memories.filter(m => - groupTags.length === 0 || m.tags.some(t => groupTags.includes(t)) - ); + // No group filtering - git repo provides scoping // Group by type:* tags const typeGroups = new Map(); - for (const m of filtered) { + for (const m of memories) { const typeTags = m.tags.filter(t => t.startsWith('type:')); for (const typeTag of typeTags) { if (topic && typeTag !== topic) continue; const existing = typeGroups.get(typeTag) || []; - existing.push(toFact(m, groupIds[0] || '')); + existing.push(toFact(m)); typeGroups.set(typeTag, existing); } } @@ -226,8 +211,6 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): return { status: 'ok', action: 'conflicts', - group: groupIds[0] || '', - groups: groupIds, topic: topic || '', conflictGroups, totalConflicts: conflictGroups.length, @@ -236,7 +219,6 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): }, async dedupe( - groupId: string, options?: { minSimilarity?: number; limit?: number; since?: Date } ): Promise { const minSimilarity = options?.minSimilarity ?? 0.6; @@ -244,9 +226,8 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): const { memories } = gitMem.recall(undefined, { limit: 500 }); - // Filter by group and date - const groupTag = `group:${groupId}`; - let filtered = memories.filter(m => m.tags.includes(groupTag)); + // No group filtering - git repo provides scoping + let filtered = memories; if (options?.since) { const sinceTime = options.since.getTime(); @@ -296,7 +277,6 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): uuid: f.uuid ?? '', name: f.name ?? '', fact: f.fact ?? '', - group_id: groupId, created_at: f.created_at ?? '', })), similarity: g.similarity, @@ -307,7 +287,6 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): return { status: 'ok', action: 'dedupe', - group: groupId, totalFactsScanned: facts.length, duplicateGroups: skillGroups, totalDuplicates, @@ -317,7 +296,6 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): }, async curate( - groupId: string, uuid: string, mark: CurationMark ): Promise { @@ -330,7 +308,7 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): const existing = memories.find(m => m.id === uuid); if (!existing) { - throw new Error(`Fact not found: uuid="${uuid}" in group="${groupId}"`); + throw new Error(`Fact not found: uuid="${uuid}"`); } const curationTag = resolveCurationTag(mark); @@ -357,7 +335,6 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): return { status: 'ok', action: 'curate', - group: groupId, uuid, mark, mode: 'git-mem', @@ -365,7 +342,6 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): }, async consolidate( - groupId: string, factUuids: string[], action: ConsolidationAction, options?: { retainUuid?: string; mergedText?: string } @@ -384,7 +360,6 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): return { status: 'ok', action: 'consolidate', - group: groupId, consolidationAction: 'keep-all', retainedUuid: factUuids[0] ?? '', archivedUuids: [], @@ -401,7 +376,7 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): // Add merged fact gitMem.remember(mergedText, { - tags: [`group:${groupId}`, 'source:consolidation'], + tags: ['source:consolidation'], }); // Delete all originals @@ -412,7 +387,6 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): return { status: 'ok', action: 'consolidate', - group: groupId, consolidationAction: 'merge', retainedUuid: 'new-merged-fact', archivedUuids: [...factUuids], @@ -432,7 +406,6 @@ export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): return { status: 'ok', action: 'consolidate', - group: groupId, consolidationAction: 'archive-duplicates', retainedUuid: retainUuid, archivedUuids: archiveUuids, diff --git a/src/lib/infrastructure/services/SkillPromptService.ts b/src/lib/infrastructure/services/SkillPromptService.ts index 8e49f68..a331d70 100644 --- a/src/lib/infrastructure/services/SkillPromptService.ts +++ b/src/lib/infrastructure/services/SkillPromptService.ts @@ -2,6 +2,9 @@ * Prompt service - captures user prompts to git-mem. * * Uses fingerprinting to detect and skip duplicate prompts. + * + * Note: Group IDs are no longer used - the git repo itself provides scoping + * via git-mem (git notes in refs/notes/mem). */ import crypto from 'crypto'; import type { IMemoryService as IGitMemMemoryService } from 'git-mem/dist/index'; @@ -33,7 +36,7 @@ export function createSkillPromptService(deps: ISkillPromptServiceDependencies): }, async addPrompt(args: IPromptArgs): Promise { - const { text, role = 'user', source = 'user-prompt', force = false, groupId } = args; + const { text, role = 'user', source = 'user-prompt', force = false } = args; if (!text) throw new Error('prompt requires text'); @@ -50,7 +53,6 @@ export function createSkillPromptService(deps: ISkillPromptServiceDependencies): } const tags = [ - `group:${groupId}`, fpTag, `role:${role}`, `source:${source}`, @@ -59,7 +61,7 @@ export function createSkillPromptService(deps: ISkillPromptServiceDependencies): gitMem.remember(text, { tags }); - return { status: 'ok', action: 'add', group: groupId, role, source }; + return { status: 'ok', action: 'add', role, source }; }, }; } diff --git a/src/lib/infrastructure/services/SkillTaskService.ts b/src/lib/infrastructure/services/SkillTaskService.ts index e2e8511..a6aced2 100644 --- a/src/lib/infrastructure/services/SkillTaskService.ts +++ b/src/lib/infrastructure/services/SkillTaskService.ts @@ -4,6 +4,9 @@ * * This is the "rich" task service with full CLI capabilities: * list, add, update, link, unlink, listLinked. + * + * Note: Group IDs are no longer used - the git repo itself provides scoping + * via git-mem (git notes in refs/notes/mem). */ import type { IMemoryService as IGitMemMemoryService } from 'git-mem/dist/index'; import type { @@ -42,9 +45,11 @@ function parseTaskContent(content: string): Record | null { * Creates a skill task service instance backed by git-mem. * * Tasks are stored as git-mem memories with: - * - Tags: `task`, `group:`, `status:`, `task_id:` + * - Tags: `task`, `status:` * - Content: JSON `{ type: "task", title, status, repo, assignee, ... }` * + * Note: Group IDs are no longer used - the git repo itself provides scoping. + * * @param deps - Service dependencies * @returns Skill task service implementation */ @@ -53,7 +58,6 @@ export function createSkillTaskService(deps: ISkillTaskServiceDependencies): ISk return { async list( - groupIds: string[], limit: number, defaultRepo: string, defaultAssignee: string, @@ -61,12 +65,8 @@ export function createSkillTaskService(deps: ISkillTaskServiceDependencies): ISk ): Promise { const { memories } = gitMem.recall(undefined, { limit: 500 }); - // Filter by group and task tag - const groupTags = groupIds.map(g => `group:${g}`); - let filtered = memories.filter(m => - m.tags.includes('task') && - (groupTags.length === 0 || m.tags.some(t => groupTags.includes(t))) - ); + // Filter by task tag only (no group filtering needed) + let filtered = memories.filter(m => m.tags.includes('task')); // Date filtering if (options?.since) { @@ -122,8 +122,6 @@ export function createSkillTaskService(deps: ISkillTaskServiceDependencies): ISk return { status: 'ok', action: 'list', - group: groupIds[0] || '', - groups: groupIds, tasks, mode: 'git-mem', }; @@ -131,7 +129,6 @@ export function createSkillTaskService(deps: ISkillTaskServiceDependencies): ISk async add( title: string, - groupId: string, options: ITaskWriteOptions ): Promise { const taskObj = { @@ -147,7 +144,6 @@ export function createSkillTaskService(deps: ISkillTaskServiceDependencies): ISk const tags = [ 'task', - `group:${groupId}`, `status:${taskObj.status}`, ]; @@ -157,14 +153,12 @@ export function createSkillTaskService(deps: ISkillTaskServiceDependencies): ISk status: 'ok', action: 'add', task: taskObj, - group: groupId, mode: 'git-mem', }; }, async update( title: string, - groupId: string, options: ITaskWriteOptions ): Promise { const taskObj = { @@ -180,11 +174,7 @@ export function createSkillTaskService(deps: ISkillTaskServiceDependencies): ISk // Find and delete old version(s) of this task by title const { memories } = gitMem.recall(undefined, { limit: 500 }); - const groupTag = `group:${groupId}`; - const existing = memories.filter(m => - m.tags.includes('task') && - m.tags.includes(groupTag) - ); + const existing = memories.filter(m => m.tags.includes('task')); for (const m of existing) { const old = parseTaskContent(m.content); @@ -196,7 +186,6 @@ export function createSkillTaskService(deps: ISkillTaskServiceDependencies): ISk // Remember updated version const tags = [ 'task', - `group:${groupId}`, `status:${taskObj.status}`, ]; @@ -206,14 +195,12 @@ export function createSkillTaskService(deps: ISkillTaskServiceDependencies): ISk status: 'ok', action: 'update', task: taskObj, - group: groupId, mode: 'git-mem', }; }, async link( taskUuid: string, - groupId: string, externalLink: ITaskExternalLink ): Promise { const { memories } = gitMem.recall(undefined, { limit: 500 }); @@ -235,10 +222,7 @@ export function createSkillTaskService(deps: ISkillTaskServiceDependencies): ISk // Delete old, remember updated gitMem.delete(taskUuid); - const tags = existing.tags.length > 0 ? [...existing.tags] : [ - 'task', - `group:${groupId}`, - ]; + const tags = existing.tags.length > 0 ? [...existing.tags] : ['task']; gitMem.remember(JSON.stringify(taskObj), { tags }); return { @@ -249,14 +233,12 @@ export function createSkillTaskService(deps: ISkillTaskServiceDependencies): ISk uuid: taskUuid, externalLink: taskObj.externalLink as ITaskExternalLink, }, - group: groupId, mode: 'git-mem', }; }, async unlink( - taskUuid: string, - groupId: string + taskUuid: string ): Promise { const { memories } = gitMem.recall(undefined, { limit: 500 }); const existing = memories.find(m => m.id === taskUuid); @@ -274,10 +256,7 @@ export function createSkillTaskService(deps: ISkillTaskServiceDependencies): ISk // Delete old, remember updated gitMem.delete(taskUuid); - const tags = existing.tags.length > 0 ? [...existing.tags] : [ - 'task', - `group:${groupId}`, - ]; + const tags = existing.tags.length > 0 ? [...existing.tags] : ['task']; gitMem.remember(JSON.stringify(taskObj), { tags }); return { @@ -287,19 +266,17 @@ export function createSkillTaskService(deps: ISkillTaskServiceDependencies): ISk title: String(taskObj.title), uuid: taskUuid, }, - group: groupId, mode: 'git-mem', }; }, async listLinked( - groupIds: string[], source: ExternalLinkSource | undefined, limit: number, defaultRepo: string, defaultAssignee: string ): Promise { - const result = await this.list(groupIds, limit * 2, defaultRepo, defaultAssignee); + const result = await this.list(limit * 2, defaultRepo, defaultAssignee); let linkedTasks = result.tasks.filter((t) => t.externalLink); if (source) { diff --git a/src/lib/infrastructure/services/SummarizationService.ts b/src/lib/infrastructure/services/SummarizationService.ts index 3826fb4..d428fef 100644 --- a/src/lib/infrastructure/services/SummarizationService.ts +++ b/src/lib/infrastructure/services/SummarizationService.ts @@ -79,7 +79,6 @@ export function createSummarizationService( ): ISummarizationService { return { async summarize( - groupId: string, options: ISummarizationOptions = {} ): Promise { const maxFacts = options.maxFacts ?? DEFAULT_MAX_FACTS; @@ -88,7 +87,7 @@ export function createSummarizationService( let facts: IMemoryItem[]; if (options.topic) { - facts = await memoryService.searchFacts([groupId], options.topic, maxFacts); + facts = await memoryService.searchFacts(options.topic, maxFacts); // Post-filter by since date — searchFacts doesn't support date filtering if (options.since) { const sinceMs = options.since.getTime(); @@ -100,7 +99,7 @@ export function createSummarizationService( } else { const dateOpts: IMemoryDateOptions = {}; if (options.since) dateOpts.since = options.since; - facts = await memoryService.loadFactsDateOrdered([groupId], maxFacts, dateOpts); + facts = await memoryService.loadFactsDateOrdered(maxFacts, dateOpts); } if (facts.length === 0) { @@ -114,7 +113,6 @@ export function createSummarizationService( } logger?.debug('Summarization: loaded facts', { - groupId, factCount: facts.length, topic: options.topic, since: options.since?.toISOString(), @@ -136,7 +134,6 @@ export function createSummarizationService( const timeRange = computeTimeRange(facts); logger?.debug('Summarization: complete', { - groupId, factCount: facts.length, topicCount: topics.length, inputTokens: response.usage.inputTokens, diff --git a/src/lib/infrastructure/services/skill-interfaces/ISkillMemoryService.ts b/src/lib/infrastructure/services/skill-interfaces/ISkillMemoryService.ts index 069ade2..b09116a 100644 --- a/src/lib/infrastructure/services/skill-interfaces/ISkillMemoryService.ts +++ b/src/lib/infrastructure/services/skill-interfaces/ISkillMemoryService.ts @@ -4,6 +4,9 @@ * * This is the "rich" memory interface used by CLI scripts, * with operations like dedupe, curate, conflicts, consolidate. + * + * Note: Group IDs are no longer used - the git repo itself provides scoping + * via git-mem (git notes in refs/notes/mem). */ import type { CurationMark } from '../../../domain/interfaces/ICurationService'; import type { ConsolidationAction } from '../../../domain/interfaces/IConsolidationService'; @@ -15,7 +18,6 @@ export interface IFact { uuid: string; name: string; fact: string; - group_id: string; created_at: string; valid_at?: string; expired_at?: string | null; @@ -32,8 +34,6 @@ export type MemoryMode = 'git-mem'; export interface IMemoryLoadResult { status: 'ok'; action: 'load'; - group: string; - groups: string[]; query: string; facts: IFact[]; mode: MemoryMode; @@ -45,7 +45,6 @@ export interface IMemoryLoadResult { export interface IMemoryAddResult { status: 'ok'; action: 'add'; - group: string; text: string; tag?: string; result?: unknown; @@ -77,7 +76,6 @@ export interface IMemoryLoadOptions { export interface IMemoryExpireResult { status: 'ok'; action: 'expire'; - group: string; uuid: string; found: boolean; mode: MemoryMode; @@ -89,7 +87,6 @@ export interface IMemoryExpireResult { export interface IMemoryCleanupResult { status: 'ok'; action: 'cleanup'; - group: string; expiredCount: number; dryRun: boolean; mode: MemoryMode; @@ -110,8 +107,6 @@ export interface IConflictGroup { export interface IMemoryConflictsResult { status: 'ok'; action: 'conflicts'; - group: string; - groups: string[]; topic: string; conflictGroups: IConflictGroup[]; totalConflicts: number; @@ -134,7 +129,6 @@ export interface IDuplicateGroup { export interface IMemoryDedupeResult { status: 'ok'; action: 'dedupe'; - group: string; totalFactsScanned: number; duplicateGroups: IDuplicateGroup[]; totalDuplicates: number; @@ -148,7 +142,6 @@ export interface IMemoryDedupeResult { export interface IMemoryCurateResult { status: 'ok'; action: 'curate'; - group: string; uuid: string; mark: CurationMark; mode: MemoryMode; @@ -160,7 +153,6 @@ export interface IMemoryCurateResult { export interface IMemoryConsolidateResult { status: 'ok'; action: 'consolidate'; - group: string; consolidationAction: ConsolidationAction; retainedUuid: string; archivedUuids: string[]; @@ -174,18 +166,18 @@ export interface IMemoryConsolidateResult { * This is the "rich" interface with full CLI capabilities. * Contrast with IMemoryService in domain/interfaces which is * the simpler infrastructure interface. + * + * Note: Group IDs are no longer used - the git repo provides scoping. */ export interface ISkillMemoryService { /** * Load memories/facts from storage. * - * @param groupIds - Group identifiers to search * @param query - Optional search query (empty string or '*' for all) * @param limit - Maximum number of facts to return * @param options - Optional date filtering options */ load( - groupIds: string[], query: string, limit: number, options?: IMemoryLoadOptions @@ -195,82 +187,56 @@ export interface ISkillMemoryService { * Add a new memory/fact. * * @param text - Memory text content - * @param groupId - Group identifier for storage * @param options - Additional options (tag, type, source) */ - add( - text: string, - groupId: string, - options: IMemoryAddOptions - ): Promise; + add(text: string, options: IMemoryAddOptions): Promise; /** * Expire a single fact by UUID. * - * @param groupId - Group identifier * @param uuid - UUID of the fact to expire */ - expire( - groupId: string, - uuid: string - ): Promise; + expire(uuid: string): Promise; /** * Clean up expired facts based on lifecycle TTL defaults. * - * @param groupId - Group identifier * @param dryRun - If true, count without expiring */ - cleanup( - groupId: string, - dryRun: boolean - ): Promise; + cleanup(dryRun: boolean): Promise; /** * Find groups of potentially conflicting facts. * - * @param groupIds - Group identifiers to search * @param topic - Optional topic tag to filter by */ - conflicts( - groupIds: string[], - topic?: string - ): Promise; + conflicts(topic?: string): Promise; /** - * Detect duplicate facts within a group. + * Detect duplicate facts. * - * @param groupId - Group identifier to scan * @param options - Detection options */ dedupe( - groupId: string, options?: { minSimilarity?: number; limit?: number; since?: Date } ): Promise; /** * Mark a fact with a curation status. * - * @param groupId - Group identifier * @param uuid - UUID of the fact to mark * @param mark - Curation mark (authoritative, draft, deprecated, needs-review) */ - curate( - groupId: string, - uuid: string, - mark: CurationMark - ): Promise; + curate(uuid: string, mark: CurationMark): Promise; /** * Consolidate multiple facts. * - * @param groupId - Group identifier * @param factUuids - UUIDs of facts to consolidate (minimum 2) * @param action - Consolidation action (merge, archive-duplicates, keep-all) * @param options - Additional options (retainUuid, mergedText) */ consolidate( - groupId: string, factUuids: string[], action: ConsolidationAction, options?: { retainUuid?: string; mergedText?: string } diff --git a/src/lib/infrastructure/services/skill-interfaces/ISkillPromptService.ts b/src/lib/infrastructure/services/skill-interfaces/ISkillPromptService.ts index 188c249..6c8296e 100644 --- a/src/lib/infrastructure/services/skill-interfaces/ISkillPromptService.ts +++ b/src/lib/infrastructure/services/skill-interfaces/ISkillPromptService.ts @@ -1,6 +1,9 @@ /** * Prompt service interface for skill scripts. * Captures user prompts to git-mem with deduplication. + * + * Note: Group IDs are no longer used - the git repo itself provides scoping + * via git-mem (git notes in refs/notes/mem). */ /** @@ -11,7 +14,6 @@ export interface IPromptArgs { role?: string; source?: string; force?: boolean; - groupId: string; } /** @@ -20,6 +22,7 @@ export interface IPromptArgs { export interface IPromptResult { status: 'ok' | 'skipped'; action?: 'add'; + /** @deprecated Group IDs are no longer used; always undefined. */ group?: string; role?: string; source?: string; diff --git a/src/lib/infrastructure/services/skill-interfaces/ISkillTaskService.ts b/src/lib/infrastructure/services/skill-interfaces/ISkillTaskService.ts index 0fa19e8..cd5bdf9 100644 --- a/src/lib/infrastructure/services/skill-interfaces/ISkillTaskService.ts +++ b/src/lib/infrastructure/services/skill-interfaces/ISkillTaskService.ts @@ -4,6 +4,9 @@ * * This is the "rich" task interface used by CLI scripts, * with operations like external linking (GitHub, Jira, Linear). + * + * Note: Group IDs are no longer used - the git repo itself provides scoping + * via git-mem (git notes in refs/notes/mem). */ /** @@ -47,8 +50,6 @@ export interface ISkillTask { export interface ITaskListResult { status: 'ok'; action: 'list'; - group: string; - groups: string[]; tasks: ISkillTask[]; mode: TaskMode; } @@ -69,7 +70,6 @@ export interface ITaskWriteResult { tag?: string | null; externalLink?: ITaskExternalLink; }; - group: string; result?: unknown; message_uuid?: string; mode: TaskMode; @@ -106,7 +106,6 @@ export interface ITaskLinkResult { uuid: string; externalLink?: ITaskExternalLink; }; - group: string; mode: TaskMode; } @@ -115,19 +114,19 @@ export interface ITaskLinkResult { * * This is the "rich" interface with full CLI capabilities * including external system linking. + * + * Note: Group IDs are no longer used - the git repo provides scoping. */ export interface ISkillTaskService { /** * List tasks from storage. * - * @param groupIds - Group identifiers to search * @param limit - Maximum number of tasks to return * @param defaultRepo - Default repo name for tasks without one * @param defaultAssignee - Default assignee for tasks without one * @param options - Optional date filtering options */ list( - groupIds: string[], limit: number, defaultRepo: string, defaultAssignee: string, @@ -138,63 +137,42 @@ export interface ISkillTaskService { * Add a new task. * * @param title - Task title/description - * @param groupId - Group identifier for storage * @param options - Additional task options */ - add( - title: string, - groupId: string, - options: ITaskWriteOptions - ): Promise; + add(title: string, options: ITaskWriteOptions): Promise; /** * Update an existing task (creates a new version). * * @param title - Task title/description - * @param groupId - Group identifier for storage * @param options - Updated task options */ - update( - title: string, - groupId: string, - options: ITaskWriteOptions - ): Promise; + update(title: string, options: ITaskWriteOptions): Promise; /** * Link a task to an external system (GitHub, Jira, etc.). * * @param taskUuid - UUID of the task to link - * @param groupId - Group identifier * @param externalLink - External link details */ - link( - taskUuid: string, - groupId: string, - externalLink: ITaskExternalLink - ): Promise; + link(taskUuid: string, externalLink: ITaskExternalLink): Promise; /** * Unlink a task from its external system. * * @param taskUuid - UUID of the task to unlink - * @param groupId - Group identifier */ - unlink( - taskUuid: string, - groupId: string - ): Promise; + unlink(taskUuid: string): Promise; /** * List tasks filtered by external link source. * - * @param groupIds - Group identifiers to search * @param source - External link source to filter by (optional, returns all linked if omitted) * @param limit - Maximum number of tasks to return * @param defaultRepo - Default repo name for tasks without one * @param defaultAssignee - Default assignee for tasks without one */ listLinked( - groupIds: string[], source: ExternalLinkSource | undefined, limit: number, defaultRepo: string, diff --git a/src/lib/skills/common/group-id.ts b/src/lib/skills/common/group-id.ts deleted file mode 100644 index 28a1ba0..0000000 --- a/src/lib/skills/common/group-id.ts +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Group ID Utilities - * - * Shared functions for normalizing paths to Graphiti group IDs. - * Used by memory, tasks, prompt, and other skills. - */ - -import * as path from 'path'; -import * as os from 'os'; - -export const MAX_GROUP_ID_LENGTH = 128; - -/** - * Normalize a path to a valid group ID string. - * Works cross-platform (Windows and Unix). - * - * @example - * normalizePathToGroupId('/Users/tony.casey/Repos/api') // 'users-tony_casey-repos-api' - * normalizePathToGroupId('C:\\dev\\lisa') // 'c-dev-lisa' - */ -export function normalizePathToGroupId(absolutePath: string): string { - let normalized = absolutePath - .toLowerCase() - .replace(/^[a-z]:/i, (match) => match.charAt(0)) // C: -> c - .replace(/^\//, '') // Remove leading slash (Unix) - .replace(/\\/g, '-') // Backslash to dash (Windows) - .replace(/\//g, '-') // Forward slash to dash (Unix) - .replace(/\./g, '_') // Dots to underscores - .replace(/^-+/, '') // Remove leading dashes - .replace(/-+/g, '-'); // Collapse multiple dashes - - if (normalized.length > MAX_GROUP_ID_LENGTH) { - normalized = normalized.slice(-MAX_GROUP_ID_LENGTH); - } - return normalized; -} - -/** - * Get the current folder's group ID. - */ -export function getCurrentGroupId(cwd: string = process.cwd()): string { - return normalizePathToGroupId(cwd); -} - -/** - * Check if we're on Windows - */ -export function isWindows(): boolean { - return os.platform() === 'win32'; -} - -/** - * Get the root boundary for hierarchical traversal. - * - Windows: drive root (e.g., C:\) or home directory, whichever is deeper - * - Unix: home directory or / - */ -export function getRootBoundary(cwd: string = process.cwd()): string { - const homeDir = os.homedir(); - if (isWindows()) { - // On Windows, check if cwd is under home directory - const cwdLower = cwd.toLowerCase(); - const homeLower = homeDir.toLowerCase(); - if (cwdLower.startsWith(homeLower)) { - return homeDir; // Under home, use home as boundary - } - // Not under home (e.g., C:\dev\), use drive root as boundary - const driveRoot = path.parse(cwd).root; // e.g., "C:\" - return driveRoot; - } - // Unix: use home directory if under it, otherwise use / - if (cwd.startsWith(homeDir)) { - return homeDir; - } - return '/'; -} - -/** - * Get hierarchical group IDs from current folder up to root boundary. - * Returns array ordered from most specific (current) to least specific (root). - * Works cross-platform (Windows and Unix). - */ -export function getHierarchicalGroupIds(cwd: string = process.cwd()): string[] { - const rootBoundary = getRootBoundary(cwd); - const groups: string[] = []; - let currentPath = path.resolve(cwd); - const maxDepth = 10; // Safety limit - let depth = 0; - - while (depth < maxDepth) { - groups.push(normalizePathToGroupId(currentPath)); - // Stop if we've reached the root boundary - if (currentPath.toLowerCase() === rootBoundary.toLowerCase()) { - break; - } - const parentPath = path.dirname(currentPath); - // Stop if we can't go up anymore (reached filesystem root) - if (parentPath === currentPath) { - break; - } - currentPath = parentPath; - depth++; - } - return groups; -} - -/** - * Entity type to tag mapping for memory classification. - */ -export const TYPE_MAP: Record = { - // Code & Architecture - 'decision': 'code:decision', - 'pattern': 'code:pattern', - 'dependency': 'code:dependency', - 'tech-debt': 'code:tech-debt', - // Context & History - 'bug': 'context:bug', - 'rationale': 'context:rationale', - 'failed': 'context:failed', - 'quirk': 'context:quirk', - // External - 'feedback': 'external:feedback', - 'incident': 'external:incident', - 'contract': 'external:contract', - // People & Process - 'contributor': 'people:contributor', - 'review': 'people:review', - 'blocker': 'people:blocker', - 'estimate': 'people:estimate', - // Project - 'scope-in': 'project:scope-in', - 'scope-out': 'project:scope-out', - 'milestone': 'project:milestone', - 'init-review': 'type:init-review', -}; - -/** - * Auto-detect prefixes in text for automatic tagging. - */ -export const PREFIX_MAP: Record = { - 'DECISION:': 'code:decision', - 'PATTERN:': 'code:pattern', - 'TECH-DEBT:': 'code:tech-debt', - 'BUG:': 'context:bug', - 'RATIONALE:': 'context:rationale', - 'FAILED:': 'context:failed', - 'INCIDENT:': 'external:incident', - 'BLOCKER:': 'people:blocker', - 'SCOPE-IN:': 'project:scope-in', - 'SCOPE-OUT:': 'project:scope-out', - 'INIT-REVIEW:': 'type:init-review', -}; - -/** - * Detect tag from text prefix. - */ -export function detectPrefixTag(text: string): string | null { - for (const [prefix, tag] of Object.entries(PREFIX_MAP)) { - if (text.toUpperCase().startsWith(prefix)) { - return tag; - } - } - return null; -} diff --git a/src/lib/skills/github/github.ts b/src/lib/skills/github/github.ts index 3a41eef..e399e69 100644 --- a/src/lib/skills/github/github.ts +++ b/src/lib/skills/github/github.ts @@ -94,11 +94,10 @@ export interface ICreatedIssueSummary { export async function persistCreatedIssueTask(options: { tasks: ITaskService; - groupId: string; repo: string; issue: ICreatedIssueSummary; }): Promise { - const { tasks, groupId, repo, issue } = options; + const { tasks, repo, issue } = options; const externalLink: ITaskExternalLink = { source: 'github', id: String(issue.number), @@ -114,7 +113,7 @@ export async function persistCreatedIssueTask(options: { externalLink, }; - const linked = await tasks.listLinked([groupId], 'github', 1000, repo, issue.assignee || ''); + const linked = await tasks.listLinked('github', 1000, repo, issue.assignee || ''); const existing = linked.tasks.find( (task) => task.externalLink?.source === 'github' && task.externalLink?.id === String(issue.number) ); @@ -125,7 +124,7 @@ export async function persistCreatedIssueTask(options: { return; } - await tasks.add(issue.title, groupId, taskOptions); + await tasks.add(issue.title, taskOptions); } async function handleIssues( @@ -173,24 +172,17 @@ async function handleIssues( assignee: args.assignee as string | undefined, milestone: args.milestone as string | undefined, }); - let taskInfo: { persisted: boolean; groupId?: string; error?: string } | undefined; + let taskInfo: { persisted: boolean; error?: string } | undefined; { - const { getCurrentGroupId } = await import('../shared/group-id'); const { createTaskService } = await import('../shared/services'); const { createGitMem } = await import('../shared/clients'); - const rawGroup = args.group; - const groupId = - typeof rawGroup === 'string' && rawGroup.trim().length > 0 - ? rawGroup - : getCurrentGroupId(); const gitMem = createGitMem(); const taskService = createTaskService({ gitMem }); try { await persistCreatedIssueTask({ tasks: taskService, - groupId, repo, issue: { number: result.number, @@ -201,10 +193,10 @@ async function handleIssues( }, }); - taskInfo = { persisted: true, groupId }; + taskInfo = { persisted: true }; } catch (error) { const message = error instanceof Error ? error.message : String(error); - taskInfo = { persisted: false, groupId, error: message }; + taskInfo = { persisted: false, error: message }; } } @@ -430,20 +422,9 @@ async function handleSync( direction = 'export'; } - // Get group ID (use canonical folder-based group, allow --group override) - const { getCurrentGroupId } = await import('../shared/group-id'); - const rawGroup = args.group; - if (rawGroup !== undefined && typeof rawGroup !== 'string') { - console.log(formatError( - '--group requires a value', - 'github sync --repo owner/repo [--import|--export] [--dry-run] [--group ]' - )); - process.exit(1); - } - const groupId = - typeof rawGroup === 'string' && rawGroup.trim().length > 0 - ? rawGroup - : getCurrentGroupId(); + // Note: Group IDs are no longer used - the git repo provides scoping via git-mem. + // --group flag is ignored for backwards compatibility. + const groupId = ''; // Unused, but kept for ISyncOptions interface // Create dependencies const ghCli = createGhCliClientFromEnv(); diff --git a/src/lib/skills/github/pr-template.txt b/src/lib/skills/github/pr-template.txt new file mode 100644 index 0000000..e4d694f --- /dev/null +++ b/src/lib/skills/github/pr-template.txt @@ -0,0 +1,8 @@ +## Summary +{{summary}} + +## Test plan +{{test_plan}} + +## Linked Issues +{{linked_issues}} diff --git a/src/lib/skills/memory/memory.ts b/src/lib/skills/memory/memory.ts index 6982883..86b3284 100644 --- a/src/lib/skills/memory/memory.ts +++ b/src/lib/skills/memory/memory.ts @@ -17,7 +17,6 @@ export {}; async function main(): Promise { const { loadEnv } = await import('../shared/utils/env'); - const { getCurrentGroupId, getGroupIds } = await import('../shared/group-id'); const { createLogger } = await import('../shared/logger'); const { popFlag, hasFlag } = await import('../shared/utils/cli'); const { createCache, createCacheConfig, nullCache } = await import('../shared/utils/cache'); @@ -62,7 +61,7 @@ async function main(): Promise { const gitMem = createGitMem(); const memoryService = createMemoryService({ gitMem }); const cliService = createMemoryCliService({ - env, logger, cache, memoryService, getGroupIds, getCurrentGroupId, resolveTag, + env, logger, cache, memoryService, resolveTag, }); try { diff --git a/src/lib/skills/prompt/prompt.ts b/src/lib/skills/prompt/prompt.ts index e4e187d..9ae28da 100644 --- a/src/lib/skills/prompt/prompt.ts +++ b/src/lib/skills/prompt/prompt.ts @@ -5,21 +5,22 @@ * Stores user prompts as memories in git-mem. * * Usage: node prompt.js --text "prompt text" [--role user] [--source user-prompt] [--force] + * + * Note: Group IDs are no longer used - the git repo itself provides scoping + * via git-mem (git notes in refs/notes/mem). */ export {}; async function main(): Promise { - const { getCurrentGroupId } = await import('../shared/group-id'); const { popFlag, hasFlag } = await import('../shared/utils/cli'); const { createGitMem } = await import('../shared/clients'); const { createPromptService } = await import('../shared/services'); const args = process.argv.slice(2); - const explicitGroup = popFlag(args, '--group', null); - // Use explicit --group if provided, otherwise use canonical folder-based group ID - const groupId = explicitGroup || getCurrentGroupId(); + // --group flag is ignored but still consumed for backwards compatibility + popFlag(args, '--group', null); const text = popFlag(args, '--text', null) || popFlag(args, '-t', null); const role = popFlag(args, '--role', 'user') || popFlag(args, '-r', 'user'); const source = popFlag(args, '--source', 'user-prompt') || popFlag(args, '-s', 'user-prompt'); @@ -38,7 +39,7 @@ async function main(): Promise { const service = createPromptService({ gitMem }); try { - const result = await service.addPrompt({ text, role, source, force, groupId }); + const result = await service.addPrompt({ text, role, source, force }); if (result.status === 'skipped') { console.log('Duplicate prompt; skipping (use --force to override).'); diff --git a/src/lib/skills/review/ai-enrich.ts b/src/lib/skills/review/ai-enrich.ts index ae36039..6da0e48 100644 --- a/src/lib/skills/review/ai-enrich.ts +++ b/src/lib/skills/review/ai-enrich.ts @@ -6,13 +6,15 @@ * Spawned by init-review.ts after static analysis. * * Usage: node ai-enrich.js + * + * Note: Group IDs are no longer used - the git repo itself provides scoping + * via git-mem (git notes in refs/notes/mem). */ export {}; import fs from 'fs'; import path from 'path'; -import { getCurrentGroupId } from '../shared/group-id'; interface IStaticAnalysis { summary: string; @@ -80,7 +82,7 @@ async function initializeMCP(endpoint: string, apiKey?: string): Promise { +async function addEnrichedMemory(endpoint: string, sessionId: string, summary: string, apiKey?: string): Promise { try { const headers: Record = { 'Content-Type': 'application/json', 'MCP-SESSION-ID': sessionId, Accept: 'application/json, text/event-stream' }; if (apiKey && endpoint.includes('getzep.com')) headers['Authorization'] = `Api-Key ${apiKey}`; @@ -96,7 +98,6 @@ async function addEnrichedMemory(endpoint: string, sessionId: string, summary: s name: 'INIT-REVIEW (AI Enriched): ' + summary.slice(0, 60), episode_body: `INIT-REVIEW: ${summary}`, source: 'skill:init-review-enrich', - group_id: groupId, tags: ['type:init-review', 'scope:codebase', 'ai:enriched'], }, }, @@ -156,13 +157,12 @@ async function main(): Promise { log(`Generated enriched summary: ${enrichedSummary.slice(0, 100)}...`); const config = loadConfig(); - const groupId = getCurrentGroupId(projectRoot); - log(`Using endpoint: ${config.endpoint}, group: ${groupId}`); + log(`Using endpoint: ${config.endpoint}`); const sessionId = await initializeMCP(config.endpoint, config.zepApiKey); if (!sessionId) { log('Could not initialize MCP'); return; } - const success = await addEnrichedMemory(config.endpoint, sessionId, enrichedSummary, groupId, config.zepApiKey); + const success = await addEnrichedMemory(config.endpoint, sessionId, enrichedSummary, config.zepApiKey); if (success) { log('Successfully stored enriched memory'); diff --git a/src/lib/skills/shared/group-id.ts b/src/lib/skills/shared/group-id.ts deleted file mode 100644 index 7c31fe1..0000000 --- a/src/lib/skills/shared/group-id.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './utils/group-id'; diff --git a/src/lib/skills/shared/services/GitHubSyncService.ts b/src/lib/skills/shared/services/GitHubSyncService.ts index 61070b9..29e0274 100644 --- a/src/lib/skills/shared/services/GitHubSyncService.ts +++ b/src/lib/skills/shared/services/GitHubSyncService.ts @@ -11,6 +11,10 @@ * - in-progress <-> open + in-progress label * - blocked <-> open + blocked label * - done <-> closed + * + * Note: Group IDs are no longer used - the git repo itself provides scoping + * via git-mem (git notes in refs/notes/mem). The groupId in ISyncOptions is + * kept for interface compatibility but ignored. */ import type { IGitHubClient, IGitHubIssue } from './GitHubService'; import type { @@ -211,7 +215,6 @@ export function createGitHubSyncService( issueNumber: number ): Promise { const result = await tasks.listLinked( - [options.groupId], 'github', 1000, options.defaultRepo || '', @@ -239,7 +242,7 @@ export function createGitHubSyncService( syncedAt: new Date().toISOString(), }; - await tasks.add(issue.title, options.groupId, { + await tasks.add(issue.title, { status, repo: options.repo, assignee: issue.assignees[0] || options.defaultAssignee || '', @@ -289,7 +292,7 @@ export function createGitHubSyncService( syncedAt: new Date().toISOString(), }; - await tasks.link(task.uuid, options.groupId, externalLink); + await tasks.link(task.uuid, externalLink); return { title: task.title, @@ -303,13 +306,13 @@ export function createGitHubSyncService( * Update a linked task's status based on GitHub issue state. */ async function updateFromGitHub( - options: ISyncOptions, + _options: ISyncOptions, task: ITask, issue: IGitHubIssue ): Promise { const newStatus = gitHubStatusToLisa(issue); - await tasks.update(task.title, options.groupId, { + await tasks.update(task.title, { status: newStatus, repo: task.repo, assignee: issue.assignees[0] || task.assignee, @@ -367,7 +370,7 @@ export function createGitHubSyncService( } // Update sync timestamp on the task - await tasks.update(task.title, options.groupId, { + await tasks.update(task.title, { status: task.status, repo: task.repo, assignee: task.assignee, @@ -437,7 +440,6 @@ export function createGitHubSyncService( // Get all Lisa tasks const tasksResult = await tasks.list( - [options.groupId], 1000, options.defaultRepo || '', options.defaultAssignee || '' diff --git a/src/lib/skills/shared/services/MemoryCliService.ts b/src/lib/skills/shared/services/MemoryCliService.ts index 8b6f855..c013e22 100644 --- a/src/lib/skills/shared/services/MemoryCliService.ts +++ b/src/lib/skills/shared/services/MemoryCliService.ts @@ -57,8 +57,6 @@ export interface IMemoryCliDependencies { logger: ILogger; cache: ICache; memoryService: IMemoryService; - getGroupIds: () => string[]; - getCurrentGroupId: () => string; resolveTag: (text: string, explicitTag: string | null, entityType: string | null) => string | undefined; } @@ -118,12 +116,12 @@ export function parseTtlDuration(input: string): number | null { * Creates a memory CLI service instance. */ export function createMemoryCliService(deps: IMemoryCliDependencies): IMemoryCliService { - const { env, logger, cache, memoryService, getGroupIds, getCurrentGroupId, resolveTag } = deps; + const { env, logger, cache, memoryService, resolveTag } = deps; return { async run(args: IMemoryCliArgs): Promise { const { - command, payload, explicitGroup, query, limit, + command, payload, query, limit, explicitTag, entityType, source, since, until, lifecycle, ttl, dryRun, uuid, topic, minSimilarity, mark, action, retain, mergedText, @@ -133,18 +131,11 @@ export function createMemoryCliService(deps: IMemoryCliDependencies): IMemoryCli throw new Error('command must be add|load|expire|cleanup|conflicts|dedupe|curate|consolidate'); } - // Use explicit --group if provided, otherwise use canonical folder-based group ID - const groupId = explicitGroup || getCurrentGroupId(); - - logger.info(`Executing command: ${command}`, { mode: env.STORAGE_MODE, group: groupId }); + logger.info(`Executing command: ${command}`, { mode: env.STORAGE_MODE }); let result: MemoryCliResult; if (command === 'load') { - // Always use canonical group IDs for loading (hierarchical lookup) - const groupIds = explicitGroup ? [explicitGroup] : getGroupIds(); - logger.debug('Using Neo4j direct mode for load'); - // Parse date filters - throw error on invalid values const loadOptions: IMemoryLoadOptions = {}; if (since) { @@ -162,16 +153,15 @@ export function createMemoryCliService(deps: IMemoryCliDependencies): IMemoryCli loadOptions.until = parsedUntil; } - result = await memoryService.load(groupIds, query, limit, loadOptions); + result = await memoryService.load(query, limit, loadOptions); } else if (command === 'expire') { const targetUuid = uuid || payload; if (!targetUuid) throw new Error('expire requires a UUID (--uuid or positional argument)'); - result = await memoryService.expire(groupId, targetUuid); + result = await memoryService.expire(targetUuid); } else if (command === 'cleanup') { - result = await memoryService.cleanup(groupId, dryRun); + result = await memoryService.cleanup(dryRun); } else if (command === 'conflicts') { - const groupIds = explicitGroup ? [explicitGroup] : getGroupIds(); - result = await memoryService.conflicts(groupIds, topic ?? undefined); + result = await memoryService.conflicts(topic ?? undefined); } else if (command === 'dedupe') { const dedupeOptions: { minSimilarity?: number; limit?: number; since?: Date } = {}; if (minSimilarity !== null) dedupeOptions.minSimilarity = minSimilarity; @@ -183,12 +173,12 @@ export function createMemoryCliService(deps: IMemoryCliDependencies): IMemoryCli } dedupeOptions.since = parsedSince; } - result = await memoryService.dedupe(groupId, dedupeOptions); + result = await memoryService.dedupe(dedupeOptions); } else if (command === 'curate') { const targetUuid = uuid || payload; if (!targetUuid) throw new Error('curate requires a UUID (--uuid or positional argument)'); if (!mark) throw new Error('curate requires --mark (authoritative, draft, deprecated, needs-review)'); - result = await memoryService.curate(groupId, targetUuid, mark as CurationMark); + result = await memoryService.curate(targetUuid, mark as CurationMark); } else if (command === 'consolidate') { // Parse UUIDs from payload (space-separated) const factUuids = payload ? payload.split(/\s+/).filter(Boolean) : []; @@ -197,7 +187,7 @@ export function createMemoryCliService(deps: IMemoryCliDependencies): IMemoryCli const consolidateOptions: { retainUuid?: string; mergedText?: string } = {}; if (retain) consolidateOptions.retainUuid = retain; if (mergedText) consolidateOptions.mergedText = mergedText; - result = await memoryService.consolidate(groupId, factUuids, consolidateAction, consolidateOptions); + result = await memoryService.consolidate(factUuids, consolidateAction, consolidateOptions); } else { // add if (!payload) throw new Error('add requires text payload'); @@ -219,7 +209,7 @@ export function createMemoryCliService(deps: IMemoryCliDependencies): IMemoryCli ttlMs = parsed; } - result = await memoryService.add(payload, groupId, { + result = await memoryService.add(payload, { tag, type: entityType ?? lifecycle ?? undefined, source, diff --git a/src/lib/skills/shared/services/StorageService.ts b/src/lib/skills/shared/services/StorageService.ts index 14ac638..f7f8431 100644 --- a/src/lib/skills/shared/services/StorageService.ts +++ b/src/lib/skills/shared/services/StorageService.ts @@ -1,9 +1,12 @@ /** * Storage service - manages Lisa storage mode (local/zep-cloud). + * + * Note: Group IDs are no longer used - the git repo itself provides scoping + * via git-mem (git notes in refs/notes/mem). */ import fs from 'fs'; +import path from 'path'; import { execFileSync } from 'child_process'; -import { getCurrentGroupId } from '../group-id'; // ============================================================================ // Types @@ -194,7 +197,8 @@ export function createStorageService(deps: IStorageServiceDependencies): IStorag const env = service.readEnvConfig(); const mode = env.STORAGE_MODE || process.env.STORAGE_MODE || 'local'; const endpoint = env.GRAPHITI_ENDPOINT || process.env.GRAPHITI_ENDPOINT || defaultLocalEndpoint; - const groupId = getCurrentGroupId(); + // Group ID is derived from cwd folder name for backwards compatibility + const groupId = path.basename(process.cwd()); let isConnected = false; let connectionError: string | undefined; diff --git a/src/lib/skills/shared/services/TaskCliService.ts b/src/lib/skills/shared/services/TaskCliService.ts index 45479b1..bc4a51b 100644 --- a/src/lib/skills/shared/services/TaskCliService.ts +++ b/src/lib/skills/shared/services/TaskCliService.ts @@ -1,5 +1,8 @@ /** * Task CLI service - encapsulates all task CLI command logic. + * + * Note: Group IDs are no longer used - the git repo itself provides scoping + * via git-mem (git notes in refs/notes/mem). */ import type { IEnvConfig } from '../utils/env'; import type { ILogger } from '../utils/interfaces/ILogger'; @@ -21,7 +24,6 @@ import { parseDate } from '../../../utils/dateParser'; export interface ITaskCliArgs { command: string; payload: string; - explicitGroup: string | null; limit: number; status: string; tag: string | null; @@ -43,8 +45,6 @@ export interface ITaskCliDependencies { logger: ILogger; cache: ICache; taskService: ITaskService; - getGroupIds: () => string[]; - getCurrentGroupId: () => string; } /** @@ -94,28 +94,24 @@ function parseExternalLinkRef(ref: string): ITaskExternalLink | null { * Creates a task CLI service instance. */ export function createTaskCliService(deps: ITaskCliDependencies): ITaskCliService { - const { env, logger, cache, taskService, getGroupIds, getCurrentGroupId } = deps; + const { env, logger, cache, taskService } = deps; return { async run(args: ITaskCliArgs): Promise { - const { command, payload, explicitGroup, limit, status, tag, repo, assignee, notes, link, linkedSource, since, until, all } = args; + const { command, payload, limit, status, tag, repo, assignee, notes, link, linkedSource, since, until, all } = args; const validCommands = ['add', 'list', 'update', 'link', 'unlink', 'list-linked']; if (!validCommands.includes(command)) { throw new Error(`command must be ${validCommands.join('|')}`); } - // Use explicit --group if provided, otherwise use canonical folder-based group ID - const groupId = explicitGroup || getCurrentGroupId(); - - logger.info(`Executing command: ${command}`, { mode: env.STORAGE_MODE, group: groupId }); + logger.info(`Executing command: ${command}`, { mode: env.STORAGE_MODE }); let result: ITaskListResult | ITaskWriteResult | ITaskLinkResult; if (command === 'list') { - const groupIds = explicitGroup ? [explicitGroup] : getGroupIds(); - logger.debug('Using Neo4j direct mode for list'); - + logger.debug('Using git-mem mode for list'); + // Parse date filters - throw error on invalid values const loadOptions: ITaskLoadOptions = {}; let effectiveSince = since; @@ -136,17 +132,16 @@ export function createTaskCliService(deps: ITaskCliDependencies): ITaskCliServic } loadOptions.until = parsedUntil; } - - result = await taskService.list(groupIds, limit, repo, assignee, loadOptions); + + result = await taskService.list(limit, repo, assignee, loadOptions); } else if (command === 'list-linked') { // List tasks with external links - const groupIds = explicitGroup ? [explicitGroup] : getGroupIds(); const source = linkedSource as ExternalLinkSource | undefined; logger.debug('Listing linked tasks', { source }); - result = await taskService.listLinked(groupIds, source, limit, repo, assignee); + result = await taskService.listLinked(source, limit, repo, assignee); } else if (command === 'add') { if (!payload) throw new Error('add requires task text (title)'); - + // Parse external link if provided let externalLink: ITaskExternalLink | undefined; if (link) { @@ -156,11 +151,11 @@ export function createTaskCliService(deps: ITaskCliDependencies): ITaskCliServic } externalLink = { ...parsed, syncedAt: new Date().toISOString() }; } - - result = await taskService.add(payload, groupId, { status, repo, assignee, notes, tag, externalLink }); + + result = await taskService.add(payload, { status, repo, assignee, notes, tag, externalLink }); } else if (command === 'update') { if (!payload) throw new Error('update requires task text (title)'); - + // Parse external link if provided (null means unlink) let externalLink: ITaskExternalLink | null | undefined; if (link === '') { @@ -173,25 +168,25 @@ export function createTaskCliService(deps: ITaskCliDependencies): ITaskCliServic } externalLink = { ...parsed, syncedAt: new Date().toISOString() }; } - - result = await taskService.update(payload, groupId, { status, repo, assignee, notes, tag, externalLink }); + + result = await taskService.update(payload, { status, repo, assignee, notes, tag, externalLink }); } else if (command === 'link') { // Link command: payload is UUID, link is the reference if (!payload) throw new Error('link requires task UUID'); if (!link) throw new Error('link requires --link github#123 or similar'); - + const parsed = parseExternalLinkRef(link); if (!parsed) { throw new Error(`Invalid link format: ${link}. Expected: github#123, jira#PROJ-456, or linear#ABC-123`); } const externalLink = { ...parsed, syncedAt: new Date().toISOString() }; - - result = await taskService.link(payload, groupId, externalLink); + + result = await taskService.link(payload, externalLink); } else if (command === 'unlink') { // Unlink command: payload is UUID if (!payload) throw new Error('unlink requires task UUID'); - - result = await taskService.unlink(payload, groupId); + + result = await taskService.unlink(payload); } else { throw new Error(`Unknown command: ${command}`); } diff --git a/src/lib/skills/shared/utils/group-id.ts b/src/lib/skills/shared/utils/group-id.ts deleted file mode 100644 index 2548a42..0000000 --- a/src/lib/skills/shared/utils/group-id.ts +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Shared group ID utilities for folder-based memory isolation. - * Used by memory, tasks, and other skills. - * - * The group ID is derived from the project root (where .lisa directory exists). - * This ensures consistent group IDs regardless of where in the project you run commands. - */ -import path from 'path'; -import fs from 'fs'; - -/** - * Find the .lisa directory by traversing up from the given directory. - * @param startDir - Directory to start searching from - * @returns Path to .lisa directory or null if not found - */ -function findLisaDir(startDir: string): string | null { - let dir = startDir; - - // Traverse up to the filesystem root looking for .lisa - // eslint-disable-next-line no-constant-condition - while (true) { - const lisaDir = path.join(dir, '.lisa'); - try { - if (fs.statSync(lisaDir).isDirectory()) { - return lisaDir; - } - } catch { - // Ignore not-found / permission errors and keep walking up - } - - const parent = path.dirname(dir); - if (parent === dir) break; // Reached root - dir = parent; - } - - return null; -} - -/** - * Get the project root directory (parent of .lisa directory). - * @param cwd - Current working directory (defaults to process.cwd()) - * @returns Project root path or cwd if .lisa not found - */ -function getProjectRoot(cwd: string = process.cwd()): string { - const lisaDir = findLisaDir(cwd); - if (lisaDir) { - return path.dirname(lisaDir); - } - // Fallback: use current directory - return cwd; -} - -/** - * Get the canonical group ID based on the project root path. - * The group ID is the full normalized path where .lisa was initialized. - * - * Cross-platform: Works on Windows (\), Mac/Linux (/) - * - * @param cwd - Current working directory (defaults to process.cwd()) - * @returns Normalized group ID - */ -export function getCurrentGroupId(cwd: string = process.cwd()): string { - const projectRoot = getProjectRoot(cwd); - return normalizeGroupId(projectRoot); -} - -/** - * Normalize a path to a valid Graphiti group ID. - * Graphiti requires alphanumeric characters, dashes, and underscores only. - * - * Cross-platform path handling: - * - Windows: C:\dev\lisa -> c-dev-lisa - * - Unix: /home/user/lisa -> home-user-lisa - * - * @param input - Path string to normalize - * @returns Normalized group ID - */ -export function normalizeGroupId(input: string): string { - return input - .toLowerCase() - // Remove drive letter colon (Windows C: -> c, must run before path separator replacement) - .replace(/^([a-z]):/, '$1') - // Normalize path separators (Windows \ and Unix /) to dash - .replace(/[\\/]+/g, '-') - // Replace dots with underscores - .replace(/\./g, '_') - // Replace any remaining invalid chars with dashes - .replace(/[^a-z0-9_-]/g, '-') - // Remove leading/trailing dashes - .replace(/^-+|-+$/g, '') - // Collapse multiple consecutive dashes - .replace(/-+/g, '-'); -} - -/** - * Get group IDs for querying. - * Returns canonical + legacy group IDs for the current project. - * - * @param cwd - Current working directory (defaults to process.cwd()) - * @returns Array of group IDs to query - */ -export function getGroupIds(cwd: string = process.cwd()): string[] { - return getGroupIdsWithLegacy(cwd); -} - -/** - * Generate a hierarchical list of group IDs from a path. - * Uses the full normalized path and its parent paths. - * E.g., "/home/user/projects/lisa" returns ["home-user-projects-lisa", "home-user-projects", "home-user"] - * - * This allows querying memories from parent directories as well. - * - * @param cwd - Current working directory (defaults to process.cwd()) - * @param maxDepth - Maximum number of parent levels to include - * @returns Array of group IDs from current to parents - */ -export function getHierarchicalGroupIds( - cwd: string = process.cwd(), - maxDepth: number = 3 -): string[] { - const groupIds: string[] = []; - let currentPath = cwd; - - for (let i = 0; i < maxDepth; i++) { - if (!currentPath || currentPath === path.dirname(currentPath)) break; // Reached root - - const groupId = normalizeGroupId(currentPath); - if (groupId && !groupIds.includes(groupId)) { - groupIds.push(groupId); - } - - currentPath = path.dirname(currentPath); - } - - return groupIds; -} - -/** - * Get group IDs for querying with backward compatibility. - * Includes the canonical group ID plus the legacy basename-based ID for migration. - * - * This ensures existing data stored under the old basename format (e.g., "lisa") - * is still accessible while new data uses the full path format (e.g., "c-dev-lisa"). - * - * @param cwd - Current working directory (defaults to process.cwd()) - * @returns Array of group IDs to query (canonical + legacy) - */ -export function getGroupIdsWithLegacy(cwd: string = process.cwd()): string[] { - const groupIds = new Set(); - - // Add canonical full-path group ID - const canonicalId = getCurrentGroupId(cwd); - if (canonicalId) { - groupIds.add(canonicalId); - } - - // Add legacy basename-based group ID for backward compatibility - // This allows reading old data stored under the previous naming scheme - const projectRoot = getProjectRoot(cwd); - const legacyId = normalizeGroupId(path.basename(projectRoot)); - if (legacyId && legacyId !== canonicalId) { - groupIds.add(legacyId); - } - - return Array.from(groupIds); -} - -/** - * Create a Zep-compatible user ID from a group ID. - * E.g., "lisa" -> "lisa-user" - */ -export function createZepUserId(groupId: string): string { - return `lisa-${groupId}`; -} - -/** - * Create a Zep-compatible thread ID for a specific purpose. - * E.g., ("lisa", "memory") -> "lisa-memory-lisa" - */ -export function createZepThreadId(groupId: string, purpose: string): string { - return `lisa-${purpose}-${groupId}`; -} diff --git a/src/lib/skills/shared/utils/index.ts b/src/lib/skills/shared/utils/index.ts index dd31cf4..33a79a7 100644 --- a/src/lib/skills/shared/utils/index.ts +++ b/src/lib/skills/shared/utils/index.ts @@ -15,15 +15,6 @@ export { isLocalMcpConfigured, type IEnvConfig, } from './env'; -export { - getCurrentGroupId, - getGroupIds, - getGroupIdsWithLegacy, - getHierarchicalGroupIds, - normalizeGroupId, - createZepUserId, - createZepThreadId, -} from './group-id'; // CLI argument parsing export { diff --git a/src/lib/skills/tasks/tasks.ts b/src/lib/skills/tasks/tasks.ts index 66a2d86..55935d6 100644 --- a/src/lib/skills/tasks/tasks.ts +++ b/src/lib/skills/tasks/tasks.ts @@ -3,12 +3,15 @@ * Task management CLI - thin entry point. * * Commands: - * node tasks.js list [--group ] [--limit N] [--since ] [--until ] [--all] [--cache] - * node tasks.js list-linked [--linked github|jira|linear] [--group ] [--limit N] - * node tasks.js add "task text" [--status todo|doing|done] [--tag foo] [--link github#123] [--group ] [--cache] - * node tasks.js update "task text" [--status ...] [--tag foo] [--link github#123] [--group ] [--cache] - * node tasks.js link --link github#123 [--group ] - * node tasks.js unlink [--group ] + * node tasks.js list [--limit N] [--since ] [--until ] [--all] [--cache] + * node tasks.js list-linked [--linked github|jira|linear] [--limit N] + * node tasks.js add "task text" [--status todo|doing|done] [--tag foo] [--link github#123] [--cache] + * node tasks.js update "task text" [--status ...] [--tag foo] [--link github#123] [--cache] + * node tasks.js link --link github#123 + * node tasks.js unlink + * + * Note: Group IDs are no longer used - the git repo itself provides scoping + * via git-mem (git notes in refs/notes/mem). */ export {}; @@ -17,7 +20,6 @@ import path from 'path'; async function main(): Promise { const { loadEnv } = await import('../shared/utils/env'); - const { getCurrentGroupId, getGroupIds } = await import('../shared/group-id'); const { createLogger } = await import('../shared/logger'); const { popFlag, hasFlag } = await import('../shared/utils/cli'); const { createCache, createCacheConfig, nullCache } = await import('../shared/utils/cache'); @@ -29,7 +31,8 @@ async function main(): Promise { const args = process.argv.slice(2); const command = args.shift() ?? ''; - const explicitGroup = popFlag(args, '--group', null); + // --group flag is ignored but still consumed for backwards compatibility + popFlag(args, '--group', null); const limit = Number(popFlag(args, '--limit', '20')) || 20; const status = popFlag(args, '--status', 'todo'); const tag = popFlag(args, '--tag', null); @@ -49,12 +52,12 @@ async function main(): Promise { const gitMem = createGitMem(); const taskService = createTaskService({ gitMem }); const cliService = createTaskCliService({ - env, logger, cache, taskService, getGroupIds, getCurrentGroupId, + env, logger, cache, taskService, }); try { const result = await cliService.run({ - command, payload, explicitGroup, limit, status, tag, repo, assignee, notes, link, linkedSource, since, until, all, + command, payload, limit, status, tag, repo, assignee, notes, link, linkedSource, since, until, all, }); console.log(JSON.stringify(result, null, 2)); } catch (err: unknown) { diff --git a/tests/unit/src/lib/application/handlers/PromptSubmitHandler.test.ts b/tests/unit/src/lib/application/handlers/PromptSubmitHandler.test.ts index ebc75a5..ca14c26 100644 --- a/tests/unit/src/lib/application/handlers/PromptSubmitHandler.test.ts +++ b/tests/unit/src/lib/application/handlers/PromptSubmitHandler.test.ts @@ -182,10 +182,10 @@ describe('PromptSubmitHandler', () => { describe('memory storage', () => { it('should add prompt to memory with ephemeral lifecycle', async () => { - let addedFact: { groupId: string; fact: string; options: unknown } | undefined; + let addedFact: { fact: string; options: unknown } | undefined; const mockMemory = createMockMemoryService({ - addFactWithLifecycle: async (groupId, fact, options) => { - addedFact = { groupId, fact, options }; + addFactWithLifecycle: async (fact, options) => { + addedFact = { fact, options }; }, }); @@ -198,7 +198,6 @@ describe('PromptSubmitHandler', () => { await handler.handle(request); assert.ok(addedFact, 'Fact should be added'); - assert.strictEqual(addedFact.groupId, 'test-group'); assert.ok(addedFact.fact.includes('User prompt at 2024-01-15T10:00:00.000Z')); assert.ok(addedFact.fact.includes('Test prompt content')); const opts = addedFact.options as { lifecycle: string; tags: string[] }; @@ -209,7 +208,7 @@ describe('PromptSubmitHandler', () => { it('should truncate long prompts to 200 characters', async () => { let addedFact: string | undefined; const mockMemory = createMockMemoryService({ - addFactWithLifecycle: async (_groupId, fact) => { + addFactWithLifecycle: async (fact) => { addedFact = fact; }, }); @@ -232,7 +231,7 @@ describe('PromptSubmitHandler', () => { it('should not truncate short prompts', async () => { let addedFact: string | undefined; const mockMemory = createMockMemoryService({ - addFactWithLifecycle: async (_groupId, fact) => { + addFactWithLifecycle: async (fact) => { addedFact = fact; }, }); @@ -302,23 +301,19 @@ describe('PromptSubmitHandler', () => { assert.strictEqual(result.additionalContext, 'Found relevant context'); }); - it('should pass hierarchical group IDs to recursion', async () => { - let receivedGroupIds: readonly string[] | undefined; + it('should pass prompt to recursion service', async () => { + let receivedPrompt: string | undefined; const mockRecursion = createMockRecursionService({ shouldRun: () => true, - run: async (_prompt, groupIds) => { - receivedGroupIds = groupIds; + run: async (prompt) => { + receivedPrompt = prompt; return createMockRecursionResult({ hasContext: true, summary: 'context' }); }, }); - const customContext = createMockContext({ - hierarchicalGroupIds: ['child-group', 'parent-group', 'root-group'], - }); - - const handler = new PromptSubmitHandler(customContext, memory, mockRecursion); + const handler = new PromptSubmitHandler(context, memory, mockRecursion); const request = new PromptSubmitRequest( - 'Test', + 'Test prompt for recursion', '2024-01-15T10:00:00.000Z', undefined, 'plan' as PermissionMode @@ -326,7 +321,7 @@ describe('PromptSubmitHandler', () => { await handler.handle(request); - assert.deepStrictEqual(receivedGroupIds, ['child-group', 'parent-group', 'root-group']); + assert.strictEqual(receivedPrompt, 'Test prompt for recursion'); }); it('should not run recursion when shouldRun returns false', async () => { diff --git a/tests/unit/src/lib/application/handlers/SessionStopHandler.quality-tags.test.ts b/tests/unit/src/lib/application/handlers/SessionStopHandler.quality-tags.test.ts index 0f20d5a..f9f1d36 100644 --- a/tests/unit/src/lib/application/handlers/SessionStopHandler.quality-tags.test.ts +++ b/tests/unit/src/lib/application/handlers/SessionStopHandler.quality-tags.test.ts @@ -11,12 +11,16 @@ import { SessionStopRequest } from '../../../../../../src/lib/application/mediat import type { ILisaContext, IMemoryService, + IMemorySaveOptions, ISessionCaptureService, IEventEmitter, ICapturedWork, LisaEvent, } from '../../../../../../src/lib/domain'; +/** Captured options from addFactWithLifecycle calls. */ +type SavedOptions = IMemorySaveOptions; + // ============================================================================ // Mock Factories // ============================================================================ @@ -69,10 +73,10 @@ function createMockEvents(): IEventEmitter { describe('SessionStopHandler — quality tags (#179)', () => { it('should include source:session-capture and confidence:medium tags', async () => { - const savedOptions: Array<{ lifecycle: string; tags: string[] }> = []; + const savedOptions: SavedOptions[] = []; const memory = createMockMemory({ - addFactWithLifecycle: async (_groupId, _fact, options) => { - savedOptions.push(options as { lifecycle: string; tags: string[] }); + addFactWithLifecycle: async (_fact, options) => { + savedOptions.push(options); }, }); @@ -92,10 +96,10 @@ describe('SessionStopHandler — quality tags (#179)', () => { }); it('should include taskType tag when work has detectedTaskType', async () => { - const savedOptions: Array<{ lifecycle: string; tags: string[] }> = []; + const savedOptions: SavedOptions[] = []; const memory = createMockMemory({ - addFactWithLifecycle: async (_groupId, _fact, options) => { - savedOptions.push(options as { lifecycle: string; tags: string[] }); + addFactWithLifecycle: async (_fact, options) => { + savedOptions.push(options); }, }); @@ -129,10 +133,10 @@ describe('SessionStopHandler — quality tags (#179)', () => { }); it('should not include taskType tag when work has no detectedTaskType', async () => { - const savedOptions: Array<{ lifecycle: string; tags: string[] }> = []; + const savedOptions: SavedOptions[] = []; const memory = createMockMemory({ - addFactWithLifecycle: async (_groupId, _fact, options) => { - savedOptions.push(options as { lifecycle: string; tags: string[] }); + addFactWithLifecycle: async (_fact, options) => { + savedOptions.push(options); }, }); @@ -167,10 +171,10 @@ describe('SessionStopHandler — quality tags (#179)', () => { }); it('should not include taskType tag when work field is undefined', async () => { - const savedOptions: Array<{ lifecycle: string; tags: string[] }> = []; + const savedOptions: SavedOptions[] = []; const memory = createMockMemory({ - addFactWithLifecycle: async (_groupId, _fact, options) => { - savedOptions.push(options as { lifecycle: string; tags: string[] }); + addFactWithLifecycle: async (_fact, options) => { + savedOptions.push(options); }, }); @@ -198,10 +202,10 @@ describe('SessionStopHandler — quality tags (#179)', () => { }); it('should apply same tags to all facts in a session', async () => { - const savedOptions: Array<{ lifecycle: string; tags: string[] }> = []; + const savedOptions: SavedOptions[] = []; const memory = createMockMemory({ - addFactWithLifecycle: async (_groupId, _fact, options) => { - savedOptions.push(options as { lifecycle: string; tags: string[] }); + addFactWithLifecycle: async (_fact, options) => { + savedOptions.push(options); }, }); @@ -245,9 +249,8 @@ describe('SessionStopHandler — quality tags (#179)', () => { for (const taskType of taskTypes) { const savedTags: string[][] = []; const memory = createMockMemory({ - addFactWithLifecycle: async (_groupId, _fact, options) => { - const opts = options as { tags: string[] }; - savedTags.push(opts.tags); + addFactWithLifecycle: async (_fact, options) => { + savedTags.push(options.tags ?? []); }, }); diff --git a/tests/unit/src/lib/application/handlers/SessionStopHandler.test.ts b/tests/unit/src/lib/application/handlers/SessionStopHandler.test.ts index e663cd7..42d4a50 100644 --- a/tests/unit/src/lib/application/handlers/SessionStopHandler.test.ts +++ b/tests/unit/src/lib/application/handlers/SessionStopHandler.test.ts @@ -94,13 +94,13 @@ function createMockTaskService(tasks: ITask[] = []): ITaskService { closed: 0, unknown: 0, }), - createTask: async (_groupId, input) => ({ + createTask: async (input) => ({ key: 'new-task', status: input.status || 'ready', title: input.title, blocked: [...(input.blocked || [])], }), - updateTask: async (_groupId, _taskId, updates) => ({ + updateTask: async (_taskId, updates) => ({ key: 'task-1', status: updates.status || 'ready', title: updates.title || 'Task', @@ -227,7 +227,7 @@ describe('SessionStopHandler', () => { const savedOptions: unknown[] = []; const context = createMockContext(); const memory = createMockMemory({ - addFactWithLifecycle: async (_groupId, fact, options) => { + addFactWithLifecycle: async (fact, options) => { savedFacts.push(fact); savedOptions.push(options); }, diff --git a/tests/unit/src/lib/application/handlers/pr/PrRememberHandler.test.ts b/tests/unit/src/lib/application/handlers/pr/PrRememberHandler.test.ts index 376b73b..8a429d7 100644 --- a/tests/unit/src/lib/application/handlers/pr/PrRememberHandler.test.ts +++ b/tests/unit/src/lib/application/handlers/pr/PrRememberHandler.test.ts @@ -69,12 +69,11 @@ describe('PrRememberHandler', () => { let handler: PrRememberHandler; let mockGithubClient: GithubClient; let mockMemoryWriter: IMemoryWriter; - const groupId = 'test-group-id'; beforeEach(() => { mockGithubClient = createMockGithubClient(); mockMemoryWriter = createMockMemoryWriter(); - handler = new PrRememberHandler(mockGithubClient, mockMemoryWriter, groupId); + handler = new PrRememberHandler(mockGithubClient, mockMemoryWriter); }); describe('execute', () => { @@ -94,11 +93,11 @@ describe('PrRememberHandler', () => { it('should include PR title in the saved fact', async () => { let savedFact: string | undefined; mockMemoryWriter = createMockMemoryWriter({ - addFact: async (_groupId, fact) => { + addFact: async (fact) => { savedFact = fact; }, }); - handler = new PrRememberHandler(mockGithubClient, mockMemoryWriter, groupId); + handler = new PrRememberHandler(mockGithubClient, mockMemoryWriter); await handler.execute({ prNumber: 50, @@ -114,11 +113,11 @@ describe('PrRememberHandler', () => { it('should save fact with correct tags', async () => { let savedTags: readonly string[] | undefined; mockMemoryWriter = createMockMemoryWriter({ - addFact: async (_groupId, _fact, tags) => { + addFact: async (_fact, tags) => { savedTags = tags; }, }); - handler = new PrRememberHandler(mockGithubClient, mockMemoryWriter, groupId); + handler = new PrRememberHandler(mockGithubClient, mockMemoryWriter); const result = await handler.execute({ prNumber: 50, @@ -131,21 +130,22 @@ describe('PrRememberHandler', () => { assert.deepStrictEqual(result.tags, ['github:pr', 'github:pr:50']); }); - it('should save to the correct group ID', async () => { - let usedGroupId: string | undefined; + it('should call addFact with fact text', async () => { + let calledFact: string | undefined; mockMemoryWriter = createMockMemoryWriter({ - addFact: async (gId) => { - usedGroupId = gId; + addFact: async (fact) => { + calledFact = fact; }, }); - handler = new PrRememberHandler(mockGithubClient, mockMemoryWriter, groupId); + handler = new PrRememberHandler(mockGithubClient, mockMemoryWriter); await handler.execute({ prNumber: 50, note: 'Test note', }); - assert.strictEqual(usedGroupId, groupId); + assert.ok(calledFact); + assert.ok(calledFact.includes('PR #50')); }); it('should use provided repo instead of detecting', async () => { @@ -156,7 +156,7 @@ describe('PrRememberHandler', () => { return createMockPrResponse(); }, }); - handler = new PrRememberHandler(mockGithubClient, mockMemoryWriter, groupId); + handler = new PrRememberHandler(mockGithubClient, mockMemoryWriter); await handler.execute({ prNumber: 50, @@ -171,7 +171,7 @@ describe('PrRememberHandler', () => { mockGithubClient = createMockGithubClient({ getPr: async () => null as unknown as IGhPrResponse, }); - handler = new PrRememberHandler(mockGithubClient, mockMemoryWriter, groupId); + handler = new PrRememberHandler(mockGithubClient, mockMemoryWriter); const result = await handler.execute({ prNumber: 999, @@ -188,7 +188,7 @@ describe('PrRememberHandler', () => { throw new Error('GitHub API error'); }, }); - handler = new PrRememberHandler(mockGithubClient, mockMemoryWriter, groupId); + handler = new PrRememberHandler(mockGithubClient, mockMemoryWriter); const result = await handler.execute({ prNumber: 50, @@ -205,7 +205,7 @@ describe('PrRememberHandler', () => { throw new Error('Memory save failed'); }, }); - handler = new PrRememberHandler(mockGithubClient, mockMemoryWriter, groupId); + handler = new PrRememberHandler(mockGithubClient, mockMemoryWriter); const result = await handler.execute({ prNumber: 50, @@ -235,7 +235,7 @@ describe('PrRememberHandler', () => { }, getPr: async () => createMockPrResponse(), }); - handler = new PrRememberHandler(mockGithubClient, mockMemoryWriter, groupId); + handler = new PrRememberHandler(mockGithubClient, mockMemoryWriter); const result = await handler.execute({ prNumber: 50, @@ -250,7 +250,7 @@ describe('PrRememberHandler', () => { mockGithubClient = createMockGithubClient({ getCurrentRepo: async () => undefined as unknown as string, }); - handler = new PrRememberHandler(mockGithubClient, mockMemoryWriter, groupId); + handler = new PrRememberHandler(mockGithubClient, mockMemoryWriter); const result = await handler.execute({ prNumber: 50, diff --git a/tests/unit/src/lib/application/services/GitIndexingService.test.ts b/tests/unit/src/lib/application/services/GitIndexingService.test.ts index da48d1a..efdba7b 100644 --- a/tests/unit/src/lib/application/services/GitIndexingService.test.ts +++ b/tests/unit/src/lib/application/services/GitIndexingService.test.ts @@ -44,7 +44,7 @@ describe('GitIndexingService', () => { const service = createGitIndexingService(mockMemory, mockLogger); const facts = [createMockFact()]; - const result = await service.indexFacts(facts, 'test-group'); + const result = await service.indexFacts(facts); assert.strictEqual(result.indexed, 1); assert.strictEqual(result.skipped, 0); @@ -53,8 +53,7 @@ describe('GitIndexingService', () => { const addFactCalls = (mockMemory.addFactWithLifecycle as ReturnType).mock.calls; assert.strictEqual(addFactCalls.length, 1); - const [groupId, factText, options] = addFactCalls[0].arguments; - assert.strictEqual(groupId, 'test-group'); + const [factText, options] = addFactCalls[0].arguments; assert.strictEqual(factText, 'Test fact about a decision'); assert.strictEqual(options.lifecycle, 'project'); @@ -76,10 +75,10 @@ describe('GitIndexingService', () => { issueNumber: 456, })]; - await service.indexFacts(facts, 'test-group'); + await service.indexFacts(facts); const addFactCalls = (mockMemory.addFactWithLifecycle as ReturnType).mock.calls; - const options = addFactCalls[0].arguments[2]; + const options = addFactCalls[0].arguments[1]; const tags = options.tags as string[]; assert.ok(tags.includes('commit:abc123'), 'Should have commit tag'); @@ -90,10 +89,10 @@ describe('GitIndexingService', () => { const service = createGitIndexingService(mockMemory, mockLogger); const facts = [createMockFact()]; - await service.indexFacts(facts, 'test-group', { conventionalType: 'feat' }); + await service.indexFacts(facts, { conventionalType: 'feat' }); const addFactCalls = (mockMemory.addFactWithLifecycle as ReturnType).mock.calls; - const tags = addFactCalls[0].arguments[2].tags as string[]; + const tags = addFactCalls[0].arguments[1].tags as string[]; assert.ok(tags.includes('memoryType:milestone'), 'Should have memoryType:milestone tag'); }); @@ -102,10 +101,10 @@ describe('GitIndexingService', () => { const service = createGitIndexingService(mockMemory, mockLogger); const facts = [createMockFact()]; - await service.indexFacts(facts, 'test-group', { conventionalType: 'fix' }); + await service.indexFacts(facts, { conventionalType: 'fix' }); const addFactCalls = (mockMemory.addFactWithLifecycle as ReturnType).mock.calls; - const tags = addFactCalls[0].arguments[2].tags as string[]; + const tags = addFactCalls[0].arguments[1].tags as string[]; assert.ok(tags.includes('memoryType:gotcha'), 'Should have memoryType:gotcha tag'); }); @@ -114,10 +113,10 @@ describe('GitIndexingService', () => { const service = createGitIndexingService(mockMemory, mockLogger); const facts = [createMockFact()]; - await service.indexFacts(facts, 'test-group', { conventionalType: 'refactor' }); + await service.indexFacts(facts, { conventionalType: 'refactor' }); const addFactCalls = (mockMemory.addFactWithLifecycle as ReturnType).mock.calls; - const tags = addFactCalls[0].arguments[2].tags as string[]; + const tags = addFactCalls[0].arguments[1].tags as string[]; assert.ok(tags.includes('memoryType:decision'), 'Should have memoryType:decision tag'); }); @@ -126,10 +125,10 @@ describe('GitIndexingService', () => { const service = createGitIndexingService(mockMemory, mockLogger); const facts = [createMockFact()]; - await service.indexFacts(facts, 'test-group', { conventionalType: 'docs' }); + await service.indexFacts(facts, { conventionalType: 'docs' }); const addFactCalls = (mockMemory.addFactWithLifecycle as ReturnType).mock.calls; - const tags = addFactCalls[0].arguments[2].tags as string[]; + const tags = addFactCalls[0].arguments[1].tags as string[]; assert.ok(tags.includes('memoryType:convention'), 'Should have memoryType:convention tag'); }); @@ -138,7 +137,7 @@ describe('GitIndexingService', () => { const service = createGitIndexingService(mockMemory, mockLogger); const facts = [createMockFact(), createMockFact()]; - const result = await service.indexFacts(facts, 'test-group', { conventionalType: 'chore' }); + const result = await service.indexFacts(facts, { conventionalType: 'chore' }); assert.strictEqual(result.indexed, 0); assert.strictEqual(result.skipped, 2); @@ -152,7 +151,7 @@ describe('GitIndexingService', () => { const service = createGitIndexingService(mockMemory, mockLogger); const facts = [createMockFact()]; - const result = await service.indexFacts(facts, 'test-group', { conventionalType: 'ci' }); + const result = await service.indexFacts(facts, { conventionalType: 'ci' }); assert.strictEqual(result.indexed, 0); assert.strictEqual(result.skipped, 1); @@ -166,7 +165,7 @@ describe('GitIndexingService', () => { const service = createGitIndexingService(mockMemory, mockLogger); const facts = [createMockFact()]; - const result = await service.indexFacts(facts, 'test-group'); + const result = await service.indexFacts(facts); assert.strictEqual(result.indexed, 0); assert.strictEqual(result.duplicates, 1); @@ -180,7 +179,7 @@ describe('GitIndexingService', () => { const service = createGitIndexingService(mockMemory, mockLogger); const facts = [createMockFact()]; - const result = await service.indexFacts(facts, 'test-group'); + const result = await service.indexFacts(facts); assert.strictEqual(result.indexed, 1); assert.strictEqual(result.duplicates, 0); @@ -194,7 +193,7 @@ describe('GitIndexingService', () => { const service = createGitIndexingService(mockMemory, mockLogger); const facts = [createMockFact()]; - const result = await service.indexFacts(facts, 'test-group', { skipDeduplication: true }); + const result = await service.indexFacts(facts, { skipDeduplication: true }); assert.strictEqual(result.indexed, 1); assert.strictEqual(result.duplicates, 0); @@ -213,7 +212,7 @@ describe('GitIndexingService', () => { const facts = [createMockFact()]; // With high threshold (0.99), should not be considered duplicate - const result1 = await service.indexFacts(facts, 'test-group', { similarityThreshold: 0.99 }); + const result1 = await service.indexFacts(facts, { similarityThreshold: 0.99 }); assert.strictEqual(result1.indexed, 1); assert.strictEqual(result1.duplicates, 0); }); @@ -226,7 +225,7 @@ describe('GitIndexingService', () => { createMockFact({ text: 'First fact about testing' }), // Duplicate within batch ]; - const result = await service.indexFacts(facts, 'test-group'); + const result = await service.indexFacts(facts); assert.strictEqual(result.indexed, 2); assert.strictEqual(result.duplicates, 1); @@ -248,7 +247,7 @@ describe('GitIndexingService', () => { createMockFact({ text: 'Fact 3' }), ]; - const result = await service.indexFacts(facts, 'test-group'); + const result = await service.indexFacts(facts); assert.strictEqual(result.indexed, 2); assert.strictEqual(result.skipped, 1); @@ -263,7 +262,7 @@ describe('GitIndexingService', () => { const facts = [createMockFact()]; // Should not throw, should continue without deduplication - const result = await service.indexFacts(facts, 'test-group'); + const result = await service.indexFacts(facts); assert.strictEqual(result.indexed, 1); }); @@ -274,10 +273,10 @@ describe('GitIndexingService', () => { matchedPattern: 'decision-keyword', })]; - await service.indexFacts(facts, 'test-group'); + await service.indexFacts(facts); const addFactCalls = (mockMemory.addFactWithLifecycle as ReturnType).mock.calls; - const tags = addFactCalls[0].arguments[2].tags as string[]; + const tags = addFactCalls[0].arguments[1].tags as string[]; assert.ok(tags.includes('pattern:decision-keyword'), 'Should have pattern tag'); }); @@ -288,10 +287,10 @@ describe('GitIndexingService', () => { tags: ['typescript', 'authentication'], })]; - await service.indexFacts(facts, 'test-group'); + await service.indexFacts(facts); const addFactCalls = (mockMemory.addFactWithLifecycle as ReturnType).mock.calls; - const tags = addFactCalls[0].arguments[2].tags as string[]; + const tags = addFactCalls[0].arguments[1].tags as string[]; assert.ok(tags.includes('tag:typescript'), 'Should have prefixed tag:typescript'); assert.ok(tags.includes('tag:authentication'), 'Should have prefixed tag:authentication'); @@ -303,7 +302,7 @@ describe('GitIndexingService', () => { const service = createGitIndexingService(mockMemory); // No logger const facts = [createMockFact()]; - const result = await service.indexFacts(facts, 'test-group'); + const result = await service.indexFacts(facts); assert.strictEqual(result.indexed, 1); }); @@ -313,7 +312,7 @@ describe('GitIndexingService', () => { it('should handle empty facts array', async () => { const service = createGitIndexingService(mockMemory, mockLogger); - const result = await service.indexFacts([], 'test-group'); + const result = await service.indexFacts([]); assert.strictEqual(result.indexed, 0); assert.strictEqual(result.skipped, 0); diff --git a/tests/unit/src/lib/application/services/MemoryContextLoader.test.ts b/tests/unit/src/lib/application/services/MemoryContextLoader.test.ts index 11a2e75..0e86bbe 100644 --- a/tests/unit/src/lib/application/services/MemoryContextLoader.test.ts +++ b/tests/unit/src/lib/application/services/MemoryContextLoader.test.ts @@ -7,7 +7,6 @@ * - Task conversion to IMemoryItem format * - Date options passing * - Graceful failure handling for each sub-operation - * - Group ID merging and deduplication */ import { describe, it } from 'node:test'; import assert from 'node:assert'; @@ -76,13 +75,13 @@ function createMockTaskService(tasks: readonly ITask[] = []): ITaskService { closed: tasks.filter((t) => t.status === 'closed').length, unknown: tasks.filter((t) => t.status === 'unknown').length, }), - createTask: async (_groupId, input) => ({ + createTask: async (input) => ({ key: 'new-task', status: input.status || 'ready', title: input.title, blocked: [...(input.blocked || [])], }), - updateTask: async (_groupId, _taskId, updates) => ({ + updateTask: async (_taskId, updates) => ({ key: 'task-1', status: updates.status || 'ready', title: updates.title || 'Task', @@ -108,10 +107,6 @@ function createMockLogger(): ILogger { // Tests // ============================================================================ -const defaultGroupIds = ['test-group', 'parent-group'] as const; -const defaultAliases = ['test-project', 'tp'] as const; -const defaultBranch = 'main'; - describe('MemoryContextLoader', () => { describe('fact loading', () => { it('should load facts via loadFactsDateOrdered', async () => { @@ -133,11 +128,7 @@ describe('MemoryContextLoader', () => { createMockLogger() ); - const result = await loader.loadMemory( - defaultGroupIds, - defaultAliases, - defaultBranch - ); + const result = await loader.loadMemory(); assert.strictEqual(loadFactsCalled, true, 'loadFactsDateOrdered should have been called'); assert.strictEqual(result.facts.length, 2); @@ -152,11 +143,7 @@ describe('MemoryContextLoader', () => { createMockLogger() ); - const result = await loader.loadMemory( - defaultGroupIds, - defaultAliases, - defaultBranch - ); + const result = await loader.loadMemory(); assert.strictEqual(result.facts.length, 0); assert.strictEqual(result.timedOut, false); @@ -169,7 +156,7 @@ describe('MemoryContextLoader', () => { let receivedLimit: number | undefined; const memory = createMockMemory({ - searchFacts: async (_groupIds, query, limit) => { + searchFacts: async (query, limit) => { receivedQuery = query; receivedLimit = limit; return [ @@ -188,11 +175,7 @@ describe('MemoryContextLoader', () => { createMockLogger() ); - const result = await loader.loadMemory( - defaultGroupIds, - defaultAliases, - defaultBranch - ); + const result = await loader.loadMemory(); assert.strictEqual(receivedQuery, 'init-review', 'should search for init-review'); assert.strictEqual(receivedLimit, 1, 'should request only 1 result'); @@ -221,11 +204,7 @@ describe('MemoryContextLoader', () => { createMockLogger() ); - const result = await loader.loadMemory( - defaultGroupIds, - defaultAliases, - defaultBranch - ); + const result = await loader.loadMemory(); assert.strictEqual(result.initReview, 'Init review from name field'); }); @@ -245,11 +224,7 @@ describe('MemoryContextLoader', () => { createMockLogger() ); - const result = await loader.loadMemory( - defaultGroupIds, - defaultAliases, - defaultBranch - ); + const result = await loader.loadMemory(); assert.strictEqual(result.initReview, null); }); @@ -277,11 +252,7 @@ describe('MemoryContextLoader', () => { createMockLogger() ); - const result = await loader.loadMemory( - defaultGroupIds, - defaultAliases, - defaultBranch - ); + const result = await loader.loadMemory(); assert.strictEqual(result.tasks.length, 1); const taskItem = result.tasks[0]; @@ -318,11 +289,7 @@ describe('MemoryContextLoader', () => { createMockLogger() ); - const result = await loader.loadMemory( - defaultGroupIds, - defaultAliases, - defaultBranch - ); + const result = await loader.loadMemory(); const tags = result.tasks[0].tags as readonly string[]; assert.ok(tags.includes('blocked_by:PROJ-50'), 'should include first blocked_by'); @@ -350,11 +317,7 @@ describe('MemoryContextLoader', () => { createMockLogger() ); - const result = await loader.loadMemory( - defaultGroupIds, - defaultAliases, - defaultBranch - ); + const result = await loader.loadMemory(); const tags = result.tasks[0].tags as readonly string[]; const blockedTags = tags.filter((t) => t.startsWith('blocked_by:')); @@ -379,11 +342,7 @@ describe('MemoryContextLoader', () => { createMockLogger() ); - const result = await loader.loadMemory( - defaultGroupIds, - defaultAliases, - defaultBranch - ); + const result = await loader.loadMemory(); assert.strictEqual(result.tasks.length, 3); assert.strictEqual(result.tasks[0].uuid, 't1'); @@ -392,68 +351,6 @@ describe('MemoryContextLoader', () => { }); }); - describe('group ID merging', () => { - it('should merge hierarchicalGroupIds and projectAliases for queries', async () => { - let receivedGroupIds: readonly string[] = []; - - const memory = createMockMemory({ - searchFacts: async (groupIds) => { - receivedGroupIds = groupIds; - return []; - }, - loadFactsDateOrdered: async () => [createMockMemoryItem()], - }); - - const loader = new MemoryContextLoader( - memory, - createMockTaskService(), - createMockLogger() - ); - - await loader.loadMemory( - ['group-a', 'group-b'], - ['alias-x', 'alias-y'], - defaultBranch - ); - - // Should combine and deduplicate - assert.ok(receivedGroupIds.includes('group-a')); - assert.ok(receivedGroupIds.includes('group-b')); - assert.ok(receivedGroupIds.includes('alias-x')); - assert.ok(receivedGroupIds.includes('alias-y')); - }); - - it('should deduplicate overlapping group IDs and aliases', async () => { - let receivedGroupIds: readonly string[] = []; - - const memory = createMockMemory({ - searchFacts: async (groupIds) => { - receivedGroupIds = groupIds; - return []; - }, - loadFactsDateOrdered: async () => [createMockMemoryItem()], - }); - - const loader = new MemoryContextLoader( - memory, - createMockTaskService(), - createMockLogger() - ); - - // 'shared-id' appears in both lists - await loader.loadMemory( - ['shared-id', 'group-only'], - ['shared-id', 'alias-only'], - defaultBranch - ); - - // Should be deduplicated via Set - const uniqueIds = [...new Set(receivedGroupIds)]; - assert.strictEqual(receivedGroupIds.length, uniqueIds.length, 'IDs should be deduplicated'); - assert.strictEqual(receivedGroupIds.length, 3, 'should have 3 unique IDs'); - }); - }); - describe('graceful failure handling', () => { it('should handle searchFacts failure gracefully and continue', async () => { let loadFactsCalled = false; @@ -473,9 +370,9 @@ describe('MemoryContextLoader', () => { createMockTask({ key: 't1', title: 'Task 1', status: 'ready' }), ]); const origGetTasks = taskService.getTasksSimple; - taskService.getTasksSimple = async (groupIds) => { + taskService.getTasksSimple = async () => { getTasksCalled = true; - return origGetTasks(groupIds); + return origGetTasks(); }; const loader = new MemoryContextLoader( @@ -484,11 +381,7 @@ describe('MemoryContextLoader', () => { createMockLogger() ); - const result = await loader.loadMemory( - defaultGroupIds, - defaultAliases, - defaultBranch - ); + const result = await loader.loadMemory(); assert.strictEqual(result.initReview, null, 'initReview should be null after search failure'); assert.strictEqual(loadFactsCalled, true, 'should still load facts'); @@ -511,9 +404,9 @@ describe('MemoryContextLoader', () => { createMockTask({ key: 't1', title: 'Task 1', status: 'ready' }), ]); const origGetTasks = taskService.getTasksSimple; - taskService.getTasksSimple = async (groupIds) => { + taskService.getTasksSimple = async () => { getTasksCalled = true; - return origGetTasks(groupIds); + return origGetTasks(); }; const loader = new MemoryContextLoader( @@ -522,11 +415,7 @@ describe('MemoryContextLoader', () => { createMockLogger() ); - const result = await loader.loadMemory( - defaultGroupIds, - defaultAliases, - defaultBranch - ); + const result = await loader.loadMemory(); assert.strictEqual(result.facts.length, 0, 'no facts after failure'); assert.strictEqual(getTasksCalled, true, 'should still load tasks after fact failure'); @@ -555,11 +444,7 @@ describe('MemoryContextLoader', () => { createMockLogger() ); - const result = await loader.loadMemory( - defaultGroupIds, - defaultAliases, - defaultBranch - ); + const result = await loader.loadMemory(); assert.strictEqual(result.facts.length, 1, 'facts should still be loaded'); assert.strictEqual(result.tasks.length, 0, 'tasks should be empty after failure'); @@ -586,11 +471,7 @@ describe('MemoryContextLoader', () => { createMockLogger() ); - const result = await loader.loadMemory( - defaultGroupIds, - defaultAliases, - defaultBranch - ); + const result = await loader.loadMemory(); assert.strictEqual(result.facts.length, 0); assert.strictEqual(result.nodes.length, 0); @@ -606,7 +487,7 @@ describe('MemoryContextLoader', () => { const memory = createMockMemory({ searchFacts: async () => [], - loadFactsDateOrdered: async (_groupIds, _limit, options) => { + loadFactsDateOrdered: async (_limit, options) => { receivedOptions = options; return [createMockMemoryItem()]; }, @@ -623,12 +504,7 @@ describe('MemoryContextLoader', () => { createMockLogger() ); - await loader.loadMemory( - defaultGroupIds, - defaultAliases, - defaultBranch, - dateOptions - ); + await loader.loadMemory(dateOptions); assert.ok(receivedOptions, 'date options should have been passed'); assert.deepStrictEqual(receivedOptions?.since, new Date('2026-01-01')); @@ -651,11 +527,7 @@ describe('MemoryContextLoader', () => { createMockLogger() ); - const result = await loader.loadMemory( - defaultGroupIds, - defaultAliases, - defaultBranch - ); + const result = await loader.loadMemory(); assert.ok(Array.isArray(result.facts), 'facts should be array'); assert.ok(Array.isArray(result.nodes), 'nodes should be array'); @@ -671,11 +543,7 @@ describe('MemoryContextLoader', () => { createMockLogger() ); - const result = await loader.loadMemory( - defaultGroupIds, - defaultAliases, - defaultBranch - ); + const result = await loader.loadMemory(); assert.strictEqual(result.nodes.length, 0, 'nodes should always be empty with git-mem'); }); diff --git a/tests/unit/src/lib/commands/doctor.test.ts b/tests/unit/src/lib/commands/doctor.test.ts index 3c2356c..c0bb907 100644 --- a/tests/unit/src/lib/commands/doctor.test.ts +++ b/tests/unit/src/lib/commands/doctor.test.ts @@ -1,8 +1,8 @@ import { describe, it, beforeEach, afterEach, mock } from 'node:test'; import assert from 'node:assert'; import fs from 'fs'; -import path from 'path'; import os from 'os'; +import path from 'path'; import { runDoctor, formatBasicOutput, @@ -14,7 +14,6 @@ import { type CheckStatus, } from '../../../../../src/lib/commands/doctor'; import type { ICliServices } from '../../../../../src/lib/commands/cli-services'; -import { normalizeGroupId } from '../../../../../src/lib/skills/shared/utils/group-id'; /** * Create a mock ICliServices object for testing. @@ -266,7 +265,7 @@ describe('Doctor Command', () => { const result = await runDoctor({ cwd: tempDir }, services); assert.strictEqual(result.config.mode, 'local'); - const expectedGroup = normalizeGroupId(tempDir); + const expectedGroup = path.basename(tempDir); assert.strictEqual(result.config.group, expectedGroup, 'Group should be derived from folder path'); assert.strictEqual(result.config.endpoint, 'http://localhost:8010/mcp/'); assert.strictEqual(result.config.envFileExists, true); @@ -283,7 +282,7 @@ describe('Doctor Command', () => { const services = createMockServices(); const result = await runDoctor({ cwd: tempDir }, services); - const expectedGroup = normalizeGroupId(tempDir); + const expectedGroup = path.basename(tempDir); assert.strictEqual(result.config.group, expectedGroup, 'Group should be derived from folder path'); }); }); diff --git a/tests/unit/src/lib/infrastructure/services/CurationService.test.ts b/tests/unit/src/lib/infrastructure/services/CurationService.test.ts index 26cfebe..5cbd630 100644 --- a/tests/unit/src/lib/infrastructure/services/CurationService.test.ts +++ b/tests/unit/src/lib/infrastructure/services/CurationService.test.ts @@ -23,20 +23,20 @@ function createMockWriter(): IMemoryWriter & { calls: { method: string; args: un const calls: { method: string; args: unknown[] }[] = []; return { calls, - async saveMemory(groupId: string, facts: readonly string[]): Promise { - calls.push({ method: 'saveMemory', args: [groupId, facts] }); + async saveMemory(facts: readonly string[]): Promise { + calls.push({ method: 'saveMemory', args: [facts] }); }, - async addFact(groupId: string, fact: string, tags?: readonly string[]): Promise { - calls.push({ method: 'addFact', args: [groupId, fact, tags] }); + async addFact(fact: string, tags?: readonly string[]): Promise { + calls.push({ method: 'addFact', args: [fact, tags] }); }, - async addFactWithLifecycle(groupId: string, fact: string, options: unknown): Promise { - calls.push({ method: 'addFactWithLifecycle', args: [groupId, fact, options] }); + async addFactWithLifecycle(fact: string, options: unknown): Promise { + calls.push({ method: 'addFactWithLifecycle', args: [fact, options] }); }, - async expireFact(groupId: string, uuid: string): Promise { - calls.push({ method: 'expireFact', args: [groupId, uuid] }); + async expireFact(uuid: string): Promise { + calls.push({ method: 'expireFact', args: [uuid] }); }, - async cleanupExpired(groupId: string): Promise { - calls.push({ method: 'cleanupExpired', args: [groupId] }); + async cleanupExpired(): Promise { + calls.push({ method: 'cleanupExpired', args: [] }); return 0; }, }; @@ -64,41 +64,41 @@ describe('CurationService', () => { describe('markFact()', () => { it('should add curation tag for draft mark', async () => { - await service.markFact('group1', 'uuid1', 'draft'); + await service.markFact('uuid1', 'draft'); const addCalls = mockWriter.calls.filter((c) => c.method === 'addFact'); assert.strictEqual(addCalls.length, 1); - assert.deepStrictEqual(addCalls[0].args[2], ['curated:draft']); + assert.deepStrictEqual(addCalls[0].args[1], ['curated:draft']); }); it('should add curation tag for needs-review mark', async () => { - await service.markFact('group1', 'uuid1', 'needs-review'); + await service.markFact('uuid1', 'needs-review'); const addCalls = mockWriter.calls.filter((c) => c.method === 'addFact'); assert.strictEqual(addCalls.length, 1); - assert.deepStrictEqual(addCalls[0].args[2], ['curated:needs-review']); + assert.deepStrictEqual(addCalls[0].args[1], ['curated:needs-review']); }); it('should expire fact when marking as deprecated', async () => { - await service.markFact('group1', 'uuid1', 'deprecated'); + await service.markFact('uuid1', 'deprecated'); const expireCalls = mockWriter.calls.filter((c) => c.method === 'expireFact'); assert.strictEqual(expireCalls.length, 1); - assert.deepStrictEqual(expireCalls[0].args, ['group1', 'uuid1']); + assert.deepStrictEqual(expireCalls[0].args, ['uuid1']); }); it('should promote confidence to verified when marking as authoritative', async () => { - await service.markFact('group1', 'uuid1', 'authoritative'); + await service.markFact('uuid1', 'authoritative'); const addCalls = mockWriter.calls.filter((c) => c.method === 'addFact'); // One for curation tag, one for confidence promotion assert.strictEqual(addCalls.length, 2); - assert.deepStrictEqual(addCalls[0].args[2], ['curated:authoritative']); - assert.deepStrictEqual(addCalls[1].args[2], ['confidence:verified']); + assert.deepStrictEqual(addCalls[0].args[1], ['curated:authoritative']); + assert.deepStrictEqual(addCalls[1].args[1], ['confidence:verified']); }); it('should not expire or promote for draft mark', async () => { - await service.markFact('group1', 'uuid1', 'draft'); + await service.markFact('uuid1', 'draft'); const expireCalls = mockWriter.calls.filter((c) => c.method === 'expireFact'); const addCalls = mockWriter.calls.filter((c) => c.method === 'addFact'); diff --git a/tests/unit/src/lib/infrastructure/services/GitMemMemoryService.test.ts b/tests/unit/src/lib/infrastructure/services/GitMemMemoryService.test.ts index 5426551..20560dc 100644 --- a/tests/unit/src/lib/infrastructure/services/GitMemMemoryService.test.ts +++ b/tests/unit/src/lib/infrastructure/services/GitMemMemoryService.test.ts @@ -47,7 +47,7 @@ describe('GitMemMemoryService', () => { const entity = createMockEntity({ content: 'Important decision' }); mockGitMem.recall = mock.fn(() => ({ memories: [entity], total: 1 })); - const result = await service.loadMemory(['test-group'], [], null); + const result = await service.loadMemory(); assert.equal(result.facts.length, 1); assert.equal(result.facts[0]?.uuid, 'test-uuid-1'); @@ -59,45 +59,31 @@ describe('GitMemMemoryService', () => { assert.equal(result.tasks.length, 0); }); - it('should filter by group tags', async () => { - const matchEntity = createMockEntity({ id: 'match', tags: ['group:mygroup'] }); - const otherEntity = createMockEntity({ id: 'other', tags: ['group:othergroup'] }); - mockGitMem.recall = mock.fn(() => ({ - memories: [matchEntity, otherEntity], - total: 2, - })); - - const result = await service.loadMemory(['mygroup'], [], null); - - assert.equal(result.facts.length, 1); - assert.equal(result.facts[0]?.uuid, 'match'); - }); - it('should extract init-review as separate field', async () => { const regular = createMockEntity({ id: 'regular', content: 'A fact' }); const initReview = createMockEntity({ id: 'review', content: 'Codebase review content', - tags: ['group:test-group', 'init-review'], + tags: ['init-review'], }); mockGitMem.recall = mock.fn(() => ({ memories: [regular, initReview], total: 2, })); - const result = await service.loadMemory(['test-group'], [], null); + const result = await service.loadMemory(); assert.equal(result.facts.length, 1); assert.equal(result.facts[0]?.uuid, 'regular'); assert.equal(result.initReview, 'Codebase review content'); }); - it('should return all facts when no groupIds filter', async () => { + it('should return all facts from git-mem', async () => { const e1 = createMockEntity({ id: '1', tags: [] }); - const e2 = createMockEntity({ id: '2', tags: ['group:something'] }); + const e2 = createMockEntity({ id: '2', tags: ['sometag'] }); mockGitMem.recall = mock.fn(() => ({ memories: [e1, e2], total: 2 })); - const result = await service.loadMemory([], [], null); + const result = await service.loadMemory(); assert.equal(result.facts.length, 2); }); @@ -111,7 +97,7 @@ describe('GitMemMemoryService', () => { ]; mockGitMem.recall = mock.fn(() => ({ memories: entities, total: 2 })); - const result = await service.loadFactsDateOrdered(['test-group'], 5); + const result = await service.loadFactsDateOrdered(5); assert.equal(result.length, 2); assert.equal(mockGitMem.recall.mock.calls[0]?.arguments[1]?.limit, 5); @@ -122,7 +108,7 @@ describe('GitMemMemoryService', () => { const recent = createMockEntity({ id: 'recent', createdAt: '2026-02-06T12:00:00.000Z' }); mockGitMem.recall = mock.fn(() => ({ memories: [old, recent], total: 2 })); - const result = await service.loadFactsDateOrdered(['test-group'], 10, { + const result = await service.loadFactsDateOrdered(10, { since: new Date('2026-02-01'), }); @@ -135,7 +121,7 @@ describe('GitMemMemoryService', () => { const recent = createMockEntity({ id: 'recent', createdAt: '2026-02-06T12:00:00.000Z' }); mockGitMem.recall = mock.fn(() => ({ memories: [old, recent], total: 2 })); - const result = await service.loadFactsDateOrdered(['test-group'], 10, { + const result = await service.loadFactsDateOrdered(10, { until: new Date('2026-01-15'), }); @@ -149,7 +135,7 @@ describe('GitMemMemoryService', () => { const entity = createMockEntity({ content: 'TypeScript config' }); mockGitMem.recall = mock.fn(() => ({ memories: [entity], total: 1 })); - const result = await service.searchFacts(['test-group'], 'typescript'); + const result = await service.searchFacts('typescript'); assert.equal(result.length, 1); assert.equal(mockGitMem.recall.mock.calls[0]?.arguments[0], 'typescript'); @@ -158,7 +144,7 @@ describe('GitMemMemoryService', () => { it('should respect limit parameter', async () => { mockGitMem.recall = mock.fn(() => ({ memories: [], total: 0 })); - await service.searchFacts(['test-group'], 'query', 5); + await service.searchFacts('query', 5); assert.equal(mockGitMem.recall.mock.calls[0]?.arguments[1]?.limit, 5); }); @@ -166,29 +152,25 @@ describe('GitMemMemoryService', () => { describe('saveMemory', () => { it('should call remember for each fact', async () => { - await service.saveMemory('mygroup', ['Fact 1', 'Fact 2', 'Fact 3']); + await service.saveMemory(['Fact 1', 'Fact 2', 'Fact 3']); assert.equal(mockGitMem.remember.mock.callCount(), 3); assert.equal(mockGitMem.remember.mock.calls[0]?.arguments[0], 'Fact 1'); - assert.deepEqual(mockGitMem.remember.mock.calls[0]?.arguments[1]?.tags, ['group:mygroup']); }); }); describe('addFact', () => { - it('should call remember with group tag', async () => { - await service.addFact('mygroup', 'A new fact'); + it('should call remember', async () => { + await service.addFact('A new fact'); assert.equal(mockGitMem.remember.mock.callCount(), 1); assert.equal(mockGitMem.remember.mock.calls[0]?.arguments[0], 'A new fact'); - const tags = mockGitMem.remember.mock.calls[0]?.arguments[1]?.tags as string[]; - assert.ok(tags.includes('group:mygroup')); }); it('should include additional tags', async () => { - await service.addFact('mygroup', 'Tagged fact', ['feature', 'auth']); + await service.addFact('Tagged fact', ['feature', 'auth']); const tags = mockGitMem.remember.mock.calls[0]?.arguments[1]?.tags as string[]; - assert.ok(tags.includes('group:mygroup')); assert.ok(tags.includes('feature')); assert.ok(tags.includes('auth')); }); @@ -196,7 +178,7 @@ describe('GitMemMemoryService', () => { describe('addFactWithLifecycle', () => { it('should pass lifecycle and confidence as tags', async () => { - await service.addFactWithLifecycle('mygroup', 'Session fact', { + await service.addFactWithLifecycle('Session fact', { lifecycle: 'session', confidence: 'medium', sourceType: 'session-capture', @@ -205,7 +187,6 @@ describe('GitMemMemoryService', () => { assert.equal(mockGitMem.remember.mock.callCount(), 1); const opts = mockGitMem.remember.mock.calls[0]?.arguments[1]; const tags = opts?.tags as string[]; - assert.ok(tags.includes('group:mygroup')); assert.ok(tags.includes('lifecycle:session')); assert.ok(tags.includes('confidence:medium')); assert.ok(tags.includes('source:session-capture')); @@ -214,21 +195,21 @@ describe('GitMemMemoryService', () => { }); it('should merge option tags without duplicates', async () => { - await service.addFactWithLifecycle('mygroup', 'Fact', { + await service.addFactWithLifecycle('Fact', { lifecycle: 'project', - tags: ['custom-tag', 'group:mygroup'], + tags: ['custom-tag', 'lifecycle:project'], }); const tags = mockGitMem.remember.mock.calls[0]?.arguments[1]?.tags as string[]; - const groupTags = tags.filter(t => t === 'group:mygroup'); - assert.equal(groupTags.length, 1); + const lifecycleTags = tags.filter(t => t === 'lifecycle:project'); + assert.equal(lifecycleTags.length, 1); assert.ok(tags.includes('custom-tag')); }); }); describe('expireFact', () => { it('should call git-mem delete with uuid', async () => { - await service.expireFact('mygroup', 'uuid-to-delete'); + await service.expireFact('uuid-to-delete'); assert.equal(mockGitMem.delete.mock.callCount(), 1); assert.equal(mockGitMem.delete.mock.calls[0]?.arguments[0], 'uuid-to-delete'); @@ -237,7 +218,7 @@ describe('GitMemMemoryService', () => { describe('cleanupExpired', () => { it('should return 0 (not supported)', async () => { - const result = await service.cleanupExpired('mygroup'); + const result = await service.cleanupExpired(); assert.equal(result, 0); }); diff --git a/tests/unit/src/lib/infrastructure/services/NlCurationService.test.ts b/tests/unit/src/lib/infrastructure/services/NlCurationService.test.ts index 626e137..b272220 100644 --- a/tests/unit/src/lib/infrastructure/services/NlCurationService.test.ts +++ b/tests/unit/src/lib/infrastructure/services/NlCurationService.test.ts @@ -57,20 +57,20 @@ function createMockLlmGuard(responseText: string): ILlmGuard & { calls: IGuardCa } function createMockMemoryService(facts: IMemoryItem[] = []): IMemoryService & { - searchCalls: Array<{ groupIds: readonly string[]; query: string; limit?: number }>; - expireCalls: Array<{ groupId: string; uuid: string }>; + searchCalls: Array<{ query: string; limit?: number }>; + expireCalls: Array<{ uuid: string }>; } { - const searchCalls: Array<{ groupIds: readonly string[]; query: string; limit?: number }> = []; - const expireCalls: Array<{ groupId: string; uuid: string }> = []; + const searchCalls: Array<{ query: string; limit?: number }> = []; + const expireCalls: Array<{ uuid: string }> = []; return { searchCalls, expireCalls, - async searchFacts(groupIds: readonly string[], query: string, limit?: number) { - searchCalls.push({ groupIds, query, limit }); + async searchFacts(query: string, limit?: number) { + searchCalls.push({ query, limit }); return facts; }, - async expireFact(groupId: string, uuid: string) { - expireCalls.push({ groupId, uuid }); + async expireFact(uuid: string) { + expireCalls.push({ uuid }); }, async loadFactsDateOrdered() { return []; }, async addMemory() { return undefined; }, @@ -81,19 +81,19 @@ function createMockMemoryService(facts: IMemoryItem[] = []): IMemoryService & { async getFactsByTags() { return []; }, async updateFact() {}, } as unknown as IMemoryService & { - searchCalls: Array<{ groupIds: readonly string[]; query: string; limit?: number }>; - expireCalls: Array<{ groupId: string; uuid: string }>; + searchCalls: Array<{ query: string; limit?: number }>; + expireCalls: Array<{ uuid: string }>; }; } function createMockCurationService(): ICurationService & { - markCalls: Array<{ groupId: string; uuid: string; mark: CurationMark }>; + markCalls: Array<{ uuid: string; mark: CurationMark }>; } { - const markCalls: Array<{ groupId: string; uuid: string; mark: CurationMark }> = []; + const markCalls: Array<{ uuid: string; mark: CurationMark }> = []; return { markCalls, - async markFact(groupId: string, uuid: string, mark: CurationMark) { - markCalls.push({ groupId, uuid, mark }); + async markFact(uuid: string, mark: CurationMark) { + markCalls.push({ uuid, mark }); }, computeQualityScore() { return 0.5; }, rankByQuality(items: readonly IMemoryItem[]) { return items; }, @@ -101,13 +101,13 @@ function createMockCurationService(): ICurationService & { } function createMockConsolidationService(): IConsolidationService & { - consolidateCalls: Array<{ groupId: string; uuids: readonly string[] }>; + consolidateCalls: Array<{ uuids: readonly string[] }>; } { - const consolidateCalls: Array<{ groupId: string; uuids: readonly string[] }> = []; + const consolidateCalls: Array<{ uuids: readonly string[] }> = []; return { consolidateCalls, - async consolidate(groupId: string, factUuids: readonly string[]) { - consolidateCalls.push({ groupId, uuids: factUuids }); + async consolidate(factUuids: readonly string[]) { + consolidateCalls.push({ uuids: factUuids }); return { action: 'archive-duplicates' as const, retainedUuid: factUuids[0]!, @@ -149,7 +149,7 @@ describe('NlCurationService', () => { createMockConsolidationService(), createMockSummarizationService() ); - await service.plan('what do we know about auth?', 'test-group'); + await service.plan('what do we know about auth?'); assert.strictEqual(guard.calls.length, 1); assert.strictEqual(guard.calls[0]?.feature, 'curation'); @@ -168,7 +168,7 @@ describe('NlCurationService', () => { createMockConsolidationService(), createMockSummarizationService() ); - const plan = await service.plan('what do we know about authentication?', 'test-group'); + const plan = await service.plan('what do we know about authentication?'); assert.strictEqual(plan.intent, 'query'); assert.strictEqual(plan.isDestructive, false); @@ -192,7 +192,7 @@ describe('NlCurationService', () => { createMockConsolidationService(), createMockSummarizationService() ); - const plan = await service.plan('mark the PostgreSQL decision as authoritative', 'test-group'); + const plan = await service.plan('mark the PostgreSQL decision as authoritative'); assert.strictEqual(plan.intent, 'mark'); assert.strictEqual(plan.isDestructive, false); @@ -215,7 +215,7 @@ describe('NlCurationService', () => { createMockConsolidationService(), createMockSummarizationService() ); - const plan = await service.plan('forget everything about the old API design', 'test-group'); + const plan = await service.plan('forget everything about the old API design'); assert.strictEqual(plan.intent, 'expire'); assert.strictEqual(plan.isDestructive, true); @@ -236,7 +236,7 @@ describe('NlCurationService', () => { createMockConsolidationService(), createMockSummarizationService() ); - const plan = await service.plan('consolidate duplicate database facts', 'test-group'); + const plan = await service.plan('consolidate duplicate database facts'); assert.strictEqual(plan.intent, 'consolidate'); assert.strictEqual(plan.isDestructive, true); @@ -255,7 +255,7 @@ describe('NlCurationService', () => { createMockConsolidationService(), createMockSummarizationService() ); - const plan = await service.plan('summarize what happened this week', 'test-group'); + const plan = await service.plan('summarize what happened this week'); assert.strictEqual(plan.intent, 'summarize'); assert.strictEqual(plan.isDestructive, false); @@ -274,7 +274,7 @@ describe('NlCurationService', () => { createMockConsolidationService(), createMockSummarizationService() ); - const plan = await service.plan('test', 'test-group'); + const plan = await service.plan('test'); assert.deepStrictEqual(plan.usage, MOCK_USAGE); }); @@ -292,7 +292,7 @@ describe('NlCurationService', () => { createMockConsolidationService(), createMockSummarizationService() ); - await service.plan('test', 'test-group'); + await service.plan('test'); assert.strictEqual(guard.calls[0]?.options?.temperature, 0.2); }); @@ -304,7 +304,7 @@ describe('NlCurationService', () => { createMockConsolidationService(), createMockSummarizationService() ); - const plan = await service.plan('something', 'test-group'); + const plan = await service.plan('something'); assert.strictEqual(plan.intent, 'query'); assert.strictEqual(plan.isDestructive, false); @@ -325,7 +325,7 @@ describe('NlCurationService', () => { createMockConsolidationService(), createMockSummarizationService() ); - const plan = await service.plan('test', 'test-group'); + const plan = await service.plan('test'); assert.strictEqual(plan.intent, 'query'); }); @@ -343,7 +343,7 @@ describe('NlCurationService', () => { createMockConsolidationService(), createMockSummarizationService() ); - const plan = await service.plan('auth stuff', 'test-group'); + const plan = await service.plan('auth stuff'); assert.strictEqual(plan.intent, 'query'); }); @@ -361,7 +361,7 @@ describe('NlCurationService', () => { createMockConsolidationService(), createMockSummarizationService() ); - const plan = await service.plan('test', 'test-group'); + const plan = await service.plan('test'); assert.ok(plan.operations.length >= 1); assert.strictEqual(plan.operations[0]?.type, 'search'); @@ -383,7 +383,7 @@ describe('NlCurationService', () => { createMockConsolidationService(), createMockSummarizationService() ); - const plan = await service.plan('test', 'test-group'); + const plan = await service.plan('test'); assert.strictEqual(plan.operations.length, 1); assert.strictEqual(plan.operations[0]?.type, 'search'); @@ -402,7 +402,7 @@ describe('NlCurationService', () => { createMockConsolidationService(), createMockSummarizationService() ); - const plan = await service.plan('forget old stuff', 'test-group'); + const plan = await service.plan('forget old stuff'); // Expire is always destructive regardless of the LLM boolean assert.strictEqual(plan.intent, 'expire'); @@ -427,7 +427,7 @@ describe('NlCurationService', () => { usage: MOCK_USAGE, }; - const result = await service.execute(plan, 'test-group'); + const result = await service.execute(plan); assert.strictEqual(result.executed, true); assert.ok(result.output.includes('Found 2 fact(s)')); @@ -455,7 +455,7 @@ describe('NlCurationService', () => { usage: MOCK_USAGE, }; - const result = await service.execute(plan, 'test-group'); + const result = await service.execute(plan); assert.strictEqual(result.executed, true); assert.ok(result.output.includes('Marked 1 fact(s) as authoritative')); @@ -481,7 +481,7 @@ describe('NlCurationService', () => { usage: MOCK_USAGE, }; - const result = await service.execute(plan, 'test-group'); + const result = await service.execute(plan); assert.strictEqual(result.executed, true); assert.ok(result.output.includes('Expired 2 fact(s)')); @@ -504,7 +504,7 @@ describe('NlCurationService', () => { usage: MOCK_USAGE, }; - const result = await service.execute(plan, 'test-group'); + const result = await service.execute(plan); assert.strictEqual(result.executed, true); assert.ok(result.output.includes('authentication')); @@ -530,7 +530,7 @@ describe('NlCurationService', () => { usage: MOCK_USAGE, }; - const result = await service.execute(plan, 'test-group'); + const result = await service.execute(plan); assert.strictEqual(result.executed, true); assert.ok(result.output.includes('Consolidated')); @@ -552,7 +552,7 @@ describe('NlCurationService', () => { usage: MOCK_USAGE, }; - const result = await service.execute(plan, 'test-group'); + const result = await service.execute(plan); assert.ok(result.output.includes('No facts found')); }); @@ -573,7 +573,7 @@ describe('NlCurationService', () => { usage: MOCK_USAGE, }; - const result = await service.execute(plan, 'test-group'); + const result = await service.execute(plan); assert.ok(result.output.includes('Invalid curation mark')); }); @@ -595,7 +595,7 @@ describe('NlCurationService', () => { usage: MOCK_USAGE, }; - const result = await service.execute(plan, 'test-group'); + const result = await service.execute(plan); assert.ok(result.output.includes('Not enough facts')); }); @@ -627,7 +627,7 @@ describe('NlCurationService', () => { usage: MOCK_USAGE, }; - const result = await service.execute(plan, 'test-group'); + const result = await service.execute(plan); assert.strictEqual(result.executed, true); assert.ok(result.output.includes('Failed')); @@ -653,7 +653,7 @@ describe('NlCurationService', () => { usage: MOCK_USAGE, }; - const result = await service.execute(plan, 'test-group'); + const result = await service.execute(plan); assert.strictEqual(result.executed, true); assert.ok(result.output.includes('Found 2 fact(s)')); @@ -674,7 +674,7 @@ describe('NlCurationService', () => { usage: MOCK_USAGE, }; - const result = await service.execute(plan, 'test-group'); + const result = await service.execute(plan); assert.strictEqual(result.plan, plan); }); diff --git a/tests/unit/src/lib/infrastructure/services/SummarizationService.test.ts b/tests/unit/src/lib/infrastructure/services/SummarizationService.test.ts index 845aafa..48223b6 100644 --- a/tests/unit/src/lib/infrastructure/services/SummarizationService.test.ts +++ b/tests/unit/src/lib/infrastructure/services/SummarizationService.test.ts @@ -39,8 +39,8 @@ const mockFacts: IMemoryItem[] = [ // ── Mock memory service ──────────────────────────────────── -interface ILoadCall { groupIds: readonly string[]; limit?: number; options?: IMemoryDateOptions } -interface ISearchCall { groupIds: readonly string[]; query: string; limit?: number } +interface ILoadCall { limit?: number; options?: IMemoryDateOptions } +interface ISearchCall { query: string; limit?: number } function createMockMemoryService(facts: IMemoryItem[] = mockFacts): IMemoryService & { loadCalls: ILoadCall[]; searchCalls: ISearchCall[] } { const loadCalls: ILoadCall[] = []; @@ -53,12 +53,12 @@ function createMockMemoryService(facts: IMemoryItem[] = mockFacts): IMemoryServi async loadMemory() { return { facts: [], nodes: [], tasks: [], initReview: null, timedOut: false } as IMemoryResult; }, - async loadFactsDateOrdered(groupIds: readonly string[], limit?: number, options?: IMemoryDateOptions) { - loadCalls.push({ groupIds, limit, options }); + async loadFactsDateOrdered(limit?: number, options?: IMemoryDateOptions) { + loadCalls.push({ limit, options }); return facts.slice(0, limit ?? facts.length); }, - async searchFacts(groupIds: readonly string[], query: string, limit?: number) { - searchCalls.push({ groupIds, query, limit }); + async searchFacts(query: string, limit?: number) { + searchCalls.push({ query, limit }); // Simulate search by filtering on topic tag return facts .filter(f => f.tags?.some(t => t.includes(query)) || f.fact?.includes(query)) @@ -109,11 +109,10 @@ describe('SummarizationService', () => { const guard = createMockLlmGuard(); const svc = createSummarizationService(memory, guard); - const result = await svc.summarize('group-1'); + const result = await svc.summarize(); // Should have loaded facts assert.strictEqual(memory.loadCalls.length, 1); - assert.deepStrictEqual(memory.loadCalls[0]?.groupIds, ['group-1']); // Should have called LLM assert.strictEqual(guard.calls.length, 1); @@ -130,7 +129,7 @@ describe('SummarizationService', () => { const guard = createMockLlmGuard(); const svc = createSummarizationService(memory, guard); - const result = await svc.summarize('group-1'); + const result = await svc.summarize(); assert.deepStrictEqual([...result.topics], ['database', 'api', 'authentication']); }); @@ -141,7 +140,7 @@ describe('SummarizationService', () => { ); const svc = createSummarizationService(memory, guard); - const result = await svc.summarize('group-1'); + const result = await svc.summarize(); assert.strictEqual(result.summary, 'This is the summary.'); }); @@ -150,7 +149,7 @@ describe('SummarizationService', () => { const guard = createMockLlmGuard(); const svc = createSummarizationService(memory, guard); - const result = await svc.summarize('group-1'); + const result = await svc.summarize(); assert.strictEqual(result.timeRange.from, '2025-01-10T10:00:00.000Z'); assert.strictEqual(result.timeRange.to, '2025-01-12T10:00:00.000Z'); }); @@ -160,7 +159,7 @@ describe('SummarizationService', () => { const guard = createMockLlmGuard(); const svc = createSummarizationService(memory, guard); - const result = await svc.summarize('group-1'); + const result = await svc.summarize(); assert.strictEqual(result.usage.inputTokens, 100); assert.strictEqual(result.usage.outputTokens, 50); assert.strictEqual(result.usage.totalTokens, 150); @@ -171,7 +170,7 @@ describe('SummarizationService', () => { const guard = createMockLlmGuard(); const svc = createSummarizationService(memory, guard); - await svc.summarize('group-1', { topic: 'database' }); + await svc.summarize({ topic: 'database' }); assert.strictEqual(memory.searchCalls.length, 1); assert.strictEqual(memory.searchCalls[0]?.query, 'database'); @@ -184,7 +183,7 @@ describe('SummarizationService', () => { const svc = createSummarizationService(memory, guard); const since = new Date('2025-01-11T00:00:00.000Z'); - await svc.summarize('group-1', { since }); + await svc.summarize({ since }); assert.strictEqual(memory.loadCalls.length, 1); assert.strictEqual(memory.loadCalls[0]?.options?.since, since); @@ -195,7 +194,7 @@ describe('SummarizationService', () => { const guard = createMockLlmGuard(); const svc = createSummarizationService(memory, guard); - await svc.summarize('group-1', { maxFacts: 2 }); + await svc.summarize({ maxFacts: 2 }); assert.strictEqual(memory.loadCalls.length, 1); assert.strictEqual(memory.loadCalls[0]?.limit, 2); @@ -206,7 +205,7 @@ describe('SummarizationService', () => { const guard = createMockLlmGuard(); const svc = createSummarizationService(memory, guard); - const result = await svc.summarize('group-1'); + const result = await svc.summarize(); assert.strictEqual(result.factCount, 0); assert.ok(result.summary.includes('No facts found')); @@ -219,7 +218,7 @@ describe('SummarizationService', () => { const guard = createMockLlmGuard(); const svc = createSummarizationService(memory, guard); - await svc.summarize('group-1', { style: 'detailed' }); + await svc.summarize({ style: 'detailed' }); assert.strictEqual(guard.calls.length, 1); assert.strictEqual(guard.calls[0]?.options?.maxTokens, 2048); @@ -230,7 +229,7 @@ describe('SummarizationService', () => { const guard = createMockLlmGuard(); const svc = createSummarizationService(memory, guard); - await svc.summarize('group-1', { style: 'concise' }); + await svc.summarize({ style: 'concise' }); assert.strictEqual(guard.calls.length, 1); assert.strictEqual(guard.calls[0]?.options?.maxTokens, 1024); @@ -241,7 +240,7 @@ describe('SummarizationService', () => { const guard = createMockLlmGuard('Just a plain summary without topics.'); const svc = createSummarizationService(memory, guard); - const result = await svc.summarize('group-1'); + const result = await svc.summarize(); assert.strictEqual(result.summary, 'Just a plain summary without topics.'); assert.strictEqual(result.topics.length, 0); }); @@ -251,7 +250,7 @@ describe('SummarizationService', () => { const guard = createMockLlmGuard('Summary text\n\nTOPICS: not-valid-json'); const svc = createSummarizationService(memory, guard); - const result = await svc.summarize('group-1'); + const result = await svc.summarize(); assert.strictEqual(result.summary, 'Summary text'); assert.strictEqual(result.topics.length, 0); }); @@ -268,7 +267,7 @@ describe('SummarizationService', () => { const svc = createSummarizationService(memory, guard); await assert.rejects( - async () => svc.summarize('group-1'), + async () => svc.summarize(), (error: unknown) => { assert.ok(error instanceof Error); assert.ok(error.message.includes('LLM provider unavailable')); @@ -288,7 +287,7 @@ describe('SummarizationService', () => { const svc = createSummarizationService(memory, guard); const since = new Date('2025-01-10T00:00:00.000Z'); - const result = await svc.summarize('group-1', { topic: 'decision', since }); + const result = await svc.summarize({ topic: 'decision', since }); // searchFacts should be called (topic path) assert.strictEqual(memory.searchCalls.length, 1); @@ -302,7 +301,7 @@ describe('SummarizationService', () => { const guard = createMockLlmGuard(); const svc = createSummarizationService(memory, guard); - await svc.summarize('group-1'); + await svc.summarize(); assert.strictEqual(guard.calls.length, 1); assert.ok(guard.calls[0]?.options?.systemPrompt); diff --git a/tests/unit/src/lib/skills/github/github.test.ts b/tests/unit/src/lib/skills/github/github.test.ts index b50a966..2da07f4 100644 --- a/tests/unit/src/lib/skills/github/github.test.ts +++ b/tests/unit/src/lib/skills/github/github.test.ts @@ -1,5 +1,7 @@ /** * Tests for GitHub issue task persistence helper. + * + * Note: Group IDs are no longer used - the git repo provides scoping via git-mem. */ import { describe, it } from 'node:test'; import assert from 'node:assert'; @@ -16,7 +18,6 @@ import type { interface ITaskCall { title: string; - groupId: string; options: ITaskWriteOptions; } @@ -41,7 +42,6 @@ function createMockTaskService(linkedTasks: ITask[] = []) { const addCalls: ITaskCall[] = []; const updateCalls: ITaskCall[] = []; const listLinkedCalls: Array<{ - groupIds: string[]; source: ExternalLinkSource | undefined; }> = []; @@ -49,13 +49,11 @@ function createMockTaskService(linkedTasks: ITask[] = []) { list: async (): Promise => ({ status: 'ok', action: 'list', - group: '', - groups: [], tasks: [], - mode: 'neo4j', + mode: 'git-mem', }), - add: async (title: string, groupId: string, options: ITaskWriteOptions): Promise => { - addCalls.push({ title, groupId, options }); + add: async (title: string, options: ITaskWriteOptions): Promise => { + addCalls.push({ title, options }); return { status: 'ok', action: 'add', @@ -66,12 +64,11 @@ function createMockTaskService(linkedTasks: ITask[] = []) { repo: options.repo || '', assignee: options.assignee || '', }, - group: groupId, - mode: 'neo4j', + mode: 'git-mem', }; }, - update: async (title: string, groupId: string, options: ITaskWriteOptions): Promise => { - updateCalls.push({ title, groupId, options }); + update: async (title: string, options: ITaskWriteOptions): Promise => { + updateCalls.push({ title, options }); return { status: 'ok', action: 'update', @@ -82,8 +79,7 @@ function createMockTaskService(linkedTasks: ITask[] = []) { repo: options.repo || '', assignee: options.assignee || '', }, - group: groupId, - mode: 'neo4j', + mode: 'git-mem', }; }, link: async (): Promise => { @@ -93,17 +89,14 @@ function createMockTaskService(linkedTasks: ITask[] = []) { throw new Error('unlink should not be called'); }, listLinked: async ( - groupIds: string[], source: ExternalLinkSource | undefined ): Promise => { - listLinkedCalls.push({ groupIds, source }); + listLinkedCalls.push({ source }); return { status: 'ok', action: 'list', - group: groupIds[0] || '', - groups: groupIds, tasks: linkedTasks, - mode: 'neo4j', + mode: 'git-mem', }; }, }; @@ -117,7 +110,6 @@ describe('persistCreatedIssueTask', () => { await persistCreatedIssueTask({ tasks: service, - groupId: 'group-1', repo: 'owner/repo', issue: { number: 42, @@ -132,7 +124,6 @@ describe('persistCreatedIssueTask', () => { assert.strictEqual(addCalls.length, 1); const call = addCalls[0]; assert.strictEqual(call.title, 'Issue title'); - assert.strictEqual(call.groupId, 'group-1'); assert.strictEqual(call.options.repo, 'owner/repo'); assert.strictEqual(call.options.assignee, 'alice'); assert.strictEqual(call.options.notes, 'Issue body'); @@ -155,7 +146,6 @@ describe('persistCreatedIssueTask', () => { await persistCreatedIssueTask({ tasks: service, - groupId: 'group-1', repo: 'owner/repo', issue: { number: 42, @@ -175,13 +165,11 @@ describe('persistCreatedIssueTask', () => { list: async (): Promise => ({ status: 'ok', action: 'list', - group: '', - groups: [], tasks: [], - mode: 'neo4j', + mode: 'git-mem', }), add: async (): Promise => { - throw new Error('Neo4j unavailable'); + throw new Error('git-mem unavailable'); }, update: async (): Promise => { throw new Error('unexpected update call'); @@ -195,17 +183,14 @@ describe('persistCreatedIssueTask', () => { listLinked: async (): Promise => ({ status: 'ok', action: 'list', - group: 'group-1', - groups: ['group-1'], tasks: [], - mode: 'neo4j', + mode: 'git-mem', }), }; await assert.rejects( () => persistCreatedIssueTask({ tasks: failingService, - groupId: 'group-1', repo: 'owner/repo', issue: { number: 42, @@ -213,7 +198,7 @@ describe('persistCreatedIssueTask', () => { title: 'Issue title', }, }), - /Neo4j unavailable/ + /git-mem unavailable/ ); }); }); diff --git a/tests/unit/src/lib/skills/shared/services/GitHubSyncService.test.ts b/tests/unit/src/lib/skills/shared/services/GitHubSyncService.test.ts index c6aaad3..4e032e3 100644 --- a/tests/unit/src/lib/skills/shared/services/GitHubSyncService.test.ts +++ b/tests/unit/src/lib/skills/shared/services/GitHubSyncService.test.ts @@ -6,15 +6,16 @@ * - Export: Lisa tasks -> GitHub Issues * - Bidirectional sync with conflict resolution * - Status mapping + * + * Note: Group IDs are no longer used - the git repo provides scoping via git-mem. + * The groupId in ISyncOptions is kept for interface compatibility but ignored. */ -import { describe, it, beforeEach } from 'node:test'; +import { describe, it } from 'node:test'; import assert from 'node:assert'; import { createGitHubSyncService, lisaStatusToGitHub, gitHubStatusToLisa, - type IGitHubSyncService, - type ISyncOptions, } from '../../../../../../../src/lib/skills/shared/services/GitHubSyncService'; import type { IGitHubClient, IGitHubIssue } from '../../../../../../../src/lib/skills/shared/services/GitHubService'; import type { @@ -24,7 +25,6 @@ import type { ITaskWriteResult, ITaskLinkResult, ITaskExternalLink, - ExternalLinkSource, ITaskWriteOptions, } from '../../../../../../../src/lib/skills/shared/services/interfaces'; @@ -83,46 +83,38 @@ function createMockTaskService(overrides: Partial = {}): ITaskServ list: async (): Promise => ({ status: 'ok', action: 'list', - group: 'test-group', - groups: ['test-group'], tasks: [], - mode: 'neo4j', + mode: 'git-mem', }), listLinked: async (): Promise => ({ status: 'ok', action: 'list', - group: 'test-group', - groups: ['test-group'], tasks: [], - mode: 'neo4j', + mode: 'git-mem', }), add: async (title: string): Promise => ({ status: 'ok', action: 'add', task: { type: 'task', title, status: 'ready', repo: '', assignee: '' }, - group: 'test-group', - mode: 'mcp', + mode: 'git-mem', }), update: async (title: string): Promise => ({ status: 'ok', action: 'update', task: { type: 'task', title, status: 'ready', repo: '', assignee: '' }, - group: 'test-group', - mode: 'mcp', + mode: 'git-mem', }), - link: async (taskUuid: string, _groupId: string, externalLink: ITaskExternalLink): Promise => ({ + link: async (taskUuid: string, externalLink: ITaskExternalLink): Promise => ({ status: 'ok', action: 'link', task: { title: 'Task', uuid: taskUuid, externalLink }, - group: 'test-group', - mode: 'mcp', + mode: 'git-mem', }), unlink: async (taskUuid: string): Promise => ({ status: 'ok', action: 'unlink', task: { title: 'Task', uuid: taskUuid }, - group: 'test-group', - mode: 'mcp', + mode: 'git-mem', }), ...overrides, }; @@ -209,8 +201,8 @@ describe('GitHubSyncService', () => { describe('sync() - Import', () => { it('should import new GitHub issues as Lisa tasks', async () => { - const addCalls: Array<{ title: string; groupId: string; options: ITaskWriteOptions }> = []; - + const addCalls: Array<{ title: string; options: ITaskWriteOptions }> = []; + const github = createMockGitHubClient({ listIssues: async () => ({ issues: [ @@ -225,27 +217,22 @@ describe('GitHubSyncService', () => { list: async () => ({ status: 'ok', action: 'list', - group: 'test', - groups: ['test'], tasks: [], - mode: 'neo4j', + mode: 'git-mem', }), listLinked: async () => ({ status: 'ok', action: 'list', - group: 'test', - groups: ['test'], tasks: [], - mode: 'neo4j', + mode: 'git-mem', }), - add: async (title, groupId, options) => { - addCalls.push({ title, groupId, options }); + add: async (title, options) => { + addCalls.push({ title, options }); return { status: 'ok', action: 'add', task: { type: 'task', title, status: 'ready', repo: '', assignee: '' }, - group: groupId, - mode: 'mcp', + mode: 'git-mem', }; }, }); @@ -277,28 +264,24 @@ describe('GitHubSyncService', () => { list: async () => ({ status: 'ok', action: 'list', - group: 'test', - groups: ['test'], tasks: [ createMockTask({ title: 'Issue 1', externalLink: { source: 'github', id: '1', url: 'https://github.com/owner/repo/issues/1' }, }), ], - mode: 'neo4j', + mode: 'git-mem', }), listLinked: async () => ({ status: 'ok', action: 'list', - group: 'test', - groups: ['test'], tasks: [ createMockTask({ title: 'Issue 1', externalLink: { source: 'github', id: '1', url: 'https://github.com/owner/repo/issues/1' }, }), ], - mode: 'neo4j', + mode: 'git-mem', }), }); @@ -327,8 +310,6 @@ describe('GitHubSyncService', () => { list: async () => ({ status: 'ok', action: 'list', - group: 'test', - groups: ['test'], tasks: [ createMockTask({ title: 'Issue 1', @@ -336,13 +317,11 @@ describe('GitHubSyncService', () => { externalLink: { source: 'github', id: '1', url: 'https://github.com/owner/repo/issues/1' }, }), ], - mode: 'neo4j', + mode: 'git-mem', }), listLinked: async () => ({ status: 'ok', action: 'list', - group: 'test', - groups: ['test'], tasks: [ createMockTask({ title: 'Issue 1', @@ -350,16 +329,15 @@ describe('GitHubSyncService', () => { externalLink: { source: 'github', id: '1', url: 'https://github.com/owner/repo/issues/1' }, }), ], - mode: 'neo4j', + mode: 'git-mem', }), - update: async (title, _groupId, options) => { + update: async (title, options) => { updateCalls.push({ title, options }); return { status: 'ok', action: 'update', task: { type: 'task', title, status: options.status || 'ready', repo: '', assignee: '' }, - group: 'test', - mode: 'mcp', + mode: 'git-mem', }; }, }); @@ -393,8 +371,7 @@ describe('GitHubSyncService', () => { status: 'ok', action: 'add', task: { type: 'task', title: 'Issue 1', status: 'ready', repo: '', assignee: '' }, - group: 'test', - mode: 'mcp', + mode: 'git-mem', }; }, }); @@ -434,22 +411,19 @@ describe('GitHubSyncService', () => { list: async () => ({ status: 'ok', action: 'list', - group: 'test', - groups: ['test'], tasks: [ createMockTask({ title: 'Task 1', uuid: 'uuid-1' }), createMockTask({ title: 'Task 2', uuid: 'uuid-2' }), ], - mode: 'neo4j', + mode: 'git-mem', }), - link: async (taskUuid, _groupId, externalLink) => { + link: async (taskUuid, externalLink) => { linkCalls.push({ taskUuid, externalLink }); return { status: 'ok', action: 'link', task: { title: 'Task', uuid: taskUuid, externalLink }, - group: 'test', - mode: 'mcp', + mode: 'git-mem', }; }, }); @@ -484,10 +458,8 @@ describe('GitHubSyncService', () => { list: async () => ({ status: 'ok', action: 'list', - group: 'test', - groups: ['test'], tasks: [createMockTask({ title: 'Task 1', status: 'done', uuid: 'uuid-1' })], - mode: 'neo4j', + mode: 'git-mem', }), }); @@ -516,10 +488,8 @@ describe('GitHubSyncService', () => { list: async () => ({ status: 'ok', action: 'list', - group: 'test', - groups: ['test'], tasks: [createMockTask({ title: 'Task 1', status: 'in-progress', uuid: 'uuid-1' })], - mode: 'neo4j', + mode: 'git-mem', }), }); @@ -561,20 +531,16 @@ describe('GitHubSyncService', () => { list: async () => ({ status: 'ok', action: 'list', - group: 'test', - groups: ['test'], tasks: [ createMockTask({ title: 'New Task', uuid: 'uuid-new' }), // No external link - will be exported ], - mode: 'neo4j', + mode: 'git-mem', }), listLinked: async () => ({ status: 'ok', action: 'list', - group: 'test', - groups: ['test'], tasks: [], - mode: 'neo4j', + mode: 'git-mem', }), add: async () => { addCalls++; @@ -582,8 +548,7 @@ describe('GitHubSyncService', () => { status: 'ok', action: 'add', task: { type: 'task', title: 'Existing Issue', status: 'ready', repo: '', assignee: '' }, - group: 'test', - mode: 'mcp', + mode: 'git-mem', }; }, }); @@ -623,8 +588,6 @@ describe('GitHubSyncService', () => { list: async () => ({ status: 'ok', action: 'list', - group: 'test', - groups: ['test'], tasks: [ createMockTask({ title: 'Conflicting Issue', @@ -634,13 +597,11 @@ describe('GitHubSyncService', () => { externalLink: { source: 'github', id: '1', url: 'https://github.com/owner/repo/issues/1' }, }), ], - mode: 'neo4j', + mode: 'git-mem', }), listLinked: async () => ({ status: 'ok', action: 'list', - group: 'test', - groups: ['test'], tasks: [ createMockTask({ title: 'Conflicting Issue', @@ -650,16 +611,15 @@ describe('GitHubSyncService', () => { externalLink: { source: 'github', id: '1', url: 'https://github.com/owner/repo/issues/1' }, }), ], - mode: 'neo4j', + mode: 'git-mem', }), - update: async (title, _groupId, options) => { + update: async (title, options) => { updateCalls.push({ title, status: options.status }); return { status: 'ok', action: 'update', task: { type: 'task', title, status: options.status || 'ready', repo: '', assignee: '' }, - group: 'test', - mode: 'mcp', + mode: 'git-mem', }; }, }); diff --git a/tests/unit/src/lib/skills/shared/services/MemoryCliService.test.ts b/tests/unit/src/lib/skills/shared/services/MemoryCliService.test.ts index 60aff26..c57a9d6 100644 --- a/tests/unit/src/lib/skills/shared/services/MemoryCliService.test.ts +++ b/tests/unit/src/lib/skills/shared/services/MemoryCliService.test.ts @@ -103,78 +103,70 @@ describe('parseTtlDuration', () => { // --- MemoryCliService expire/cleanup command tests --- describe('MemoryCliService', () => { - let expireCalls: Array<{ groupId: string; uuid: string }>; - let cleanupCalls: Array<{ groupId: string; dryRun: boolean }>; - let addCalls: Array<{ text: string; groupId: string; options: unknown }>; - let conflictsCalls: Array<{ groupIds: string[]; topic?: string }>; - let dedupeCalls: Array<{ groupId: string; options?: unknown }>; + let expireCalls: Array<{ uuid: string }>; + let cleanupCalls: Array<{ dryRun: boolean }>; + let addCalls: Array<{ text: string; options: unknown }>; + let conflictsCalls: Array<{ topic?: string }>; + let dedupeCalls: Array<{ options?: unknown }>; const memoryService: IMemoryService = { - load: async (groupIds): Promise => ({ + load: async (): Promise => ({ status: 'ok', action: 'load', - group: groupIds[0] || '', - groups: groupIds, query: '', facts: [], - mode: 'neo4j', + mode: 'git-mem', }), - add: async (text, groupId, options): Promise => { - addCalls.push({ text, groupId, options }); + add: async (text, options): Promise => { + addCalls.push({ text, options }); return { status: 'ok', action: 'add', - group: groupId, text, - mode: 'mcp', + mode: 'git-mem', }; }, - expire: async (groupId, uuid): Promise => { - expireCalls.push({ groupId, uuid }); + expire: async (uuid): Promise => { + expireCalls.push({ uuid }); return { status: 'ok', action: 'expire', - group: groupId, uuid, found: true, - mode: 'neo4j', + mode: 'git-mem', }; }, - cleanup: async (groupId, dryRun): Promise => { - cleanupCalls.push({ groupId, dryRun }); + cleanup: async (dryRun): Promise => { + cleanupCalls.push({ dryRun }); return { status: 'ok', action: 'cleanup', - group: groupId, expiredCount: 3, dryRun, - mode: 'neo4j', + mode: 'git-mem', }; }, - conflicts: async (groupIds, topic): Promise => { - conflictsCalls.push({ groupIds, topic }); + conflicts: async (topic): Promise => { + conflictsCalls.push({ topic }); return { status: 'ok', action: 'conflicts', - group: groupIds[0] || '', - groups: groupIds, topic: topic || '', conflictGroups: [], totalConflicts: 0, - mode: 'neo4j', + mode: 'git-mem', }; }, - dedupe: async (groupId, options): Promise => { - dedupeCalls.push({ groupId, options }); + dedupe: async (options): Promise => { + dedupeCalls.push({ options }); return { status: 'ok', action: 'dedupe', - group: groupId, totalFactsScanned: 0, duplicateGroups: [], totalDuplicates: 0, minSimilarity: (options as Record)?.minSimilarity as number ?? 0.6, - mode: 'neo4j', + mode: 'git-mem', }; }, }; @@ -197,6 +189,10 @@ describe('MemoryCliService', () => { uuid: null, topic: null, minSimilarity: null, + mark: null, + action: null, + retain: null, + mergedText: null, }; } @@ -213,8 +209,6 @@ describe('MemoryCliService', () => { logger: noopLogger, cache: noopCache, memoryService, - getGroupIds: () => ['test-group'], - getCurrentGroupId: () => 'test-group', resolveTag: (_text, explicitTag, entityType) => { if (explicitTag) return explicitTag; if (entityType === 'session') return 'lifecycle:session'; @@ -232,7 +226,6 @@ describe('MemoryCliService', () => { assert.strictEqual(expireCalls.length, 1); assert.strictEqual(expireCalls[0].uuid, 'abc-123'); - assert.strictEqual(expireCalls[0].groupId, 'test-group'); assert.strictEqual(result.action, 'expire'); }); @@ -248,17 +241,6 @@ describe('MemoryCliService', () => { assert.strictEqual(result.action, 'expire'); }); - it('should use explicit group if provided', async () => { - await cliService.run({ - ...defaultArgs(), - command: 'expire', - uuid: 'abc-123', - explicitGroup: 'custom-group', - }); - - assert.strictEqual(expireCalls[0].groupId, 'custom-group'); - }); - it('should throw if no uuid provided', async () => { await assert.rejects( () => cliService.run({ ...defaultArgs(), command: 'expire' }), @@ -276,7 +258,6 @@ describe('MemoryCliService', () => { assert.strictEqual(cleanupCalls.length, 1); assert.strictEqual(cleanupCalls[0].dryRun, false); - assert.strictEqual(cleanupCalls[0].groupId, 'test-group'); assert.strictEqual(result.action, 'cleanup'); }); @@ -292,16 +273,6 @@ describe('MemoryCliService', () => { const cleanupResult = result as IMemoryCleanupResult; assert.strictEqual(cleanupResult.dryRun, true); }); - - it('should use explicit group if provided', async () => { - await cliService.run({ - ...defaultArgs(), - command: 'cleanup', - explicitGroup: 'custom-group', - }); - - assert.strictEqual(cleanupCalls[0].groupId, 'custom-group'); - }); }); describe('add with --lifecycle flag', () => { @@ -350,14 +321,13 @@ describe('MemoryCliService', () => { }); describe('conflicts command', () => { - it('should call memoryService.conflicts with default group IDs', async () => { + it('should call memoryService.conflicts', async () => { const result = await cliService.run({ ...defaultArgs(), command: 'conflicts', }); assert.strictEqual(conflictsCalls.length, 1); - assert.deepStrictEqual(conflictsCalls[0].groupIds, ['test-group']); assert.strictEqual(conflictsCalls[0].topic, undefined); assert.strictEqual(result.action, 'conflicts'); }); @@ -372,17 +342,6 @@ describe('MemoryCliService', () => { assert.strictEqual(conflictsCalls.length, 1); assert.strictEqual(conflictsCalls[0].topic, 'type:decision'); }); - - it('should use explicit group if provided', async () => { - await cliService.run({ - ...defaultArgs(), - command: 'conflicts', - explicitGroup: 'custom-group', - }); - - assert.strictEqual(conflictsCalls.length, 1); - assert.deepStrictEqual(conflictsCalls[0].groupIds, ['custom-group']); - }); }); describe('dedupe command', () => { @@ -393,7 +352,6 @@ describe('MemoryCliService', () => { }); assert.strictEqual(dedupeCalls.length, 1); - assert.strictEqual(dedupeCalls[0].groupId, 'test-group'); assert.strictEqual(result.action, 'dedupe'); }); @@ -443,16 +401,6 @@ describe('MemoryCliService', () => { { message: /Invalid --since date/ } ); }); - - it('should use explicit group if provided', async () => { - await cliService.run({ - ...defaultArgs(), - command: 'dedupe', - explicitGroup: 'custom-group', - }); - - assert.strictEqual(dedupeCalls[0].groupId, 'custom-group'); - }); }); describe('invalid command', () => { diff --git a/tests/unit/src/lib/skills/shared/services/TaskCliService.test.ts b/tests/unit/src/lib/skills/shared/services/TaskCliService.test.ts index 10b3ac5..2d7eb16 100644 --- a/tests/unit/src/lib/skills/shared/services/TaskCliService.test.ts +++ b/tests/unit/src/lib/skills/shared/services/TaskCliService.test.ts @@ -1,5 +1,7 @@ /** * Tests for TaskCliService default date filtering. + * + * Note: Group IDs are no longer used - the git repo provides scoping via git-mem. */ import { describe, it } from 'node:test'; import assert from 'node:assert'; @@ -41,55 +43,47 @@ const env: IEnvConfig = { }; function createTaskServiceRecorder() { - const listCalls: Array<{ groupIds: string[]; limit: number; options?: unknown }> = []; + const listCalls: Array<{ limit: number; options?: unknown }> = []; const service: ITaskService = { - list: async (groupIds, limit, _repo, _assignee, options): Promise => { - listCalls.push({ groupIds, limit, options }); + list: async (limit, _repo, _assignee, options): Promise => { + listCalls.push({ limit, options }); return { status: 'ok', action: 'list', - group: groupIds[0] || '', - groups: groupIds, tasks: [], - mode: 'neo4j', + mode: 'git-mem', }; }, listLinked: async (): Promise => ({ status: 'ok', action: 'list', - group: 'group-1', - groups: ['group-1'], tasks: [], - mode: 'neo4j', + mode: 'git-mem', }), add: async (): Promise => ({ status: 'ok', action: 'add', task: { type: 'task', title: 'x', status: 'todo', repo: '', assignee: '' }, - group: 'group-1', - mode: 'neo4j', + mode: 'git-mem', }), update: async (): Promise => ({ status: 'ok', action: 'update', task: { type: 'task', title: 'x', status: 'todo', repo: '', assignee: '' }, - group: 'group-1', - mode: 'neo4j', + mode: 'git-mem', }), link: async (): Promise => ({ status: 'ok', action: 'link', task: { title: 'x', uuid: 'task-1' }, - group: 'group-1', - mode: 'neo4j', + mode: 'git-mem', }), unlink: async (): Promise => ({ status: 'ok', action: 'unlink', task: { title: 'x', uuid: 'task-1' }, - group: 'group-1', - mode: 'neo4j', + mode: 'git-mem', }), }; @@ -104,14 +98,11 @@ describe('TaskCliService list defaults', () => { logger: noopLogger, cache: noopCache, taskService: service, - getGroupIds: () => ['group-1'], - getCurrentGroupId: () => 'group-1', }); await cli.run({ command: 'list', payload: '', - explicitGroup: null, limit: 20, status: 'todo', tag: null, @@ -139,14 +130,11 @@ describe('TaskCliService list defaults', () => { logger: noopLogger, cache: noopCache, taskService: service, - getGroupIds: () => ['group-1'], - getCurrentGroupId: () => 'group-1', }); await cli.run({ command: 'list', payload: '', - explicitGroup: null, limit: 20, status: 'todo', tag: null, diff --git a/tests/unit/src/lib/skills/shared/utils/group-id.test.ts b/tests/unit/src/lib/skills/shared/utils/group-id.test.ts deleted file mode 100644 index 4a52375..0000000 --- a/tests/unit/src/lib/skills/shared/utils/group-id.test.ts +++ /dev/null @@ -1,221 +0,0 @@ -/** - * Tests for group-id utility functions. - * - * Covers: - * - normalizeGroupId() with Windows and Unix paths - * - getCurrentGroupId() with .lisa directory traversal - * - getGroupIdsWithLegacy() backward compatibility - * - getHierarchicalGroupIds() parent path generation - */ -import { describe, it, beforeEach, afterEach } from 'node:test'; -import assert from 'node:assert'; -import fs from 'fs'; -import path from 'path'; -import os from 'os'; -import { - normalizeGroupId, - getCurrentGroupId, - getGroupIds, - getGroupIdsWithLegacy, - getHierarchicalGroupIds, - createZepUserId, - createZepThreadId, -} from '../../../../../../../src/lib/skills/shared/utils/group-id'; - -describe('group-id', () => { - describe('normalizeGroupId()', () => { - it('normalizeGroupId_givenWindowsPath_shouldNormalizeToLowercaseHyphenated', () => { - const result = normalizeGroupId('C:\\dev\\lisa'); - assert.strictEqual(result, 'c-dev-lisa'); - }); - - it('normalizeGroupId_givenUnixPath_shouldNormalizeToHyphenated', () => { - const result = normalizeGroupId('/home/user/lisa'); - assert.strictEqual(result, 'home-user-lisa'); - }); - - it('normalizeGroupId_givenWindowsDeepNesting_shouldPreserveSegments', () => { - const result = normalizeGroupId('C:\\Users\\tony\\Projects\\my-app'); - assert.strictEqual(result, 'c-users-tony-projects-my-app'); - }); - - it('normalizeGroupId_givenUnixDeepNesting_shouldPreserveSegments', () => { - const result = normalizeGroupId('/home/tony/projects/my-app'); - assert.strictEqual(result, 'home-tony-projects-my-app'); - }); - - it('normalizeGroupId_givenDotsInSegment_shouldReplaceDotsWithUnderscores', () => { - const result = normalizeGroupId('/home/tony.casey/repos/api'); - assert.strictEqual(result, 'home-tony_casey-repos-api'); - }); - - it('normalizeGroupId_givenMultipleConsecutiveSeparators_shouldCollapseToSingleDash', () => { - const result = normalizeGroupId('/home//user///lisa'); - assert.strictEqual(result, 'home-user-lisa'); - }); - - it('normalizeGroupId_givenBasenameOnly_shouldReturnBasename', () => { - const result = normalizeGroupId('lisa'); - assert.strictEqual(result, 'lisa'); - }); - - it('normalizeGroupId_givenMixedCase_shouldLowercaseAll', () => { - const result = normalizeGroupId('MyProject'); - assert.strictEqual(result, 'myproject'); - }); - - it('normalizeGroupId_givenEmptyString_shouldReturnEmptyString', () => { - const result = normalizeGroupId(''); - assert.strictEqual(result, ''); - }); - - it('normalizeGroupId_givenTrailingSeparators_shouldTrimTrailing', () => { - const result = normalizeGroupId('/home/user/lisa/'); - assert.strictEqual(result, 'home-user-lisa'); - }); - - it('normalizeGroupId_givenMixedSeparators_shouldNormalizeBoth', () => { - const result = normalizeGroupId('C:\\Users/tony\\projects/app'); - assert.strictEqual(result, 'c-users-tony-projects-app'); - }); - }); - - describe('getCurrentGroupId()', () => { - let tempDir: string; - - beforeEach(() => { - tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lisa-group-id-test-')); - }); - - afterEach(() => { - try { - fs.rmSync(tempDir, { recursive: true, force: true }); - } catch { - // Ignore cleanup errors - } - }); - - it('getCurrentGroupId_givenDotLisaInCurrentDir_shouldReturnNormalizedGroupId', () => { - fs.mkdirSync(path.join(tempDir, '.lisa'), { recursive: true }); - const result = getCurrentGroupId(tempDir); - assert.strictEqual(result, normalizeGroupId(tempDir)); - }); - - it('getCurrentGroupId_givenDotLisaInParentDir_shouldReturnParentNormalizedGroupId', () => { - fs.mkdirSync(path.join(tempDir, '.lisa'), { recursive: true }); - const subDir = path.join(tempDir, 'src', 'lib'); - fs.mkdirSync(subDir, { recursive: true }); - const result = getCurrentGroupId(subDir); - // Should resolve to tempDir (where .lisa is), not subDir - assert.strictEqual(result, normalizeGroupId(tempDir)); - }); - - it('getCurrentGroupId_givenDotLisaSeveralLevelsUp_shouldReturnAncestorNormalizedGroupId', () => { - fs.mkdirSync(path.join(tempDir, '.lisa'), { recursive: true }); - const deepDir = path.join(tempDir, 'src', 'lib', 'skills', 'shared'); - fs.mkdirSync(deepDir, { recursive: true }); - const result = getCurrentGroupId(deepDir); - assert.strictEqual(result, normalizeGroupId(tempDir)); - }); - - it('getCurrentGroupId_givenNoLocalDotLisa_shouldReturnValidNormalizedGroupId', () => { - // No .lisa directory in tempDir - may find one in an ancestor directory. - // Either way, the result should be a valid normalized group ID. - const result = getCurrentGroupId(tempDir); - assert.ok(result.length > 0, 'Should return a non-empty group ID'); - assert.ok(!/[:\\\/]/.test(result), 'Should not contain path separators or colons'); - }); - - it('getCurrentGroupId_givenDifferentSubdirectories_shouldProduceConsistentIds', () => { - fs.mkdirSync(path.join(tempDir, '.lisa'), { recursive: true }); - const dir1 = path.join(tempDir, 'src'); - const dir2 = path.join(tempDir, 'tests', 'unit'); - fs.mkdirSync(dir1, { recursive: true }); - fs.mkdirSync(dir2, { recursive: true }); - - const id1 = getCurrentGroupId(dir1); - const id2 = getCurrentGroupId(dir2); - const idRoot = getCurrentGroupId(tempDir); - - assert.strictEqual(id1, id2, 'IDs from different subdirectories should match'); - assert.strictEqual(id1, idRoot, 'Subdirectory ID should match root ID'); - }); - }); - - describe('getGroupIdsWithLegacy()', () => { - let tempDir: string; - - beforeEach(() => { - tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lisa-group-id-test-')); - }); - - afterEach(() => { - try { - fs.rmSync(tempDir, { recursive: true, force: true }); - } catch { - // Ignore cleanup errors - } - }); - - it('getGroupIdsWithLegacy_givenRepoWithLisaFolder_shouldIncludeCanonicalAndLegacyIds', () => { - fs.mkdirSync(path.join(tempDir, '.lisa'), { recursive: true }); - const result = getGroupIdsWithLegacy(tempDir); - - const canonicalId = normalizeGroupId(tempDir); - const legacyId = normalizeGroupId(path.basename(tempDir)); - - assert.ok(result.includes(canonicalId), 'Should include canonical full-path ID'); - if (legacyId !== canonicalId) { - assert.ok(result.includes(legacyId), 'Should include legacy basename ID'); - } - }); - - it('getGroupIdsWithLegacy_givenCanonicalEqualsLegacy_shouldNotContainDuplicates', () => { - // If the folder name equals the full path normalization (unlikely but test the logic) - const result = getGroupIdsWithLegacy(tempDir); - const uniqueIds = new Set(result); - assert.strictEqual(result.length, uniqueIds.size, 'Should not contain duplicates'); - }); - - it('getGroupIdsWithLegacy_givenLisaFolder_shouldMatchGetGroupIdsOutput', () => { - fs.mkdirSync(path.join(tempDir, '.lisa'), { recursive: true }); - const legacy = getGroupIdsWithLegacy(tempDir); - const regular = getGroupIds(tempDir); - assert.deepStrictEqual(regular, legacy, 'getGroupIds should delegate to getGroupIdsWithLegacy'); - }); - }); - - describe('getHierarchicalGroupIds()', () => { - it('getHierarchicalGroupIds_givenPathAndDepth3_shouldReturnGroupIdsForPathAndParents', () => { - const result = getHierarchicalGroupIds('/home/user/projects/lisa', 3); - assert.strictEqual(result.length, 3); - assert.strictEqual(result[0], 'home-user-projects-lisa'); - assert.strictEqual(result[1], 'home-user-projects'); - assert.strictEqual(result[2], 'home-user'); - }); - - it('getHierarchicalGroupIds_givenMaxDepth1_shouldRespectMaxDepth', () => { - const result = getHierarchicalGroupIds('/home/user/projects/lisa', 1); - assert.strictEqual(result.length, 1); - assert.strictEqual(result[0], 'home-user-projects-lisa'); - }); - - it('getHierarchicalGroupIds_givenShortPath_shouldNotIncludeDuplicates', () => { - const result = getHierarchicalGroupIds('/home/user', 5); - const unique = new Set(result); - assert.strictEqual(result.length, unique.size); - }); - }); - - describe('createZepUserId()', () => { - it('createZepUserId_givenProjectName_shouldPrefixWithLisa', () => { - assert.strictEqual(createZepUserId('my-project'), 'lisa-my-project'); - }); - }); - - describe('createZepThreadId()', () => { - it('createZepThreadId_givenProjectAndPurpose_shouldReturnThreadId', () => { - assert.strictEqual(createZepThreadId('my-project', 'memory'), 'lisa-memory-my-project'); - }); - }); -});