105 lines
4.4 KiB
TypeScript
105 lines
4.4 KiB
TypeScript
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);
|
||
}
|