agent-ecosystem/agent-teams-controller/src/internal/taskStore.js
777genius d018002c3e feat(docs): restructure VitePress IA, improve onboarding/troubleshooting docs
- Restructure sidebar: Start → Guide → Operations → Developers → Reference
- Fix EN/RU sidebar order (Installation before Quickstart)
- Expand troubleshooting with diagnostics commands and task-log triage
- Improve quickstart with prerequisites, pitfalls, and contributor links
- Expand installation docs with verification commands
- Add cyberpunk hero theme to landing page
- Add atomicFile utility with tests and stage-runtime script
- Harden team provisioning with better error handling and progress output
- Add cross-team communication, kanban, and workSync improvements
2026-05-15 23:34:06 +03:00

907 lines
29 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { writeJsonFileSync } = require('./atomicFile.js');
const reviewStateHelpers = require('./reviewState.js');
const TASK_STATUSES = new Set(['pending', 'in_progress', 'completed', 'deleted']);
const UUID_TASK_ID_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
function nowIso() {
return new Date().toISOString();
}
function ensureDir(dirPath) {
fs.mkdirSync(dirPath, { recursive: true });
}
function readJson(filePath, fallbackValue) {
try {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch {
return fallbackValue;
}
}
function writeJson(filePath, value) {
writeJsonFileSync(filePath, value);
}
function getTaskPath(paths, taskId) {
return path.join(paths.tasksDir, `${String(taskId)}.json`);
}
function looksLikeCanonicalTaskId(taskId) {
return UUID_TASK_ID_PATTERN.test(String(taskId || '').trim());
}
function deriveDisplayId(taskId) {
const normalized = String(taskId || '').trim();
if (!normalized) return normalized;
return looksLikeCanonicalTaskId(normalized) ? normalized.slice(0, 8).toLowerCase() : normalized;
}
function normalizeTask(rawTask, filePath) {
if (!rawTask || typeof rawTask !== 'object') {
throw new Error(`Invalid task payload${filePath ? `: ${filePath}` : ''}`);
}
const id =
typeof rawTask.id === 'string' || typeof rawTask.id === 'number' ? String(rawTask.id) : '';
if (!id) {
throw new Error(`Task is missing id${filePath ? `: ${filePath}` : ''}`);
}
const task = {
...rawTask,
id,
displayId:
typeof rawTask.displayId === 'string' && rawTask.displayId.trim()
? rawTask.displayId.trim()
: deriveDisplayId(id),
reviewState: normalizeTaskReviewState(rawTask.reviewState),
};
if (!TASK_STATUSES.has(String(task.status || '').trim())) {
throw new Error(
`Invalid task status "${String(task.status || '')}"${filePath ? `: ${filePath}` : ''}`
);
}
task.status = String(task.status).trim();
return task;
}
function normalizeTaskReviewState(value) {
return reviewStateHelpers.normalizeReviewState(value);
}
function listTaskRows(paths, options = {}) {
ensureDir(paths.tasksDir);
const entries = fs.readdirSync(paths.tasksDir);
const includeDeleted = options.includeDeleted === true;
const tasks = [];
const anomalies = [];
for (const fileName of entries) {
if (!fileName.endsWith('.json') || fileName.startsWith('.')) continue;
const filePath = path.join(paths.tasksDir, fileName);
let rawTask;
try {
rawTask = JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (error) {
anomalies.push({
code: 'unreadable_task',
taskId: path.basename(fileName, '.json'),
filePath,
detail: error instanceof Error ? error.message : 'Unreadable task row',
});
continue;
}
if (!rawTask) continue;
if (rawTask.metadata && rawTask.metadata._internal === true) continue;
try {
const task = normalizeTask(rawTask, filePath);
if (includeDeleted || task.status !== 'deleted') {
tasks.push(task);
}
} catch (error) {
const taskId =
typeof rawTask?.id === 'string' || typeof rawTask?.id === 'number'
? String(rawTask.id)
: path.basename(fileName, '.json');
anomalies.push({
code: 'unreadable_task',
taskId,
filePath,
detail: error instanceof Error ? error.message : 'Unreadable task row',
});
}
}
tasks.sort((a, b) => {
const byDisplay = String(a.displayId || a.id).localeCompare(
String(b.displayId || b.id),
undefined,
{
numeric: true,
sensitivity: 'base',
}
);
if (byDisplay !== 0) return byDisplay;
return String(a.id).localeCompare(String(b.id), undefined, {
numeric: true,
sensitivity: 'base',
});
});
return { tasks, anomalies };
}
function listRawTasks(paths) {
return listTaskRows(paths, { includeDeleted: true }).tasks;
}
function listTasks(paths, options = {}) {
return listTaskRows(paths, options).tasks;
}
function resolveTaskRef(paths, taskRef, options = {}) {
const normalizedRef = String(taskRef || '')
.trim()
.replace(/^#/, '');
if (!normalizedRef) {
throw new Error('Missing taskId');
}
const includeDeleted = options.includeDeleted === true;
// Fast path: if taskRef looks like a canonical UUID, try direct file read first
if (looksLikeCanonicalTaskId(normalizedRef)) {
const taskPath = getTaskPath(paths, normalizedRef);
const rawTask = readJson(taskPath, null);
if (rawTask && (includeDeleted || rawTask.status !== 'deleted')) {
return normalizedRef;
}
}
// Fallback: scan all tasks for displayId match or non-UUID refs
const tasks = listRawTasks(paths);
const exact = tasks.find((task) => task.id === normalizedRef);
if (exact && (includeDeleted || exact.status !== 'deleted')) {
return exact.id;
}
const byDisplay = tasks.find(
(task) => task.displayId === normalizedRef && (includeDeleted || task.status !== 'deleted')
);
if (byDisplay) {
return byDisplay.id;
}
throw new Error(`Task not found: ${normalizedRef}`);
}
function readTask(paths, taskRef, options = {}) {
const taskId = resolveTaskRef(paths, taskRef, options);
const taskPath = getTaskPath(paths, taskId);
const rawTask = readJson(taskPath, null);
if (!rawTask) {
throw new Error(`Task not found: ${String(taskRef)}`);
}
return normalizeTask(rawTask, taskPath);
}
function appendHistoryEvent(events, event) {
const list = Array.isArray(events) ? [...events] : [];
list.push({ id: crypto.randomUUID(), timestamp: nowIso(), ...event });
return list;
}
function isOpenReviewInterval(interval) {
return interval && interval.completedAt === undefined;
}
function closeOpenReviewIntervals(task, timestamp) {
if (!Array.isArray(task.reviewIntervals)) return false;
let changed = false;
task.reviewIntervals = task.reviewIntervals.map((interval) => {
if (!isOpenReviewInterval(interval)) return interval;
changed = true;
const startedAtMs = Date.parse(interval.startedAt);
const timestampMs = Date.parse(timestamp);
const completedAt =
Number.isFinite(startedAtMs) && Number.isFinite(timestampMs) && timestampMs < startedAtMs
? interval.startedAt
: timestamp;
return { ...interval, completedAt };
});
return changed;
}
function normalizeStatus(status) {
const normalized = String(status || '').trim();
return TASK_STATUSES.has(normalized) ? normalized : null;
}
function parseRelationshipList(paths, value) {
const rawValues = Array.isArray(value)
? value
: typeof value === 'string'
? value
.split(',')
.map((entry) => entry.trim())
.filter(Boolean)
: [];
return rawValues.map((entry) => resolveTaskRef(paths, entry));
}
function normalizeTaskRefs(taskRefs) {
if (!Array.isArray(taskRefs) || taskRefs.length === 0) {
return undefined;
}
const normalized = taskRefs
.filter((item) => item && typeof item === 'object')
.map((item) => ({
taskId: String(item.taskId || '').trim(),
displayId: String(item.displayId || '').trim(),
teamName: String(item.teamName || '').trim(),
}))
.filter((item) => item.taskId && item.displayId && item.teamName);
return normalized.length > 0 ? normalized : undefined;
}
function computeInitialStatus(paths, input, owner, blockedByIds) {
const explicit = normalizeStatus(input.status);
if (explicit) return explicit;
if (blockedByIds.length > 0) return 'pending';
if (owner && input.startImmediately === true) return 'in_progress';
return 'pending';
}
function pickTaskId(input) {
if (typeof input.id === 'string' && input.id.trim()) {
return input.id.trim();
}
return crypto.randomUUID();
}
function pickUniqueDisplayId(paths, canonicalId, explicitDisplayId) {
const preferred =
typeof explicitDisplayId === 'string' && explicitDisplayId.trim()
? explicitDisplayId.trim()
: deriveDisplayId(canonicalId);
const existing = new Set(
listRawTasks(paths).map((task) => task.displayId || deriveDisplayId(task.id))
);
if (!existing.has(preferred)) {
return preferred;
}
let length = Math.max(preferred.length, 8);
while (length < canonicalId.length) {
const candidate = canonicalId.slice(0, length).toLowerCase();
if (!existing.has(candidate)) {
return candidate;
}
length += 1;
}
return canonicalId.toLowerCase();
}
function wouldCreateBlockCycle(paths, sourceId, targetId) {
const visited = new Set();
const stack = [targetId];
while (stack.length > 0) {
const currentId = stack.pop();
if (!currentId || visited.has(currentId)) continue;
if (currentId === sourceId) return true;
visited.add(currentId);
try {
const currentTask = readTask(paths, currentId, { includeDeleted: true });
for (const depId of currentTask.blockedBy || []) {
stack.push(depId);
}
} catch {
// Ignore unreadable dependency rows during cycle probe.
}
}
return false;
}
function writeTask(paths, task) {
writeJson(getTaskPath(paths, task.id), task);
}
function createTask(paths, input = {}) {
ensureDir(paths.tasksDir);
const canonicalId = pickTaskId(input);
if (fs.existsSync(getTaskPath(paths, canonicalId))) {
throw new Error(`Task already exists: ${canonicalId}`);
}
const blockedByIds = parseRelationshipList(paths, input['blocked-by'] ?? input.blockedBy);
const relatedIds = parseRelationshipList(paths, input.related);
const owner =
typeof input.owner === 'string' && input.owner.trim() ? input.owner.trim() : undefined;
const createdBy =
typeof input.from === 'string' && input.from.trim()
? input.from.trim()
: typeof input.createdBy === 'string' && input.createdBy.trim()
? input.createdBy.trim()
: undefined;
const createdAt =
typeof input.createdAt === 'string' && input.createdAt.trim()
? input.createdAt.trim()
: nowIso();
const status = computeInitialStatus(paths, input, owner, blockedByIds);
const displayId = pickUniqueDisplayId(paths, canonicalId, input.displayId);
for (const depId of blockedByIds) {
if (wouldCreateBlockCycle(paths, canonicalId, depId)) {
throw new Error(`Circular dependency: ${depId} already depends on ${canonicalId}`);
}
}
const task = normalizeTask({
id: canonicalId,
displayId,
subject:
typeof input.subject === 'string' && input.subject.trim()
? input.subject.trim()
: String(input.subject || '').trim(),
description:
typeof input.description === 'string' && input.description.length > 0
? input.description
: String(input.subject || '').trim(),
descriptionTaskRefs: normalizeTaskRefs(input.descriptionTaskRefs),
activeForm:
typeof input.activeForm === 'string'
? input.activeForm
: typeof input['active-form'] === 'string'
? input['active-form']
: undefined,
owner,
createdBy,
status,
createdAt,
updatedAt: createdAt,
workIntervals:
status === 'in_progress'
? [{ startedAt: createdAt }]
: Array.isArray(input.workIntervals)
? input.workIntervals
: undefined,
historyEvents: appendHistoryEvent(undefined, {
type: 'task_created',
status,
...(createdBy ? { actor: createdBy } : {}),
timestamp: createdAt,
}),
blocks: Array.isArray(input.blocks) ? [...input.blocks] : [],
blockedBy: blockedByIds,
related: relatedIds.length > 0 ? relatedIds : undefined,
projectPath:
typeof input.projectPath === 'string' && input.projectPath.trim()
? input.projectPath.trim()
: undefined,
comments: Array.isArray(input.comments) ? input.comments : undefined,
prompt:
typeof input.prompt === 'string' && input.prompt.trim() ? input.prompt.trim() : undefined,
promptTaskRefs: normalizeTaskRefs(input.promptTaskRefs),
needsClarification:
input.needsClarification === 'lead' || input.needsClarification === 'user'
? input.needsClarification
: undefined,
reviewState: normalizeTaskReviewState(input.reviewState),
deletedAt:
status === 'deleted' && typeof input.deletedAt === 'string' ? input.deletedAt : undefined,
attachments: Array.isArray(input.attachments) ? input.attachments : undefined,
...(typeof input.sourceMessageId === 'string' && input.sourceMessageId.trim()
? { sourceMessageId: input.sourceMessageId.trim() }
: {}),
...(input.sourceMessage && typeof input.sourceMessage === 'object'
? { sourceMessage: input.sourceMessage }
: {}),
});
if (!task.subject) {
throw new Error('Missing subject');
}
writeTask(paths, task);
for (const depId of blockedByIds) {
const dependencyTask = readTask(paths, depId, { includeDeleted: true });
const dependencyBlocks = Array.isArray(dependencyTask.blocks) ? dependencyTask.blocks : [];
if (!dependencyBlocks.includes(task.id)) {
dependencyTask.blocks = dependencyBlocks.concat([task.id]);
dependencyTask.updatedAt = nowIso();
writeTask(paths, dependencyTask);
}
}
for (const relatedId of relatedIds) {
const relatedTask = readTask(paths, relatedId, { includeDeleted: true });
const existingRelated = Array.isArray(relatedTask.related) ? relatedTask.related : [];
if (!existingRelated.includes(task.id)) {
relatedTask.related = existingRelated.concat([task.id]);
relatedTask.updatedAt = nowIso();
writeTask(paths, relatedTask);
}
}
return task;
}
function updateTask(paths, taskRef, updater, options = {}) {
const existingTask = readTask(paths, taskRef, { includeDeleted: true });
const nextTask = normalizeTask(updater({ ...existingTask }) || existingTask);
nextTask.updatedAt = nowIso();
writeTask(paths, nextTask);
return nextTask;
}
function setTaskStatus(paths, taskRef, nextStatus, actor) {
const status = normalizeStatus(nextStatus);
if (!status) {
throw new Error(`Invalid status: ${String(nextStatus)}`);
}
return updateTask(paths, taskRef, (task) => {
if (task.status === status) {
if (status === 'deleted' || status === 'in_progress') {
task.reviewState = 'none';
} else if (
status === 'pending' &&
normalizeTaskReviewState(task.reviewState) !== 'needsFix'
) {
task.reviewState = 'none';
}
return task;
}
const timestamp = nowIso();
const workIntervals = Array.isArray(task.workIntervals) ? [...task.workIntervals] : [];
const lastInterval = workIntervals.length > 0 ? workIntervals[workIntervals.length - 1] : null;
if (task.status !== 'in_progress' && status === 'in_progress') {
if (!lastInterval || lastInterval.completedAt !== undefined) {
workIntervals.push({ startedAt: timestamp });
}
} else if (task.status === 'in_progress' && status !== 'in_progress') {
if (lastInterval && lastInterval.completedAt === undefined) {
lastInterval.completedAt = timestamp;
}
}
if (status === 'pending' || status === 'in_progress' || status === 'deleted') {
closeOpenReviewIntervals(task, timestamp);
}
task.workIntervals = workIntervals.length > 0 ? workIntervals : undefined;
task.historyEvents = appendHistoryEvent(task.historyEvents, {
type: 'status_changed',
from: task.status,
to: status,
...(actor ? { actor } : {}),
timestamp,
});
task.status = status;
if (status === 'deleted') {
task.deletedAt = timestamp;
task.reviewState = 'none';
} else if (task.deletedAt) {
delete task.deletedAt;
}
if (status === 'in_progress') {
task.reviewState = 'none';
} else if (status === 'pending' && normalizeTaskReviewState(task.reviewState) !== 'needsFix') {
task.reviewState = 'none';
}
return task;
});
}
function normalizeOwnerValue(owner) {
if (owner == null || owner === 'clear' || owner === 'none') {
return undefined;
}
const normalized = String(owner).trim();
return normalized ? normalized : undefined;
}
function setTaskOwner(paths, taskRef, owner, actor) {
return updateTask(paths, taskRef, (task) => {
const previousOwner = normalizeOwnerValue(task.owner);
const nextOwner = normalizeOwnerValue(owner);
if (nextOwner) {
task.owner = nextOwner;
} else {
delete task.owner;
}
if (previousOwner !== nextOwner) {
task.historyEvents = appendHistoryEvent(task.historyEvents, {
type: 'owner_changed',
...(previousOwner ? { from: previousOwner } : {}),
...(nextOwner ? { to: nextOwner } : {}),
...(actor ? { actor } : {}),
});
}
return task;
});
}
function updateTaskFields(paths, taskRef, fields) {
return updateTask(paths, taskRef, (task) => {
if (fields.subject !== undefined) {
task.subject = fields.subject;
}
if (fields.description !== undefined) {
task.description = fields.description;
}
return task;
});
}
function normalizeMemberName(value) {
return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : '';
}
function addTaskComment(paths, taskRef, text, options = {}) {
if (typeof text !== 'string' || !text.trim()) {
throw new Error('Missing comment text');
}
const comment = {
id: options.id || crypto.randomUUID(),
author:
typeof options.author === 'string' && options.author.trim() ? options.author.trim() : 'user',
text,
createdAt:
typeof options.createdAt === 'string' && options.createdAt.trim()
? options.createdAt.trim()
: nowIso(),
type: options.type || 'regular',
...(normalizeTaskRefs(options.taskRefs)
? { taskRefs: normalizeTaskRefs(options.taskRefs) }
: {}),
...(Array.isArray(options.attachments) && options.attachments.length > 0
? { attachments: options.attachments }
: {}),
};
let inserted = false;
const task = updateTask(paths, taskRef, (currentTask) => {
const comments = Array.isArray(currentTask.comments) ? currentTask.comments : [];
if (comments.some((entry) => entry.id === comment.id)) {
return currentTask;
}
currentTask.comments = comments.concat([comment]);
inserted = true;
return currentTask;
});
return { comment, task, inserted, clarificationCleared: false };
}
function setNeedsClarification(paths, taskRef, value) {
return updateTask(paths, taskRef, (task) => {
if (value === null || value === 'clear') {
delete task.needsClarification;
} else if (value === 'lead' || value === 'user') {
task.needsClarification = value;
} else {
throw new Error(`Invalid clarification value: ${String(value)}`);
}
return task;
});
}
function addTaskAttachmentMeta(paths, taskRef, meta) {
return updateTask(paths, taskRef, (task) => {
const attachments = Array.isArray(task.attachments) ? task.attachments : [];
if (!attachments.some((entry) => entry.id === meta.id)) {
task.attachments = attachments.concat([meta]);
}
return task;
});
}
function removeTaskAttachment(paths, taskRef, attachmentId) {
return updateTask(paths, taskRef, (task) => {
const attachments = Array.isArray(task.attachments) ? task.attachments : [];
const filtered = attachments.filter((entry) => entry.id !== attachmentId);
if (filtered.length > 0) task.attachments = filtered;
else delete task.attachments;
return task;
});
}
function addCommentAttachmentMeta(paths, taskRef, commentRef, meta) {
return updateTask(paths, taskRef, (task) => {
const comments = Array.isArray(task.comments) ? [...task.comments] : [];
const commentIndex = comments.findIndex((entry) => String(entry.id) === String(commentRef));
if (commentIndex < 0) {
throw new Error(`Comment not found: ${String(commentRef)}`);
}
const comment = { ...comments[commentIndex] };
const attachments = Array.isArray(comment.attachments) ? comment.attachments : [];
if (!attachments.some((entry) => entry.id === meta.id)) {
comment.attachments = attachments.concat([meta]);
}
comments[commentIndex] = comment;
task.comments = comments;
return task;
});
}
function linkTask(paths, taskRef, targetRef, relationship) {
const sourceId = resolveTaskRef(paths, taskRef);
const targetId = resolveTaskRef(paths, targetRef);
if (sourceId === targetId) {
throw new Error('Cannot link a task to itself');
}
if (relationship === 'blocks') {
return linkTask(paths, targetId, sourceId, 'blocked-by');
}
if (relationship === 'blocked-by') {
if (wouldCreateBlockCycle(paths, sourceId, targetId)) {
throw new Error(`Circular dependency: ${targetId} already depends on ${sourceId}`);
}
const sourceTask = readTask(paths, sourceId, { includeDeleted: true });
const targetTask = readTask(paths, targetId, { includeDeleted: true });
if (!(sourceTask.blockedBy || []).includes(targetId)) {
sourceTask.blockedBy = [...(sourceTask.blockedBy || []), targetId];
writeTask(paths, sourceTask);
}
if (!(targetTask.blocks || []).includes(sourceId)) {
targetTask.blocks = [...(targetTask.blocks || []), sourceId];
writeTask(paths, targetTask);
}
return readTask(paths, sourceId, { includeDeleted: true });
}
if (relationship !== 'related') {
throw new Error(`Unsupported relationship: ${String(relationship)}`);
}
const sourceTask = readTask(paths, sourceId, { includeDeleted: true });
const targetTask = readTask(paths, targetId, { includeDeleted: true });
if (!(sourceTask.related || []).includes(targetId)) {
sourceTask.related = [...(sourceTask.related || []), targetId];
writeTask(paths, sourceTask);
}
if (!(targetTask.related || []).includes(sourceId)) {
targetTask.related = [...(targetTask.related || []), sourceId];
writeTask(paths, targetTask);
}
return readTask(paths, sourceId, { includeDeleted: true });
}
function unlinkTask(paths, taskRef, targetRef, relationship) {
const sourceId = resolveTaskRef(paths, taskRef, { includeDeleted: true });
const targetId = resolveTaskRef(paths, targetRef, { includeDeleted: true });
if (relationship === 'blocks') {
return unlinkTask(paths, targetId, sourceId, 'blocked-by');
}
const sourceTask = readTask(paths, sourceId, { includeDeleted: true });
if (relationship === 'blocked-by') {
sourceTask.blockedBy = (sourceTask.blockedBy || []).filter((entry) => entry !== targetId);
writeTask(paths, sourceTask);
try {
const targetTask = readTask(paths, targetId, { includeDeleted: true });
targetTask.blocks = (targetTask.blocks || []).filter((entry) => entry !== sourceId);
writeTask(paths, targetTask);
} catch {
// Ignore missing reverse link target.
}
return readTask(paths, sourceId, { includeDeleted: true });
}
if (relationship !== 'related') {
throw new Error(`Unsupported relationship: ${String(relationship)}`);
}
sourceTask.related = (sourceTask.related || []).filter((entry) => entry !== targetId);
writeTask(paths, sourceTask);
try {
const targetTask = readTask(paths, targetId, { includeDeleted: true });
targetTask.related = (targetTask.related || []).filter((entry) => entry !== sourceId);
writeTask(paths, targetTask);
} catch {
// Ignore missing reverse link target.
}
return readTask(paths, sourceId, { includeDeleted: true });
}
function buildTaskReference(task) {
return `#${task.displayId || deriveDisplayId(task.id)} (taskId: ${task.id})`;
}
function getTaskFreshness(task) {
const updated = Date.parse(String(task.updatedAt || ''));
if (Number.isFinite(updated) && updated > 0) return updated;
const created = Date.parse(String(task.createdAt || ''));
if (Number.isFinite(created) && created > 0) return created;
return 0;
}
function compareTasksByFreshness(a, b) {
const freshnessDiff = getTaskFreshness(b) - getTaskFreshness(a);
if (freshnessDiff !== 0) return freshnessDiff;
const byDisplay = String(a.displayId || a.id).localeCompare(
String(b.displayId || b.id),
undefined,
{
numeric: true,
sensitivity: 'base',
}
);
if (byDisplay !== 0) return byDisplay;
return String(a.id).localeCompare(String(b.id), undefined, {
numeric: true,
sensitivity: 'base',
});
}
function getEffectiveReviewState(kanbanEntry, task) {
return reviewStateHelpers.getEffectiveReviewState(task, kanbanEntry).state;
}
function formatBriefTaskLine(task, reviewState) {
const reviewSuffix = reviewState !== 'none' ? `, review=${reviewState}` : '';
return `- #${task.displayId || deriveDisplayId(task.id)} [status=${task.status}${reviewSuffix}] ${task.subject}`;
}
function formatCommentLine(comment) {
const author =
typeof comment.author === 'string' && comment.author.trim() ? comment.author.trim() : 'unknown';
const text = typeof comment.text === 'string' ? comment.text.trim() : '';
return ` - ${author}: ${text || '(empty comment)'}`;
}
function formatTaskBriefing(paths, teamName, memberName) {
const kanbanState = readJson(path.join(paths.teamDir, 'kanban-state.json'), {
teamName,
reviewers: [],
tasks: {},
});
const activeTasks = listTasks(paths)
.filter((task) => task.owner === memberName && task.status !== 'deleted')
.sort(compareTasksByFreshness);
if (activeTasks.length === 0) {
return `No assigned tasks for ${memberName}.`;
}
const groups = {
in_progress: activeTasks.filter((task) => task.status === 'in_progress'),
needs_fix: activeTasks.filter((task) => {
const kanbanEntry = kanbanState.tasks ? kanbanState.tasks[task.id] : undefined;
return (
task.status !== 'in_progress' && getEffectiveReviewState(kanbanEntry, task) === 'needsFix'
);
}),
pending: activeTasks.filter((task) => {
const kanbanEntry = kanbanState.tasks ? kanbanState.tasks[task.id] : undefined;
return task.status === 'pending' && getEffectiveReviewState(kanbanEntry, task) === 'none';
}),
review: activeTasks.filter((task) => {
const kanbanEntry = kanbanState.tasks ? kanbanState.tasks[task.id] : undefined;
return getEffectiveReviewState(kanbanEntry, task) === 'review';
}),
completed: activeTasks.filter((task) => {
const kanbanEntry = kanbanState.tasks ? kanbanState.tasks[task.id] : undefined;
return task.status === 'completed' && getEffectiveReviewState(kanbanEntry, task) === 'none';
}),
approved: activeTasks.filter((task) => {
const kanbanEntry = kanbanState.tasks ? kanbanState.tasks[task.id] : undefined;
return getEffectiveReviewState(kanbanEntry, task) === 'approved';
}),
};
const lines = [`Task briefing for ${memberName}:`];
if (groups.in_progress.length > 0) {
lines.push('', 'In progress:');
for (const task of groups.in_progress) {
const kanbanEntry = kanbanState.tasks ? kanbanState.tasks[task.id] : undefined;
const reviewState = getEffectiveReviewState(kanbanEntry, task);
lines.push(formatBriefTaskLine(task, reviewState));
if (task.description) {
lines.push(` Description: ${task.description}`);
}
if (Array.isArray(task.comments) && task.comments.length > 0) {
lines.push(' Comments:');
for (const comment of task.comments) {
lines.push(formatCommentLine(comment));
}
}
}
}
if (groups.needs_fix.length > 0) {
lines.push('', 'Needs fixes after review:');
for (const task of groups.needs_fix) {
const kanbanEntry = kanbanState.tasks ? kanbanState.tasks[task.id] : undefined;
lines.push(formatBriefTaskLine(task, getEffectiveReviewState(kanbanEntry, task)));
}
}
if (groups.pending.length > 0) {
lines.push('', 'Pending:');
for (const task of groups.pending) {
const kanbanEntry = kanbanState.tasks ? kanbanState.tasks[task.id] : undefined;
lines.push(formatBriefTaskLine(task, getEffectiveReviewState(kanbanEntry, task)));
}
}
if (groups.review.length > 0) {
lines.push('', 'Review:');
for (const task of groups.review) {
const kanbanEntry = kanbanState.tasks ? kanbanState.tasks[task.id] : undefined;
lines.push(formatBriefTaskLine(task, getEffectiveReviewState(kanbanEntry, task)));
}
}
if (groups.completed.length > 0) {
lines.push('', 'Completed:');
for (const task of groups.completed) {
const kanbanEntry = kanbanState.tasks ? kanbanState.tasks[task.id] : undefined;
lines.push(formatBriefTaskLine(task, getEffectiveReviewState(kanbanEntry, task)));
}
}
if (groups.approved.length > 0) {
lines.push('', 'Approved (last 10):');
for (const task of groups.approved.slice(0, 10)) {
const kanbanEntry = kanbanState.tasks ? kanbanState.tasks[task.id] : undefined;
lines.push(formatBriefTaskLine(task, getEffectiveReviewState(kanbanEntry, task)));
}
}
return lines.join('\n');
}
module.exports = {
addCommentAttachmentMeta,
addTaskAttachmentMeta,
addTaskComment,
appendHistoryEvent,
buildTaskReference,
createTask,
deriveDisplayId,
formatTaskBriefing,
linkTask,
listTaskRows,
listTasks,
readTask,
removeTaskAttachment,
resolveTaskRef,
setNeedsClarification,
setTaskOwner,
setTaskStatus,
unlinkTask,
updateTask,
updateTaskFields,
};