From 421420040a082e48858da6550c1fd87ff967a27f Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Wed, 24 Jun 2026 23:36:58 +0200 Subject: [PATCH] feat(status): add status aggregation and latest.md generation --- src/core/status.ts | 58 ++++++++++++++++++++++++++++++++++++++++++++ tests/status.test.ts | 28 +++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 src/core/status.ts create mode 100644 tests/status.test.ts diff --git a/src/core/status.ts b/src/core/status.ts new file mode 100644 index 0000000..3d99393 --- /dev/null +++ b/src/core/status.ts @@ -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)); +} diff --git a/tests/status.test.ts b/tests/status.test.ts new file mode 100644 index 0000000..320ded2 --- /dev/null +++ b/tests/status.test.ts @@ -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); + }); +});