feat(services): add handoff service and refactor CLI

This commit is contained in:
chahinebrini 2026-06-25 12:23:02 +02:00
parent b4874afbe9
commit 6c8ee37a08
3 changed files with 81 additions and 35 deletions

View File

@ -1,10 +1,6 @@
import { input, select } from '@inquirer/prompts'; import { input, select } from '@inquirer/prompts';
import { join } from 'path'; import { createHandoff, listHandoffs, getHandoff } from '../../core/services/handoffService.js';
import { getEntityDir } from '../../core/paths.js'; import type { Handoff } from '../../core/schema.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';
const roles = ['architect', 'implementer', 'reviewer', 'tester']; const roles = ['architect', 'implementer', 'reviewer', 'tester'];
@ -15,9 +11,7 @@ export async function handoffCreate(cwd: string, options: Partial<Handoff> = {})
const summary = options.summary ?? await input({ message: 'Summary:' }); const summary = options.summary ?? await input({ message: 'Summary:' });
const context = options.context ?? await input({ message: 'Context:' }); const context = options.context ?? await input({ message: 'Context:' });
const now = new Date().toISOString(); const handoff = createHandoff(cwd, {
const handoff: Handoff = HandoffSchema.parse({
id: getNextId(cwd, 'handoff'),
fromRole, fromRole,
toRole, toRole,
fromAgent: options.fromAgent, fromAgent: options.fromAgent,
@ -25,46 +19,25 @@ export async function handoffCreate(cwd: string, options: Partial<Handoff> = {})
taskId: taskId || undefined, taskId: taskId || undefined,
summary, summary,
context, 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}`); console.log(`Handoff created: ${handoff.id}`);
} }
export function handoffRead(cwd: string, id: string): void { export function handoffRead(cwd: string, id: string): void {
const filePath = join(getEntityDir(cwd, 'handoffs'), `${id}.md`); const { handoff, body } = getHandoff(cwd, id);
const { frontmatter, body } = readEntity(filePath); console.log(`# ${handoff.summary}`);
console.log(`# ${frontmatter.summary}`); console.log(`From: ${handoff.fromRole}${handoff.toRole}`);
console.log(`From: ${frontmatter.fromRole}${frontmatter.toRole}`); if (handoff.taskId) console.log(`Task: ${handoff.taskId}`);
if (frontmatter.taskId) console.log(`Task: ${frontmatter.taskId}`);
console.log('\n' + body); console.log('\n' + body);
} }
export function handoffList(cwd: string): void { export function handoffList(cwd: string): void {
const index = new Index(cwd); const handoffs = listHandoffs(cwd);
const handoffs = index.list('handoff');
index.close();
if (handoffs.length === 0) { if (handoffs.length === 0) {
console.log('No handoffs found.'); console.log('No handoffs found.');
return; return;
} }
for (const h of handoffs) { for (const h of handoffs) {
console.log(`${h.id}: ${h.title}`); console.log(`${h.id}: ${h.title}`);
} }

View File

@ -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> = {}): 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<Index['list']> {
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 };
}

View File

@ -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);
});
});