kin: KIN-091 Улучшения из исследования рынка: (1) Revise button с feedback loop, (2) auto-test before review — агент сам прогоняет тесты и фиксит до review, (3) spec-driven workflow для новых проектов — constitution → spec → plan → tasks, (4) git worktrees для параллельных агентов без конфликтов, (5) auto-trigger pipeline при создании задачи с label auto

This commit is contained in:
Gros Frumos 2026-03-16 22:35:31 +02:00
parent 0cc063d47a
commit 0ccd451b4b
14 changed files with 1660 additions and 18 deletions

View file

@ -33,6 +33,34 @@ const startPhaseSaving = ref(false)
const approvePhaseSaving = ref(false)
let phasePollTimer: ReturnType<typeof setInterval> | null = null
// Task Revise
const showTaskReviseModal = ref(false)
const taskReviseTaskId = ref<string | null>(null)
const taskReviseComment = ref('')
const taskReviseError = ref('')
const taskReviseSaving = ref(false)
function openTaskRevise(taskId: string) {
taskReviseTaskId.value = taskId
taskReviseComment.value = ''
taskReviseError.value = ''
showTaskReviseModal.value = true
}
async function submitTaskRevise() {
if (!taskReviseComment.value.trim()) { taskReviseError.value = 'Комментарий обязателен'; return }
taskReviseSaving.value = true
try {
await api.reviseTask(taskReviseTaskId.value!, taskReviseComment.value)
showTaskReviseModal.value = false
await load()
} catch (e: any) {
taskReviseError.value = e.message
} finally {
taskReviseSaving.value = false
}
}
function checkAndPollPhases() {
const hasRunning = phases.value.some(ph => ph.task?.status === 'in_progress')
if (hasRunning && !phasePollTimer) {
@ -143,7 +171,7 @@ function phaseStatusColor(s: string) {
}
// Filters
const ALL_TASK_STATUSES = ['pending', 'in_progress', 'review', 'blocked', 'decomposed', 'done', 'cancelled']
const ALL_TASK_STATUSES = ['pending', 'in_progress', 'review', 'blocked', 'decomposed', 'done', 'revising', 'cancelled']
function initStatusFilter(): string[] {
const q = route.query.status as string
@ -333,6 +361,21 @@ const CATEGORY_COLORS: Record<string, string> = {
const showAddTask = ref(false)
const taskForm = ref({ title: '', priority: 5, route_type: '', category: '', acceptance_criteria: '' })
const taskFormError = ref('')
const pendingFiles = ref<File[]>([])
const fileInputRef = ref<HTMLInputElement | null>(null)
function onFileSelect(e: Event) {
const input = e.target as HTMLInputElement
if (input.files) {
pendingFiles.value.push(...Array.from(input.files))
input.value = ''
}
}
function closeAddTaskModal() {
showAddTask.value = false
pendingFiles.value = []
}
// Add decision modal
const showAddDecision = ref(false)
@ -421,6 +464,7 @@ function taskStatusColor(s: string) {
const m: Record<string, string> = {
pending: 'gray', in_progress: 'blue', review: 'purple',
done: 'green', blocked: 'red', decomposed: 'yellow', cancelled: 'gray',
revising: 'orange',
}
return m[s] || 'gray'
}
@ -449,7 +493,7 @@ const decTypes = computed(() => {
async function addTask() {
taskFormError.value = ''
try {
await api.createTask({
const task = await api.createTask({
project_id: props.id,
title: taskForm.value.title,
priority: taskForm.value.priority,
@ -457,6 +501,20 @@ async function addTask() {
category: taskForm.value.category || undefined,
acceptance_criteria: taskForm.value.acceptance_criteria || undefined,
})
if (pendingFiles.value.length > 0) {
const failedFiles: string[] = []
for (const file of pendingFiles.value) {
try {
await api.uploadAttachment(task.id, file)
} catch {
failedFiles.push(file.name)
}
}
pendingFiles.value = []
if (failedFiles.length > 0) {
console.warn('Failed to upload attachments:', failedFiles)
}
}
showAddTask.value = false
taskForm.value = { title: '', priority: 5, route_type: '', category: '', acceptance_criteria: '' }
await load()
@ -798,6 +856,12 @@ async function addDecision() {
</button>
<span v-if="t.status === 'in_progress'"
class="inline-block w-2 h-2 bg-blue-500 rounded-full animate-pulse" title="Running"></span>
<button v-if="t.status === 'review' || t.status === 'done'"
@click.prevent.stop="openTaskRevise(t.id)"
class="px-2 py-0.5 bg-orange-900/40 text-orange-400 border border-orange-800 rounded hover:bg-orange-900 text-[10px]"
title="Отправить на доработку">
Revise
</button>
</div>
</router-link>
</div>
@ -1092,7 +1156,7 @@ async function addDecision() {
</div>
<!-- Add Task Modal -->
<Modal v-if="showAddTask" title="Add Task" @close="showAddTask = false">
<Modal v-if="showAddTask" title="Add Task" @close="closeAddTaskModal">
<form @submit.prevent="addTask" class="space-y-3">
<input v-model="taskForm.title" placeholder="Task title" required
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
@ -1117,6 +1181,25 @@ async function addDecision() {
placeholder="Что должно быть на выходе? Какой результат считается успешным?"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600 resize-y"></textarea>
</div>
<div v-if="project?.path">
<label class="block text-xs text-gray-500 mb-1">Вложения</label>
<div class="flex items-center gap-2">
<button type="button" @click="fileInputRef?.click()"
class="px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-xs text-gray-400 hover:bg-gray-700 hover:text-gray-200">
Прикрепить файлы
</button>
<span v-if="pendingFiles.length" class="text-xs text-gray-500">{{ pendingFiles.length }} файл(ов)</span>
</div>
<input ref="fileInputRef" type="file" multiple class="hidden" @change="onFileSelect" />
<ul v-if="pendingFiles.length" class="mt-2 space-y-1">
<li v-for="(file, i) in pendingFiles" :key="i"
class="flex items-center justify-between text-xs text-gray-400 bg-gray-800/50 rounded px-2 py-1">
<span class="truncate">{{ file.name }}</span>
<button type="button" @click="pendingFiles.splice(i, 1)"
class="ml-2 text-gray-600 hover:text-red-400 flex-shrink-0"></button>
</li>
</ul>
</div>
<p v-if="taskFormError" class="text-red-400 text-xs">{{ taskFormError }}</p>
<button type="submit"
class="w-full py-2 bg-blue-900/50 text-blue-400 border border-blue-800 rounded text-sm hover:bg-blue-900">
@ -1218,6 +1301,20 @@ async function addDecision() {
</form>
</Modal>
<!-- Task Revise Modal -->
<Modal v-if="showTaskReviseModal" title="Отправить на доработку" @close="showTaskReviseModal = false">
<div class="space-y-3">
<p class="text-xs text-gray-500">Задача <span class="text-orange-400">{{ taskReviseTaskId }}</span> вернётся в pipeline с вашим комментарием.</p>
<textarea v-model="taskReviseComment" placeholder="Что нужно доработать?" rows="4"
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600 resize-none"></textarea>
<p v-if="taskReviseError" class="text-red-400 text-xs">{{ taskReviseError }}</p>
<button @click="submitTaskRevise" :disabled="taskReviseSaving"
class="w-full py-2 bg-orange-900/50 text-orange-400 border border-orange-800 rounded text-sm hover:bg-orange-900 disabled:opacity-50">
{{ taskReviseSaving ? 'Отправляем...' : 'Отправить на доработку' }}
</button>
</div>
</Modal>
<!-- Audit Modal -->
<Modal v-if="showAuditModal && auditResult" title="Backlog Audit Results" @close="showAuditModal = false">
<div v-if="!auditResult.success" class="text-red-400 text-sm">