agent-ecosystem/src/main/services/team/stallMonitor/TaskProgressSignalClassifier.ts
2026-04-27 20:01:05 +03:00

105 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { stripAgentBlocks } from '@shared/constants/agentBlocks';
import type { BoardTaskActivityRecord } from '../taskLogs/activity/BoardTaskActivityRecord';
import type { TaskComment, TeamTask } from '@shared/types';
export type TaskProgressSignal =
| 'strong_progress'
| 'weak_start_only'
| 'blocker_or_clarification'
| 'terminal_progress'
| 'unknown';
export interface TaskProgressTouchClassification {
signal: TaskProgressSignal;
reason: string;
}
const CONCRETE_FILE_OR_PATH_RE =
/(?:^|\s)(?:\.{1,2}\/|~\/|\/|\w[\w.-]*\/)[\w./\s-]+|\b[\w.-]+\.(?:[cm]?[tj]sx?|json|md|css|scss|py|go|rs|java|kt|swift|ya?ml|toml|lock|sh|sql)\b/i;
const TASK_OR_ISSUE_REF_RE = /#[a-f0-9]{6,}|\btask-[\w-]+/i;
const TEST_OR_BUILD_RESULT_RE =
/\b(?:test(?:s|ed|ing)?|vitest|jest|playwright|pnpm|npm|bun|build|typecheck|lint|passed|failed|green|red|error|exception|stack trace)\b|тест|сборк|линт|ошибк|упал|прош[её]л/i;
const SUBSTANTIVE_WORK_RE =
/\b(?:implemented|fixed|added|updated|changed|removed|found|verified|confirmed|completed|created|refactored|patched|root cause|next step)\b|исправ|добав|обнов|измен|удал|наш[её]л|подтверд|готово|сделал|сделана|причин|следующ/i;
const BLOCKER_OR_CLARIFICATION_RE =
/\?|(?:^|\b)(?:blocked|blocker|cannot|can't|need|needs|waiting|clarification|question|permission|access denied|not enough context)\b|не могу|не получается|нужн|жду|блок|уточн|вопрос|нет доступа|недостаточно контекст/i;
const WEAK_START_ONLY_RE =
/^(?:я\s+)?(?:начинаю(?:\s+работу)?|начну|приступаю(?:\s+к\s+работе)?|беру\s+в\s+работу|проверю|сейчас\s+проверю|посмотрю|разберусь|готов(?:а)?\s+приступить|готов(?:а)?\s+к\s+работе|will\s+start|starting\s+work|starting|taking\s+this|i(?:'|)?ll\s+start|i\s+will\s+start|i\s+am\s+starting|i(?:'|)?ll\s+check|i\s+will\s+check|checking\s+now|on\s+it)(?:[.!…\s]*)$/i;
function normalizeCommentText(text: string): string {
return stripAgentBlocks(text).replace(/\s+/g, ' ').trim();
}
function isConcreteProgress(text: string): boolean {
return (
CONCRETE_FILE_OR_PATH_RE.test(text) ||
TASK_OR_ISSUE_REF_RE.test(text) ||
TEST_OR_BUILD_RESULT_RE.test(text) ||
SUBSTANTIVE_WORK_RE.test(text)
);
}
function classifyTaskCommentText(text: string): TaskProgressTouchClassification {
const normalized = normalizeCommentText(text);
if (!normalized) {
return { signal: 'unknown', reason: 'comment_text_empty' };
}
if (BLOCKER_OR_CLARIFICATION_RE.test(normalized)) {
return {
signal: 'blocker_or_clarification',
reason: 'comment_mentions_blocker_or_clarification',
};
}
if (isConcreteProgress(normalized)) {
return { signal: 'strong_progress', reason: 'comment_contains_concrete_progress' };
}
if (normalized.length <= 120 && WEAK_START_ONLY_RE.test(normalized)) {
return { signal: 'weak_start_only', reason: 'comment_is_start_only' };
}
return { signal: 'unknown', reason: 'comment_progress_signal_unclear' };
}
export function getTaskCommentForActivityRecord(
task: TeamTask,
record: BoardTaskActivityRecord
): TaskComment | null {
const commentId = record.action?.details?.commentId?.trim();
if (!commentId) {
return null;
}
return task.comments?.find((comment) => comment.id === commentId) ?? null;
}
export function classifyTaskProgressTouch(args: {
task: TeamTask;
record: BoardTaskActivityRecord;
}): TaskProgressTouchClassification {
const toolName = args.record.action?.canonicalToolName;
if (toolName === 'task_start' || toolName === 'task_set_status') {
return { signal: 'strong_progress', reason: `${toolName}_is_authoritative_touch` };
}
if (toolName === 'task_complete') {
return { signal: 'terminal_progress', reason: 'task_complete_is_terminal' };
}
if (toolName === 'task_set_clarification') {
return {
signal: 'blocker_or_clarification',
reason: 'task_set_clarification_is_blocker_signal',
};
}
if (toolName !== 'task_add_comment') {
return { signal: 'unknown', reason: 'tool_is_not_classified_for_task_progress' };
}
const comment = getTaskCommentForActivityRecord(args.task, args.record);
if (!comment) {
return { signal: 'unknown', reason: 'task_comment_text_unavailable' };
}
return classifyTaskCommentText(comment.text);
}