feat(services): add handoff service and refactor CLI
This commit is contained in:
parent
b4874afbe9
commit
6c8ee37a08
@ -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<Handoff> = {})
|
||||
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<Handoff> = {})
|
||||
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}`);
|
||||
}
|
||||
|
||||
54
src/core/services/handoffService.ts
Normal file
54
src/core/services/handoffService.ts
Normal 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 };
|
||||
}
|
||||
19
tests/handoffService.test.ts
Normal file
19
tests/handoffService.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user