feat(status): add status aggregation and latest.md generation

This commit is contained in:
chahinebrini 2026-06-24 23:36:58 +02:00
parent 2b9e409b8b
commit 421420040a
2 changed files with 86 additions and 0 deletions

58
src/core/status.ts Normal file
View 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
View 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);
});
});