From 6c8ee37a085f8aea3ddf4b5e45e3680165912ced Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Thu, 25 Jun 2026 12:23:02 +0200 Subject: [PATCH] feat(services): add handoff service and refactor CLI --- src/cli/commands/handoff.ts | 43 +++++------------------ src/core/services/handoffService.ts | 54 +++++++++++++++++++++++++++++ tests/handoffService.test.ts | 19 ++++++++++ 3 files changed, 81 insertions(+), 35 deletions(-) create mode 100644 src/core/services/handoffService.ts create mode 100644 tests/handoffService.test.ts diff --git a/src/cli/commands/handoff.ts b/src/cli/commands/handoff.ts index 89e6f9f..841fbec 100644 --- a/src/cli/commands/handoff.ts +++ b/src/cli/commands/handoff.ts @@ -1,10 +1,6 @@ import { input, select } from '@inquirer/prompts'; -import { join } from 'path'; -import { getEntityDir } from '../../core/paths.js'; -import { getNextId } from '../../core/counter.js'; -import { readEntity, writeEntity } from '../../core/files.js'; -import { HandoffSchema, type Handoff } from '../../core/schema.js'; -import { Index } from '../../core/index.js'; +import { createHandoff, listHandoffs, getHandoff } from '../../core/services/handoffService.js'; +import type { Handoff } from '../../core/schema.js'; const roles = ['architect', 'implementer', 'reviewer', 'tester']; @@ -15,9 +11,7 @@ export async function handoffCreate(cwd: string, options: Partial = {}) const summary = options.summary ?? await input({ message: 'Summary:' }); const context = options.context ?? await input({ message: 'Context:' }); - const now = new Date().toISOString(); - const handoff: Handoff = HandoffSchema.parse({ - id: getNextId(cwd, 'handoff'), + const handoff = createHandoff(cwd, { fromRole, toRole, fromAgent: options.fromAgent, @@ -25,46 +19,25 @@ export async function handoffCreate(cwd: string, options: Partial = {}) taskId: taskId || undefined, summary, context, - createdAt: now, }); - const filePath = join(getEntityDir(cwd, 'handoffs'), `${handoff.id}.md`); - writeEntity(filePath, handoff, `# ${handoff.summary}\n\n${handoff.context}`); - - const index = new Index(cwd); - index.upsert({ - id: handoff.id, - type: 'handoff', - title: handoff.summary, - content: handoff.context, - filePath, - createdAt: handoff.createdAt, - updatedAt: handoff.createdAt, - }); - index.close(); - console.log(`Handoff created: ${handoff.id}`); } export function handoffRead(cwd: string, id: string): void { - const filePath = join(getEntityDir(cwd, 'handoffs'), `${id}.md`); - const { frontmatter, body } = readEntity(filePath); - console.log(`# ${frontmatter.summary}`); - console.log(`From: ${frontmatter.fromRole} → ${frontmatter.toRole}`); - if (frontmatter.taskId) console.log(`Task: ${frontmatter.taskId}`); + const { handoff, body } = getHandoff(cwd, id); + console.log(`# ${handoff.summary}`); + console.log(`From: ${handoff.fromRole} → ${handoff.toRole}`); + if (handoff.taskId) console.log(`Task: ${handoff.taskId}`); console.log('\n' + body); } export function handoffList(cwd: string): void { - const index = new Index(cwd); - const handoffs = index.list('handoff'); - index.close(); - + const handoffs = listHandoffs(cwd); if (handoffs.length === 0) { console.log('No handoffs found.'); return; } - for (const h of handoffs) { console.log(`${h.id}: ${h.title}`); } diff --git a/src/core/services/handoffService.ts b/src/core/services/handoffService.ts new file mode 100644 index 0000000..7880527 --- /dev/null +++ b/src/core/services/handoffService.ts @@ -0,0 +1,54 @@ +import { join } from 'path'; +import { getEntityDir } from '../paths.js'; +import { getNextId } from '../counter.js'; +import { readEntity, writeEntity } from '../files.js'; +import { HandoffSchema, type Handoff } from '../schema.js'; +import { Index } from '../index.js'; + +export function createHandoff(cwd: string, options: Partial = {}): Handoff { + const now = new Date().toISOString(); + const handoff: Handoff = HandoffSchema.parse({ + id: getNextId(cwd, 'handoff'), + fromRole: options.fromRole ?? 'user', + toRole: options.toRole ?? 'user', + fromAgent: options.fromAgent, + toAgent: options.toAgent, + taskId: options.taskId, + summary: options.summary ?? 'Handoff', + context: options.context ?? '', + decisions: options.decisions ?? [], + openQuestions: options.openQuestions ?? [], + nextSteps: options.nextSteps ?? [], + createdAt: now, + }); + + const filePath = join(getEntityDir(cwd, 'handoffs'), `${handoff.id}.md`); + writeEntity(filePath, handoff, `# ${handoff.summary}\n\n${handoff.context}`); + + const index = new Index(cwd); + index.upsert({ + id: handoff.id, + type: 'handoff', + title: handoff.summary, + content: handoff.context, + filePath, + createdAt: handoff.createdAt, + updatedAt: handoff.createdAt, + }); + index.close(); + + return handoff; +} + +export function listHandoffs(cwd: string): ReturnType { + const index = new Index(cwd); + const handoffs = index.list('handoff'); + index.close(); + return handoffs; +} + +export function getHandoff(cwd: string, id: string): { handoff: Handoff; body: string; filePath: string } { + const filePath = join(getEntityDir(cwd, 'handoffs'), `${id}.md`); + const { frontmatter, body } = readEntity(filePath); + return { handoff: HandoffSchema.parse(frontmatter), body, filePath }; +} diff --git a/tests/handoffService.test.ts b/tests/handoffService.test.ts new file mode 100644 index 0000000..24706cd --- /dev/null +++ b/tests/handoffService.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtempSync, rmSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { createHandoff, listHandoffs, getHandoff } from '../src/core/services/handoffService.js'; + +describe('handoffService', () => { + let cwd: string; + + beforeEach(() => { cwd = mkdtempSync(join(tmpdir(), 'ah-hof-')); }); + afterEach(() => { rmSync(cwd, { recursive: true, force: true }); }); + + it('creates and reads a handoff', () => { + const h = createHandoff(cwd, { fromRole: 'architect', toRole: 'implementer', summary: 's', context: 'c' }); + expect(h.id).toBe('HOF-0001'); + expect(getHandoff(cwd, h.id).handoff.summary).toBe('s'); + expect(listHandoffs(cwd)).toHaveLength(1); + }); +});