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 { 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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
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