UNPKG

rigjs

Version:

A multi-repos dev tool based on yarn and git.Rigjs is intended to be the simplest way to develop,share and deliver codes between different developers or different projects.

129 lines (116 loc) 5.62 kB
import path from 'path'; import print from '../print'; import { requireCrew, shortPath } from './config'; import { scanTasks, openPendingQuestions, summarize, taskProgress, CrewTask } from './task'; import { rootPath, writeText, readText } from './vault'; import { writeCrewState } from './state'; import { roleDefinitionsForCrew } from './role'; interface BoardOpts { crew?: string; } export default function crewBoard(opts: BoardOpts): void { const crew = requireCrew(opts.crew); const tasks = scanTasks(crew); const pending = openPendingQuestions(crew); const summary = summarize(tasks); const dashboard = renderDashboard(crew, tasks, pending); const file = rootPath(crew, 'Dashboard.md'); writeText(file, dashboard); writeCrewState(crew, tasks); print.succeed(`crew dashboard refreshed: ${shortPath(file)}`); print.info(`tasks: ${summary.done}/${summary.total} done, pending questions: ${pending.length} open`); } function renderDashboard(crew: ReturnType<typeof requireCrew>, tasks: CrewTask[], pending: CrewTask[]): string { const summary = summarize(tasks); const health = summary.blocked > 0 ? 'At Risk' : 'On Track'; const goal = currentGoal(rootPath(crew, 'Current-Goal.md')); return [ '# Dashboard', '', `Last updated: ${new Date().toISOString()}`, '', '## Orchestrator Brief', '', `Current Goal: ${goal || '_No current goal yet_'}`, `Overall: ${taskProgress(tasks)}% (${summary.done}/${summary.total})`, `Health: ${health}`, `Next Agent Action: ${pending.length > 0 ? 'Read `rig orchestrate pending-questions` and surface only needed decisions to the human.' : 'Run `rig orchestrate` to continue the next Orchestrator tick.'}`, '', '## Needs Your Attention', '', pendingTable(pending), '', '## Project Progress', '', projectTable(crew, tasks), '', '## Role Progress', '', roleTable(crew, tasks), '', '## Blockers', '', blockersTable(tasks), '', '## Active Tasks', '', activeTable(tasks), '', ].join('\n'); } function currentGoal(file: string): string { const lines = readText(file) .split(/\r?\n/) .map(l => l.trim()) .filter(l => l && l.charAt(0) !== '#'); return lines.length ? lines[lines.length - 1].replace(/^\-\s*/, '') : ''; } function pendingTable(tasks: CrewTask[]): string { if (tasks.length === 0) return '_No open pending questions._'; const rows = tasks.map(t => `| ${t.id || '-'} | ${t.fields.type || '-'} | ${cleanTaskText(t.text)} | ${t.fields.priority || '-'} |`); return ['| ID | Type | Item | Priority |', '|---|---|---|---|'].concat(rows).join('\n'); } function projectTable(crew: ReturnType<typeof requireCrew>, tasks: CrewTask[]): string { const projects = crew.projects || []; if (projects.length === 0) return '_No projects registered yet._'; const rows = projects.map(p => { const scoped = tasks.filter(t => t.scope !== 'pending' && (t.scope === `project:${p.name}` || t.scope.startsWith(`project:${p.name}:`) || t.fields.project === p.name)); const s = summarize(scoped); const health = s.blocked > 0 ? 'At Risk' : 'On Track'; return `| ${p.name} | ${p.owner} | ${p.defaultExecutor || crew.defaultExecutor || 'claude'} | ${health} | ${s.open} | ${s.blocked} | ${shortPath(p.path)} |`; }); return ['| Project | Owner | Executor | Health | Open | Blocked | Path |', '|---|---|---|---|---:|---:|---|'].concat(rows).join('\n'); } function roleTable(crew: ReturnType<typeof requireCrew>, tasks: CrewTask[]): string { const roles = roleDefinitionsForCrew(crew); const rows = roles.map(role => { const scoped = tasks.filter(t => t.scope === `legacy-role:${role.name}` || t.scope.includes(`:role:${role.name}`) || t.fields.role === role.name || t.fields.owner === role.name); const s = summarize(scoped); return `| ${role.title} | ${taskProgress(scoped)}% | ${s.doing} | ${s.blocked} | ${s.open} |`; }); return ['| Role | Progress | WIP | Blocked | Open |', '|---|---:|---:|---:|---:|'].concat(rows).join('\n'); } function blockersTable(tasks: CrewTask[]): string { const blocked = tasks.filter(t => (t.fields.status || '').toLowerCase() === 'blocked'); if (blocked.length === 0) return '_No blockers._'; const rows = blocked.slice(0, 20).map(t => `| ${t.id || '-'} | ${t.fields.owner || t.scope} | ${cleanTaskText(t.text)} | ${relFile(t)} |`); return ['| ID | Owner | Blocker | File |', '|---|---|---|---|'].concat(rows).join('\n'); } function activeTable(tasks: CrewTask[]): string { const active = tasks.filter(t => !t.done && t.scope !== 'pending').slice(0, 20); if (active.length === 0) return '_No active tasks._'; const rows = active.map(t => `| ${t.id || '-'} | ${t.fields.project || '-'} | ${t.fields.owner || displayScope(t.scope)} | ${t.fields.status || 'pending'} | ${cleanTaskText(t.text)} |`); return ['| ID | Project | Owner | Status | Task |', '|---|---|---|---|---|'].concat(rows).join('\n'); } function displayScope(scope: string): string { if (scope.startsWith('legacy-role:')) return scope.slice('legacy-role:'.length); const projectRole = scope.match(/^project:([^:]+):role:([^:]+)/); if (projectRole) return `${projectRole[1]}/${projectRole[2]}`; const projectTasklist = scope.match(/^project:([^:]+):tasklist/); if (projectTasklist) return projectTasklist[1]; return scope; } function cleanTaskText(text: string): string { return text.replace(/\[[A-Za-z0-9_-]+::\s*[^\]]+\]/g, '').trim().replace(/\|/g, '\\|'); } function relFile(t: CrewTask): string { return path.basename(t.file) + ':' + t.line; }