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