feat(wave3): @mosaic/coord TypeScript orchestrator (#6)

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #6.
This commit is contained in:
2026-03-07 01:32:10 +00:00
committed by jason.woltje
parent 2fca61fe04
commit 7f7109fc09
12 changed files with 2047 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
import { promises as fs } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { createMission, loadMission, missionFilePath } from '../src/mission.js';
describe('mission lifecycle', () => {
it('creates and loads mission state files', async () => {
const projectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'coord-mission-'));
try {
const mission = await createMission({
name: 'Wave 3 Mission',
projectPath: projectDir,
milestones: ['Phase One', 'Phase Two'],
qualityGates: 'pnpm lint && pnpm typecheck && pnpm test',
description: 'Wave 3 implementation',
});
expect(mission.id).toMatch(/^wave-3-mission-\d{8}$/);
expect(mission.status).toBe('active');
expect(mission.milestones).toHaveLength(2);
await expect(fs.stat(missionFilePath(projectDir, mission))).resolves.toBeDefined();
await expect(fs.stat(path.join(projectDir, 'docs/TASKS.md'))).resolves.toBeDefined();
await expect(
fs.stat(path.join(projectDir, '.mosaic/orchestrator/mission.json')),
).resolves.toBeDefined();
const loaded = await loadMission(projectDir);
expect(loaded.id).toBe(mission.id);
expect(loaded.name).toBe('Wave 3 Mission');
expect(loaded.qualityGates).toBe('pnpm lint && pnpm typecheck && pnpm test');
} finally {
await fs.rm(projectDir, { recursive: true, force: true });
}
});
it('rejects inactive missions on load', async () => {
const projectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'coord-mission-inactive-'));
try {
const mission = await createMission({
name: 'Inactive Mission',
projectPath: projectDir,
});
const missionPath = missionFilePath(projectDir, mission);
const payload = JSON.parse(await fs.readFile(missionPath, 'utf8')) as {
status: string;
};
payload.status = 'inactive';
await fs.writeFile(missionPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
await expect(loadMission(projectDir)).rejects.toThrow(
'Mission exists but is inactive',
);
} finally {
await fs.rm(projectDir, { recursive: true, force: true });
}
});
});

View File

@@ -0,0 +1,74 @@
import { describe, expect, it } from 'vitest';
import { parseTasksFile, writeTasksFile } from '../src/tasks-file.js';
import type { MissionTask } from '../src/types.js';
describe('parseTasksFile', () => {
it('normalizes legacy statuses from TASKS.md', () => {
const content = [
'# Tasks — Demo',
'',
'| id | status | milestone | description | pr | notes |',
'|----|--------|-----------|-------------|----|-------|',
'| T-1 | pending | phase-1 | First task | #10 | note a |',
'| T-2 | completed | phase-1 | Second task | #11 | note b |',
'| T-3 | in_progress | phase-2 | Third task | | |',
'| T-4 | failed | phase-2 | Fourth task | | |',
'',
'trailing text ignored',
].join('\n');
const tasks = parseTasksFile(content);
expect(tasks).toHaveLength(4);
expect(tasks.map((task) => task.status)).toEqual([
'not-started',
'done',
'in-progress',
'blocked',
]);
expect(tasks.map((task) => task.rawStatus)).toEqual([
'pending',
'completed',
'in_progress',
'failed',
]);
expect(tasks[0]?.line).toBe(5);
});
});
describe('writeTasksFile', () => {
it('round-trips parse/write output', () => {
const tasks: MissionTask[] = [
{
id: 'W3-001',
title: 'Implement parser',
status: 'not-started',
milestone: 'phase-1',
pr: '#20',
notes: 'pending',
dependencies: [],
},
{
id: 'W3-002',
title: 'Implement runner',
status: 'in-progress',
milestone: 'phase-2',
notes: 'active',
dependencies: [],
},
];
const content = writeTasksFile(tasks);
const reparsed = parseTasksFile(content);
expect(reparsed).toHaveLength(2);
expect(reparsed.map((task) => task.id)).toEqual(['W3-001', 'W3-002']);
expect(reparsed.map((task) => task.status)).toEqual([
'not-started',
'in-progress',
]);
expect(reparsed[0]?.title).toBe('Implement parser');
expect(reparsed[1]?.milestone).toBe('phase-2');
});
});