feat(status): add status aggregation and latest.md generation
This commit is contained in:
parent
2b9e409b8b
commit
421420040a
58
src/core/status.ts
Normal file
58
src/core/status.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { getEntityDir, getStatusPath } from './paths.js';
|
||||
import { readEntity, writeEntity } from './files.js';
|
||||
import { StatusSchema } from './schema.js';
|
||||
|
||||
export function generateStatus(cwd: string) {
|
||||
const taskDir = getEntityDir(cwd, 'tasks');
|
||||
const decisionDir = getEntityDir(cwd, 'decisions');
|
||||
const handoffDir = getEntityDir(cwd, 'handoffs');
|
||||
|
||||
const activeTasks: string[] = [];
|
||||
const blockedTasks: string[] = [];
|
||||
|
||||
if (existsSync(taskDir)) {
|
||||
for (const file of listMdFiles(taskDir)) {
|
||||
const { frontmatter } = readEntity(file);
|
||||
if (frontmatter.status === 'open' || frontmatter.status === 'in_progress') {
|
||||
activeTasks.push(String(frontmatter.id));
|
||||
} else if (frontmatter.status === 'review') {
|
||||
blockedTasks.push(String(frontmatter.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const recentDecisions = recentIds(decisionDir, 5);
|
||||
const recentHandoffs = recentIds(handoffDir, 5);
|
||||
|
||||
const status = StatusSchema.parse({
|
||||
generatedAt: new Date().toISOString(),
|
||||
activeTasks,
|
||||
blockedTasks,
|
||||
recentDecisions,
|
||||
recentHandoffs,
|
||||
summary: `Active tasks: ${activeTasks.length}. Review/blocked: ${blockedTasks.length}.`,
|
||||
});
|
||||
|
||||
const statusPath = getStatusPath(cwd);
|
||||
mkdirSync(join(cwd, '.agenthub', 'status'), { recursive: true });
|
||||
writeEntity(statusPath, status, '# Project Status\n\n' + status.summary);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
function listMdFiles(dir: string): string[] {
|
||||
if (!existsSync(dir)) return [];
|
||||
return readdirSync(dir)
|
||||
.filter((f) => f.endsWith('.md'))
|
||||
.map((f) => join(dir, f));
|
||||
}
|
||||
|
||||
function recentIds(dir: string, limit: number): string[] {
|
||||
return listMdFiles(dir)
|
||||
.map((f) => ({ file: f, ...readEntity(f) }))
|
||||
.sort((a, b) => String(b.frontmatter.createdAt).localeCompare(String(a.frontmatter.createdAt)))
|
||||
.slice(0, limit)
|
||||
.map((entry) => String(entry.frontmatter.id));
|
||||
}
|
||||
28
tests/status.test.ts
Normal file
28
tests/status.test.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtempSync, rmSync, existsSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { generateStatus } from '../src/core/status.js';
|
||||
import { writeEntity } from '../src/core/files.js';
|
||||
import { getEntityDir } from '../src/core/paths.js';
|
||||
|
||||
describe('status', () => {
|
||||
let cwd: string;
|
||||
|
||||
beforeEach(() => {
|
||||
cwd = mkdtempSync(join(tmpdir(), 'agenthub-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('generates a status file', () => {
|
||||
const taskDir = getEntityDir(cwd, 'tasks');
|
||||
writeEntity(join(taskDir, 'TSK-0001.md'), { id: 'TSK-0001', title: 'Open task', status: 'open', createdAt: '2026-06-24T10:00:00Z', updatedAt: '2026-06-24T10:00:00Z' }, 'desc');
|
||||
|
||||
const status = generateStatus(cwd);
|
||||
expect(status.activeTasks).toContain('TSK-0001');
|
||||
expect(existsSync(join(cwd, '.agenthub', 'status', 'latest.md'))).toBe(true);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user