agent-ecosystem/agent-teams-controller/src/internal/agenda.js

914 lines
31 KiB
JavaScript

const kanbanStore = require('./kanbanStore.js');
const taskStore = require('./taskStore.js');
const runtimeHelpers = require('./runtimeHelpers.js');
const reviewStateHelpers = require('./reviewState.js');
const { withTeamBoardLock } = require('./boardLock.js');
const INVENTORY_KANBAN_COLUMNS = new Set(['review', 'approved']);
const MAX_MEMBER_ACTIONABLE_ITEMS = 50;
const MAX_MEMBER_AWARENESS_ITEMS = 30;
const MAX_LEAD_SECTION_ITEMS = 50;
const MAX_EXPANDED_CONTEXT_ITEMS = 8;
const MAX_DESCRIPTION_CHARS = 1200;
const MAX_COMMENT_CHARS = 500;
const MAX_SUBJECT_CHARS = 240;
const MAX_ANOMALY_ITEMS = 25;
const MAX_ANOMALY_DETAIL_CHARS = 500;
function normalizeName(value) {
return typeof value === 'string' && value.trim() ? value.trim() : '';
}
function normalizeKey(value) {
return normalizeName(value).toLowerCase();
}
function formatTaskLabel(task) {
return `#${task.displayId || task.id}`;
}
function isLeadCandidate(member) {
return runtimeHelpers.isCanonicalLeadMember(member);
}
function buildQueueRoster(paths) {
const resolved = runtimeHelpers.resolveTeamMembers(paths);
const explicit = runtimeHelpers.collectExplicitTeamMembers(paths);
const membersByKey = new Map();
for (const member of resolved.members || []) {
const key = normalizeKey(member && member.name);
if (!key) continue;
membersByKey.set(key, member);
}
const leadCandidates = (resolved.members || []).filter(isLeadCandidate);
const uniqueLeadName = leadCandidates.length === 1 ? normalizeName(leadCandidates[0].name) : '';
const inferredLeadName = normalizeName(runtimeHelpers.inferLeadName(paths));
const canonicalLeadName =
uniqueLeadName ||
(membersByKey.get(normalizeKey(inferredLeadName)) &&
normalizeName(membersByKey.get(normalizeKey(inferredLeadName)).name)) ||
'';
const leadAliases = new Set(['team-lead']);
if (canonicalLeadName) {
leadAliases.add(normalizeKey(canonicalLeadName));
leadAliases.add('lead');
}
return {
membersByKey,
explicitMemberKeys: new Set(explicit.membersByKey.keys()),
removedNames: resolved.removedNames || new Set(),
leadAliases,
leadCandidates: leadCandidates.map((member) => normalizeName(member.name)).filter(Boolean),
canonicalLeadName,
leadHeaderName: uniqueLeadName || '',
};
}
function collectExplicitMemberKeys(paths) {
return new Set(runtimeHelpers.collectExplicitTeamMembers(paths).membersByKey.keys());
}
function isCurrentRuntimeMember(teamName, memberName) {
const requestedKey = normalizeKey(memberName);
if (!requestedKey) return false;
const runtimeIdentity = runtimeHelpers.getCurrentRuntimeMemberIdentity();
if (!runtimeIdentity) return false;
const runtimeAgentName = normalizeKey(runtimeIdentity.agentName);
const runtimeAgentId = normalizeKey(runtimeIdentity.agentId);
const runtimeTeamName = normalizeKey(runtimeIdentity.teamName);
const requestedAgentId = `${requestedKey}@${normalizeKey(teamName)}`;
return (
(runtimeAgentName === requestedKey || runtimeAgentId === requestedAgentId) &&
(!runtimeTeamName || runtimeTeamName === normalizeKey(teamName))
);
}
function validateBriefingMember(paths, teamName, memberName) {
const normalized = normalizeName(memberName);
const key = normalizeKey(normalized);
if (!key) {
throw new Error('Missing member name');
}
const roster = buildQueueRoster(paths);
if (roster.removedNames.has(key)) {
throw new Error(`Member is removed from the team: ${normalized}`);
}
const explicitMemberKeys = collectExplicitMemberKeys(paths);
if (explicitMemberKeys.has(key) || isCurrentRuntimeMember(teamName, normalized)) {
return { warnings: [] };
}
if (roster.membersByKey.has(key)) {
return {
warnings: [
`Member identity warning: ${normalized} is known only from inbox state, not team config/member metadata. Verify the member name before acting.`,
],
};
}
throw new Error(`Member not found in team metadata or inboxes: ${normalized}`);
}
function resolveQueueActor(value, roster) {
const normalized = normalizeName(value);
if (!normalized) return null;
const key = normalizeKey(normalized);
if (roster.removedNames.has(key)) {
return null;
}
if (roster.leadAliases.has(key) && roster.canonicalLeadName) {
return { kind: 'lead', memberName: roster.canonicalLeadName };
}
const member = roster.membersByKey.get(key);
if (!member) return null;
if (!roster.explicitMemberKeys || !roster.explicitMemberKeys.has(key)) return null;
if (roster.canonicalLeadName && normalizeKey(member.name) === normalizeKey(roster.canonicalLeadName)) {
return { kind: 'lead', memberName: roster.canonicalLeadName };
}
return { kind: 'member', memberName: normalizeName(member.name) };
}
function areSameActors(left, right) {
if (!left || !right || left.kind !== right.kind) return false;
if (left.kind === 'lead') return true;
return normalizeKey(left.memberName) === normalizeKey(right.memberName);
}
function resolveEffectiveReviewState(task, kanbanEntry) {
return reviewStateHelpers.getEffectiveReviewState(task, kanbanEntry);
}
function resolveLegacyKanbanReviewer(task, roster, options = {}) {
const reviewState = normalizeName(options.reviewState);
const kanbanEntry = options.kanbanEntry;
if (reviewState !== 'review' || !kanbanEntry || kanbanEntry.column !== 'review') {
return null;
}
const legacyReviewer = normalizeName(kanbanEntry.reviewer);
if (!legacyReviewer) {
return null;
}
const actor = resolveQueueActor(legacyReviewer, roster);
if (actor) {
return { actor, source: 'legacy_kanban_reviewer', invalidValue: null };
}
return {
actor: null,
source: 'legacy_kanban_reviewer_invalid',
invalidValue: legacyReviewer,
};
}
function resolveCurrentCycleReviewer(task, roster, options = {}) {
const events = Array.isArray(task.historyEvents) ? task.historyEvents : [];
for (let i = events.length - 1; i >= 0; i -= 1) {
const event = events[i];
if (event.type === 'review_started') {
const actor = resolveQueueActor(event.actor, roster);
if (actor) {
return { actor, source: 'history_review_started_actor', invalidValue: null };
}
return {
actor: null,
source: 'history_review_started_invalid',
invalidValue: normalizeName(event.actor) || null,
};
}
if (event.type === 'review_requested') {
const reviewer = resolveQueueActor(event.reviewer, roster);
if (reviewer) {
return { actor: reviewer, source: 'history_review_requested_reviewer', invalidValue: null };
}
return {
actor: null,
source: 'history_review_requested_invalid',
invalidValue: normalizeName(event.reviewer) || null,
};
}
if (event.type === 'review_approved' || event.type === 'review_changes_requested') {
break;
}
if (
event.type === 'status_changed' &&
(event.to === 'in_progress' || event.to === 'pending' || event.to === 'deleted')
) {
break;
}
if (event.type === 'task_created') {
break;
}
}
const legacyFallback = resolveLegacyKanbanReviewer(task, roster, options);
if (legacyFallback) {
return legacyFallback;
}
return { actor: null, source: 'none', invalidValue: null };
}
function compareTasksByFreshness(left, right) {
const leftTs = Date.parse(normalizeName(left.updatedAt) || normalizeName(left.createdAt) || '') || 0;
const rightTs = Date.parse(normalizeName(right.updatedAt) || normalizeName(right.createdAt) || '') || 0;
if (leftTs !== rightTs) return rightTs - leftTs;
const byDisplay = String(left.displayId || left.id).localeCompare(String(right.displayId || right.id), undefined, {
numeric: true,
sensitivity: 'base',
});
if (byDisplay !== 0) return byDisplay;
return String(left.id).localeCompare(String(right.id), undefined, {
numeric: true,
sensitivity: 'base',
});
}
function buildBoardState(paths, teamName) {
const taskRows = taskStore.listTaskRows(paths);
const kanbanState = kanbanStore.readKanbanState(paths, teamName);
const roster = buildQueueRoster(paths);
const tasksById = new Map(taskRows.tasks.map((task) => [task.id, task]));
const anomalies = [];
if (kanbanState.tasks && typeof kanbanState.tasks === 'object') {
for (const [taskId, entry] of Object.entries(kanbanState.tasks)) {
if (!tasksById.has(taskId)) {
anomalies.push({
code: 'stale_kanban_task',
taskId,
detail: `Kanban ${entry && entry.column ? entry.column : 'entry'} references a missing or deleted task row.`,
});
}
}
}
if (kanbanState.columnOrder && typeof kanbanState.columnOrder === 'object') {
for (const [columnId, orderedTaskIds] of Object.entries(kanbanState.columnOrder)) {
if (!Array.isArray(orderedTaskIds)) continue;
for (const taskId of orderedTaskIds) {
const id = String(taskId);
const entry = kanbanState.tasks ? kanbanState.tasks[id] : undefined;
if (!tasksById.has(id) || !entry || entry.column !== columnId) {
anomalies.push({
code: 'stale_kanban_order',
taskId: id,
detail: `Kanban columnOrder.${columnId} references a task that is not in that column overlay.`,
});
}
}
}
}
for (const anomaly of taskRows.anomalies) {
anomalies.push({
code: anomaly.code,
detail: anomaly.detail,
...(anomaly.taskId ? { taskId: anomaly.taskId } : {}),
});
}
return {
tasks: [...taskRows.tasks].sort(compareTasksByFreshness),
tasksById,
kanbanState,
roster,
anomalies,
};
}
function buildWatchers(ownerActor, reviewerActor, actionOwner) {
const watchers = new Set();
if (ownerActor && ownerActor.kind === 'member') {
watchers.add(ownerActor.memberName);
}
if (reviewerActor && reviewerActor.kind === 'member') {
watchers.add(reviewerActor.memberName);
}
if (actionOwner && actionOwner.kind === 'member') {
watchers.delete(actionOwner.memberName);
}
return [...watchers];
}
function getLastMeaningfulEventAt(task) {
const timestamps = [];
if (normalizeName(task.updatedAt)) timestamps.push(task.updatedAt);
if (normalizeName(task.createdAt)) timestamps.push(task.createdAt);
const comments = Array.isArray(task.comments) ? task.comments : [];
for (const comment of comments) {
if (normalizeName(comment && comment.createdAt)) {
timestamps.push(comment.createdAt);
}
}
timestamps.sort((left, right) => {
const leftTs = Date.parse(left) || 0;
const rightTs = Date.parse(right) || 0;
return rightTs - leftTs;
});
return timestamps[0] || undefined;
}
function truncateText(value, maxChars) {
const text = normalizeName(value);
if (!text || text.length <= maxChars) {
return text;
}
return `${text.slice(0, maxChars)}... [truncated]`;
}
function buildAgendaItem(task, boardState) {
const kanbanEntry = boardState.kanbanState.tasks ? boardState.kanbanState.tasks[task.id] : undefined;
const reviewStateResult = resolveEffectiveReviewState(task, kanbanEntry);
const reviewerResult = resolveCurrentCycleReviewer(task, boardState.roster, {
reviewState: reviewStateResult.state,
kanbanEntry,
});
const ownerActor = resolveQueueActor(task.owner, boardState.roster);
const hasOwnerField = normalizeName(task.owner).length > 0;
const hasMissingOwner = !hasOwnerField;
const hasInvalidOwner = hasOwnerField && !ownerActor;
const reviewActor = reviewerResult.actor;
const reviewActorIsInvalid = reviewStateResult.state === 'review' && !reviewActor;
const hasSelfReview = Boolean(ownerActor && reviewActor && areSameActors(ownerActor, reviewActor));
const brokenDependencyIds = [];
const waitingDependencyIds = [];
const blockedByIds = Array.isArray(task.blockedBy) ? task.blockedBy.map(String) : [];
for (const dependencyId of blockedByIds) {
const dependency = boardState.tasksById.get(dependencyId);
if (!dependency || dependency.status === 'deleted') {
brokenDependencyIds.push(dependencyId);
continue;
}
if (dependency.status !== 'completed') {
waitingDependencyIds.push(dependencyId);
}
}
let actionOwner = { kind: 'none' };
let nextAction = 'none';
let queueCategory = 'done';
let reasonCode = 'completed_no_followup';
const derivedFrom = [reviewStateResult.source, reviewerResult.source].filter(
(value) => value && value !== 'none'
);
if (task.status === 'deleted') {
reasonCode = 'terminal_deleted';
} else if (reviewStateResult.state === 'approved') {
reasonCode = 'terminal_approved';
} else if (task.needsClarification === 'user') {
actionOwner = { kind: 'user' };
nextAction = 'clarify_with_user';
queueCategory = 'waiting';
reasonCode = 'waiting_user_clarification';
derivedFrom.push('clarification_flag');
} else if (task.needsClarification === 'lead') {
actionOwner = { kind: 'lead' };
nextAction = 'clarify_with_lead';
queueCategory = 'oversight';
reasonCode = 'waiting_lead_clarification';
derivedFrom.push('clarification_flag');
} else if (hasMissingOwner) {
actionOwner = { kind: 'lead' };
nextAction = 'assign_owner';
queueCategory = 'oversight';
reasonCode = 'owner_missing';
derivedFrom.push('owner_status');
} else if (hasInvalidOwner) {
actionOwner = { kind: 'lead' };
nextAction = 'assign_owner';
queueCategory = 'oversight';
reasonCode = 'owner_invalid';
derivedFrom.push('owner_status');
} else if (reviewStateResult.state === 'review') {
if (hasSelfReview) {
actionOwner = { kind: 'lead' };
nextAction = 'assign_reviewer';
queueCategory = 'oversight';
reasonCode = 'self_review_invalid';
derivedFrom.push('self_review_invalid');
} else if (reviewActorIsInvalid) {
actionOwner = { kind: 'lead' };
nextAction = 'assign_reviewer';
queueCategory = 'oversight';
reasonCode = 'review_reviewer_missing';
derivedFrom.push('history_reviewer_invalid');
} else if (reviewActor) {
actionOwner = reviewActor.kind === 'lead'
? { kind: 'lead' }
: { kind: 'member', memberName: reviewActor.memberName };
nextAction = 'review';
queueCategory = actionOwner.kind === 'lead' ? 'oversight' : 'actionable';
reasonCode =
reviewerResult.source === 'history_review_started_actor'
? 'review_in_progress'
: 'review_requested_waiting_pickup';
}
} else if (brokenDependencyIds.length > 0) {
actionOwner = { kind: 'lead' };
nextAction = 'repair_dependencies';
queueCategory = 'oversight';
reasonCode = 'dependency_broken';
derivedFrom.push('dependency_graph');
} else if (waitingDependencyIds.length > 0) {
actionOwner = { kind: 'none' };
nextAction = 'wait_dependency';
queueCategory = 'waiting';
reasonCode = 'dependency_waiting';
derivedFrom.push('dependency_graph');
} else if (reviewStateResult.state === 'needsFix') {
actionOwner =
ownerActor.kind === 'lead'
? { kind: 'lead' }
: { kind: 'member', memberName: ownerActor.memberName };
nextAction = 'apply_changes';
queueCategory = actionOwner.kind === 'lead' ? 'oversight' : 'actionable';
reasonCode = 'needs_fix';
} else if (task.status === 'in_progress') {
actionOwner =
ownerActor.kind === 'lead'
? { kind: 'lead' }
: { kind: 'member', memberName: ownerActor.memberName };
nextAction = 'execute';
queueCategory = actionOwner.kind === 'lead' ? 'oversight' : 'actionable';
reasonCode = 'owner_executing';
} else if (task.status === 'pending') {
actionOwner =
ownerActor.kind === 'lead'
? { kind: 'lead' }
: { kind: 'member', memberName: ownerActor.memberName };
nextAction = 'execute';
queueCategory = actionOwner.kind === 'lead' ? 'oversight' : 'actionable';
reasonCode = 'owner_ready';
}
const watchers = buildWatchers(ownerActor, reviewActor, actionOwner);
const lastMeaningfulEventAt = getLastMeaningfulEventAt(task);
return {
taskId: task.id,
displayId: task.displayId,
subject: task.subject,
status: task.status,
reviewState: reviewStateResult.state,
actionOwner,
nextAction,
queueCategory,
reasonCode,
...(normalizeName(task.owner) ? { owner: task.owner } : {}),
reviewer:
reviewActor && reviewActor.kind === 'member'
? reviewActor.memberName
: reviewActor && reviewActor.kind === 'lead'
? reviewActor.memberName
: null,
...(blockedByIds.length > 0 ? { blockedBy: blockedByIds } : {}),
...(watchers.length > 0 ? { watchers } : {}),
...(task.needsClarification ? { needsClarification: task.needsClarification } : {}),
...(lastMeaningfulEventAt ? { lastMeaningfulEventAt } : {}),
derivedFrom,
_fullTask: task,
};
}
function compareAgendaItems(left, right) {
const leftTs = Date.parse(normalizeName(left.lastMeaningfulEventAt)) || 0;
const rightTs = Date.parse(normalizeName(right.lastMeaningfulEventAt)) || 0;
if (leftTs !== rightTs) return rightTs - leftTs;
const byDisplay = String(left.displayId || left.taskId).localeCompare(String(right.displayId || right.taskId), undefined, {
numeric: true,
sensitivity: 'base',
});
if (byDisplay !== 0) return byDisplay;
return String(left.taskId).localeCompare(String(right.taskId), undefined, {
numeric: true,
sensitivity: 'base',
});
}
function buildAgendaSnapshot(paths, teamName, actor) {
return withTeamBoardLock(paths, () => {
const boardState = buildBoardState(paths, teamName);
const items = boardState.tasks.map((task) => buildAgendaItem(task, boardState));
const actionable = [];
const awareness = [];
for (const item of items) {
if (actor.kind === 'member') {
const memberKey = normalizeKey(actor.memberName);
const isActionable =
item.actionOwner.kind === 'member' &&
normalizeKey(item.actionOwner.memberName) === memberKey;
const isRelevant =
isActionable ||
normalizeKey(item.owner) === memberKey ||
normalizeKey(item.reviewer) === memberKey ||
(Array.isArray(item.watchers) && item.watchers.some((entry) => normalizeKey(entry) === memberKey));
if (isActionable) {
actionable.push(item);
} else if (isRelevant) {
awareness.push(item);
}
continue;
}
if (item.actionOwner.kind === 'lead') {
actionable.push(item);
} else if (
item.actionOwner.kind === 'user' ||
item.reasonCode === 'dependency_waiting' ||
item.reasonCode === 'review_in_progress' ||
item.reasonCode === 'review_requested_waiting_pickup'
) {
awareness.push(item);
}
}
actionable.sort(compareAgendaItems);
awareness.sort(compareAgendaItems);
return {
actor,
actionable,
awareness,
anomalies: boardState.anomalies,
counters: {
actionable: actionable.length,
awareness: awareness.length,
blocked: items.filter(
(item) =>
item.reasonCode === 'dependency_waiting' || item.reasonCode === 'dependency_broken'
).length,
waitingOnUser: items.filter((item) => item.reasonCode === 'waiting_user_clarification').length,
waitingOnLead: items.filter((item) => item.reasonCode === 'waiting_lead_clarification').length,
reviewNeeded: items.filter(
(item) =>
item.reasonCode === 'review_reviewer_missing' ||
item.reasonCode === 'review_requested_waiting_pickup' ||
item.reasonCode === 'review_in_progress' ||
item.reasonCode === 'self_review_invalid'
).length,
anomalies: boardState.anomalies.length,
},
};
});
}
function buildInventoryRow(task, reviewState, kanbanEntry) {
return {
id: task.id,
displayId: task.displayId,
subject: truncateText(task.subject, MAX_SUBJECT_CHARS),
status: task.status,
...(normalizeName(task.owner) ? { owner: task.owner } : {}),
reviewState,
...(kanbanEntry && INVENTORY_KANBAN_COLUMNS.has(kanbanEntry.column)
? { kanbanColumn: kanbanEntry.column }
: {}),
...(task.needsClarification ? { needsClarification: task.needsClarification } : {}),
...(Array.isArray(task.blockedBy) && task.blockedBy.length > 0 ? { blockedBy: task.blockedBy } : {}),
...(Array.isArray(task.blocks) && task.blocks.length > 0 ? { blocks: task.blocks } : {}),
...(Array.isArray(task.related) && task.related.length > 0 ? { related: task.related } : {}),
commentCount: Array.isArray(task.comments) ? task.comments.length : 0,
...(normalizeName(task.createdAt) ? { createdAt: task.createdAt } : {}),
...(normalizeName(task.updatedAt) ? { updatedAt: task.updatedAt } : {}),
};
}
function matchesInventoryFilters(row, filters) {
if (normalizeName(filters.owner) && normalizeKey(row.owner) !== normalizeKey(filters.owner)) {
return false;
}
if (normalizeName(filters.status) && row.status !== filters.status) {
return false;
}
if (normalizeName(filters.reviewState) && row.reviewState !== filters.reviewState) {
return false;
}
if (normalizeName(filters.kanbanColumn)) {
const kanbanColumn = filters.kanbanColumn;
if (!INVENTORY_KANBAN_COLUMNS.has(kanbanColumn) || row.kanbanColumn !== kanbanColumn) {
return false;
}
}
if (normalizeName(filters.relatedTo)) {
const related = Array.isArray(row.related) ? row.related : [];
if (!related.includes(filters.relatedTo)) {
return false;
}
}
if (normalizeName(filters.blockedBy)) {
const blockedBy = Array.isArray(row.blockedBy) ? row.blockedBy : [];
if (!blockedBy.includes(filters.blockedBy)) {
return false;
}
}
return true;
}
function listTaskInventory(paths, teamName, filters = {}) {
return withTeamBoardLock(paths, () => {
const taskRows = taskStore.listTaskRows(paths);
const kanbanState = kanbanStore.readKanbanState(paths, teamName);
const resolvedRelatedTo = normalizeName(filters.relatedTo)
? taskStore.resolveTaskRef(paths, filters.relatedTo)
: '';
const resolvedBlockedBy = normalizeName(filters.blockedBy)
? taskStore.resolveTaskRef(paths, filters.blockedBy)
: '';
const limit =
typeof filters.limit === 'number' && Number.isFinite(filters.limit)
? Math.max(1, Math.floor(filters.limit))
: null;
const resolvedFilters = {
...filters,
...(resolvedRelatedTo ? { relatedTo: resolvedRelatedTo } : {}),
...(resolvedBlockedBy ? { blockedBy: resolvedBlockedBy } : {}),
};
const candidates = [];
const addCandidate = (candidate) => {
if (limit == null || candidates.length < limit) {
candidates.push(candidate);
return;
}
let oldestIndex = 0;
for (let index = 1; index < candidates.length; index += 1) {
if (compareTasksByFreshness(candidates[index].task, candidates[oldestIndex].task) > 0) {
oldestIndex = index;
}
}
if (compareTasksByFreshness(candidate.task, candidates[oldestIndex].task) < 0) {
candidates[oldestIndex] = candidate;
}
};
for (const task of taskRows.tasks) {
const kanbanEntry = kanbanState.tasks ? kanbanState.tasks[task.id] : undefined;
const reviewState = resolveEffectiveReviewState(task, kanbanEntry).state;
const row = buildInventoryRow(task, reviewState, kanbanEntry);
if (!matchesInventoryFilters(row, resolvedFilters)) {
continue;
}
addCandidate({ task, row });
}
return candidates.sort((left, right) => compareTasksByFreshness(left.task, right.task)).map((entry) => entry.row);
});
}
function formatActionOwner(actionOwner) {
if (actionOwner.kind === 'member') return `@${actionOwner.memberName}`;
if (actionOwner.kind === 'lead') return 'lead';
if (actionOwner.kind === 'user') return 'user';
return 'none';
}
function formatAgendaLine(item) {
const reviewSuffix = item.reviewState !== 'none' ? `, review=${item.reviewState}` : '';
const meta = [
`next=${item.nextAction}`,
`owner=${normalizeName(item.owner) || 'none'}`,
`actionOwner=${formatActionOwner(item.actionOwner)}`,
`reason=${item.reasonCode}`,
];
if (normalizeName(item.reviewer)) {
meta.push(`reviewer=${item.reviewer}`);
}
if (item.needsClarification) {
meta.push(`clarification=${item.needsClarification}`);
}
return `- ${formatTaskLabel(item)} [status=${item.status}${reviewSuffix}] ${truncateText(item.subject, MAX_SUBJECT_CHARS)} (${meta.join(', ')})`;
}
function appendExpandedTaskContext(lines, item) {
const task = item._fullTask;
if (!task || typeof task !== 'object') return;
if (normalizeName(task.description)) {
lines.push(` Description: ${truncateText(task.description, MAX_DESCRIPTION_CHARS)}`);
}
const comments = Array.isArray(task.comments) ? task.comments : [];
if (comments.length === 0) return;
lines.push(' Comments:');
for (const comment of comments.slice(-5)) {
const author = normalizeName(comment && comment.author) || 'unknown';
const text = truncateText(comment && comment.text, MAX_COMMENT_CHARS) || '(empty comment)';
lines.push(` - ${author}: ${text}`);
}
}
function appendOmittedLine(lines, sectionLabel, shownCount, totalCount) {
if (totalCount <= shownCount) return;
lines.push(
`... ${totalCount - shownCount} more ${sectionLabel} item(s) omitted. Use task_list filters and task_get for drill-down.`
);
}
function formatAnomalyLine(anomaly) {
const ref = normalizeName(anomaly.taskId) ? ` (${anomaly.taskId})` : '';
return `- ${anomaly.code}${ref}: ${truncateText(anomaly.detail, MAX_ANOMALY_DETAIL_CHARS)}`;
}
function appendAnomalies(lines, anomalies) {
const shown = anomalies.slice(0, MAX_ANOMALY_ITEMS);
for (const anomaly of shown) {
lines.push(formatAnomalyLine(anomaly));
}
if (anomalies.length > shown.length) {
lines.push(
`... ${anomalies.length - shown.length} more board anomaly item(s) omitted. Run maintenance/reconcile or inspect board files for full details.`
);
}
}
function formatTaskBriefing(paths, teamName, memberName) {
const memberValidation = validateBriefingMember(paths, teamName, memberName);
const snapshot = buildAgendaSnapshot(paths, teamName, {
kind: 'member',
memberName: normalizeName(memberName),
});
const lines = [
`Task briefing for ${memberName}:`,
`Primary queue for ${memberName}. Act only on Actionable items. Awareness items are watch-only context unless the lead reroutes the task or you become the actionOwner.`,
`Use task_list only to search/browse inventory rows, not as your working queue.`,
];
if (memberValidation.warnings.length > 0 || snapshot.anomalies.length > 0) {
lines.push('', 'Board warnings:');
for (const warning of memberValidation.warnings) {
lines.push(`- ${warning}`);
}
appendAnomalies(lines, snapshot.anomalies);
}
if (snapshot.actionable.length === 0 && snapshot.awareness.length === 0) {
lines.push('', `No actionable or awareness tasks for ${memberName}.`);
return lines.join('\n');
}
if (snapshot.actionable.length > 0) {
lines.push('', 'Actionable:');
let expandedCount = 0;
const actionableItems = snapshot.actionable.slice(0, MAX_MEMBER_ACTIONABLE_ITEMS);
for (const item of actionableItems) {
lines.push(formatAgendaLine(item));
if (item.status === 'in_progress' || item.reasonCode === 'needs_fix') {
if (expandedCount < MAX_EXPANDED_CONTEXT_ITEMS) {
appendExpandedTaskContext(lines, item);
expandedCount += 1;
} else {
lines.push(' Context omitted: use task_get for full task details.');
}
}
}
appendOmittedLine(lines, 'Actionable', actionableItems.length, snapshot.actionable.length);
}
if (snapshot.awareness.length > 0) {
lines.push('', 'Awareness:');
const awarenessItems = snapshot.awareness.slice(0, MAX_MEMBER_AWARENESS_ITEMS);
for (const item of awarenessItems) {
lines.push(formatAgendaLine(item));
}
appendOmittedLine(lines, 'Awareness', awarenessItems.length, snapshot.awareness.length);
}
lines.push(
'',
`Counters: actionable=${snapshot.counters.actionable}, awareness=${snapshot.counters.awareness}, blocked=${snapshot.counters.blocked}, waitingOnUser=${snapshot.counters.waitingOnUser}, waitingOnLead=${snapshot.counters.waitingOnLead}, reviewNeeded=${snapshot.counters.reviewNeeded}, anomalies=${snapshot.counters.anomalies}`
);
return lines.join('\n');
}
function bucketLeadItems(items) {
const buckets = {
assign_owner: [],
assign_reviewer: [],
clarify_with_lead: [],
repair_dependencies: [],
lead_owned: [],
};
for (const item of items) {
if (item.nextAction === 'assign_owner') {
buckets.assign_owner.push(item);
continue;
}
if (item.nextAction === 'assign_reviewer') {
buckets.assign_reviewer.push(item);
continue;
}
if (item.nextAction === 'clarify_with_lead') {
buckets.clarify_with_lead.push(item);
continue;
}
if (item.nextAction === 'repair_dependencies') {
buckets.repair_dependencies.push(item);
continue;
}
buckets.lead_owned.push(item);
}
return buckets;
}
function formatLeadBriefing(paths, teamName) {
const roster = buildQueueRoster(paths);
const leadHeaderName = roster.leadHeaderName ? ` for ${roster.leadHeaderName}` : '';
const snapshot = buildAgendaSnapshot(paths, teamName, { kind: 'lead' });
const buckets = bucketLeadItems(snapshot.actionable);
const lines = [
`Lead queue${leadHeaderName} on team "${teamName}":`,
`Primary lead queue. Sections below already represent lead-owned actions or watch-only context.`,
`Use task_list only for search, filtering, and drill-down inventory lookups.`,
];
if (snapshot.anomalies.length > 0) {
lines.push('', 'Board anomalies:');
appendAnomalies(lines, snapshot.anomalies);
}
const sections = [
['Needs owner assignment:', buckets.assign_owner],
['Needs reviewer assignment:', buckets.assign_reviewer],
['Needs clarification from lead:', buckets.clarify_with_lead],
['Dependency repair:', buckets.repair_dependencies],
['Lead-owned follow-up:', buckets.lead_owned],
[
'Waiting on user:',
snapshot.awareness.filter((item) => item.reasonCode === 'waiting_user_clarification'),
],
[
'Watching:',
snapshot.awareness.filter((item) => item.reasonCode !== 'waiting_user_clarification'),
],
];
let renderedAnySection = false;
for (const [title, items] of sections) {
if (!items || items.length === 0) continue;
renderedAnySection = true;
lines.push('', title);
const sectionItems = items.slice(0, MAX_LEAD_SECTION_ITEMS);
for (const item of sectionItems) {
lines.push(formatAgendaLine(item));
}
appendOmittedLine(lines, title.replace(/:$/, ''), sectionItems.length, items.length);
}
if (!renderedAnySection && snapshot.anomalies.length === 0) {
lines.push('', 'No lead action items.');
}
lines.push(
'',
`Counters: actionable=${snapshot.counters.actionable}, awareness=${snapshot.counters.awareness}, blocked=${snapshot.counters.blocked}, waitingOnUser=${snapshot.counters.waitingOnUser}, waitingOnLead=${snapshot.counters.waitingOnLead}, reviewNeeded=${snapshot.counters.reviewNeeded}, anomalies=${snapshot.counters.anomalies}`
);
return lines.join('\n');
}
module.exports = {
buildAgendaSnapshot,
formatLeadBriefing,
formatTaskBriefing,
listTaskInventory,
resolveCurrentCycleReviewer,
resolveEffectiveReviewState,
};