Add follow-up task generation on approve
When approving a task, PM agent analyzes pipeline output and creates
follow-up tasks automatically (e.g. security audit → 8 fix tasks).
core/followup.py:
generate_followups() — collects pipeline output, runs followup agent,
parses JSON task list, creates tasks with parent_task_id linkage.
Handles: bare arrays, {tasks:[...]} wrappers, invalid JSON, empty.
agents/prompts/followup.md — PM prompt for analyzing results and
creating actionable follow-up tasks with priority from severity.
CLI: kin approve <task_id> [--followup] [--decision "text"]
API: POST /api/tasks/{id}/approve {create_followups: true}
Returns {status, decision, followup_tasks: [...]}
Frontend (TaskDetail approve modal):
- Checkbox "Create follow-up tasks" (default ON)
- Loading state during generation
- Results view: list of created tasks with links to /task/:id
ProjectView: tasks show "from VDOL-001" for follow-ups.
13 new tests (followup), 125 total, all passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f7830d484c
commit
9264415776
8 changed files with 426 additions and 17 deletions
|
|
@ -112,8 +112,8 @@ export const api = {
|
|||
post<Project>('/projects', data),
|
||||
createTask: (data: { project_id: string; title: string; priority?: number; route_type?: string }) =>
|
||||
post<Task>('/tasks', data),
|
||||
approveTask: (id: string, data?: { decision_title?: string; decision_description?: string; decision_type?: string }) =>
|
||||
post<{ status: string }>(`/tasks/${id}/approve`, data || {}),
|
||||
approveTask: (id: string, data?: { decision_title?: string; decision_description?: string; decision_type?: string; create_followups?: boolean }) =>
|
||||
post<{ status: string; followup_tasks: Task[] }>(`/tasks/${id}/approve`, data || {}),
|
||||
rejectTask: (id: string, reason: string) =>
|
||||
post<{ status: string }>(`/tasks/${id}/reject`, { reason }),
|
||||
runTask: (id: string) =>
|
||||
|
|
|
|||
|
|
@ -197,6 +197,7 @@ async function addDecision() {
|
|||
<span class="text-gray-500 shrink-0 w-24">{{ t.id }}</span>
|
||||
<Badge :text="t.status" :color="taskStatusColor(t.status)" />
|
||||
<span class="text-gray-300 truncate">{{ t.title }}</span>
|
||||
<span v-if="t.parent_task_id" class="text-[10px] text-gray-600 shrink-0">from {{ t.parent_task_id }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-gray-600 shrink-0">
|
||||
<span v-if="t.assigned_role">{{ t.assigned_role }}</span>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ let pollTimer: ReturnType<typeof setInterval> | null = null
|
|||
|
||||
// Approve modal
|
||||
const showApprove = ref(false)
|
||||
const approveForm = ref({ title: '', description: '', type: 'decision' })
|
||||
const approveForm = ref({ title: '', description: '', type: 'decision', createFollowups: true })
|
||||
const approveLoading = ref(false)
|
||||
const followupResults = ref<{ id: string; title: string }[]>([])
|
||||
|
||||
// Reject modal
|
||||
const showReject = ref(false)
|
||||
|
|
@ -74,16 +76,29 @@ function formatOutput(text: string | null): string {
|
|||
|
||||
async function approve() {
|
||||
if (!task.value) return
|
||||
approveLoading.value = true
|
||||
followupResults.value = []
|
||||
try {
|
||||
const data = approveForm.value.title
|
||||
? { decision_title: approveForm.value.title, decision_description: approveForm.value.description, decision_type: approveForm.value.type }
|
||||
: undefined
|
||||
await api.approveTask(props.id, data)
|
||||
showApprove.value = false
|
||||
approveForm.value = { title: '', description: '', type: 'decision' }
|
||||
const data: Record<string, unknown> = {
|
||||
create_followups: approveForm.value.createFollowups,
|
||||
}
|
||||
if (approveForm.value.title) {
|
||||
data.decision_title = approveForm.value.title
|
||||
data.decision_description = approveForm.value.description
|
||||
data.decision_type = approveForm.value.type
|
||||
}
|
||||
const res = await api.approveTask(props.id, data as any)
|
||||
if (res.followup_tasks?.length) {
|
||||
followupResults.value = res.followup_tasks.map(t => ({ id: t.id, title: t.title }))
|
||||
} else {
|
||||
showApprove.value = false
|
||||
}
|
||||
approveForm.value = { title: '', description: '', type: 'decision', createFollowups: true }
|
||||
await load()
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
} finally {
|
||||
approveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -227,16 +242,36 @@ const hasSteps = computed(() => (task.value?.pipeline_steps?.length ?? 0) > 0)
|
|||
</div>
|
||||
|
||||
<!-- Approve Modal -->
|
||||
<Modal v-if="showApprove" title="Approve Task" @close="showApprove = false">
|
||||
<form @submit.prevent="approve" class="space-y-3">
|
||||
<p class="text-sm text-gray-400">Optionally record a decision from this task:</p>
|
||||
<Modal v-if="showApprove" title="Approve Task" @close="showApprove = false; followupResults = []">
|
||||
<!-- Follow-up results -->
|
||||
<div v-if="followupResults.length" class="space-y-3">
|
||||
<p class="text-sm text-green-400">Task approved. Created {{ followupResults.length }} follow-up tasks:</p>
|
||||
<div class="space-y-1">
|
||||
<router-link v-for="f in followupResults" :key="f.id" :to="`/task/${f.id}`"
|
||||
class="block px-3 py-2 border border-gray-800 rounded text-sm text-gray-300 hover:border-gray-600 no-underline">
|
||||
<span class="text-gray-500">{{ f.id }}</span> {{ f.title }}
|
||||
</router-link>
|
||||
</div>
|
||||
<button @click="showApprove = false; followupResults = []"
|
||||
class="w-full py-2 bg-gray-800 text-gray-300 border border-gray-700 rounded text-sm hover:bg-gray-700">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<!-- Approve form -->
|
||||
<form v-else @submit.prevent="approve" class="space-y-3">
|
||||
<label class="flex items-center gap-2 text-sm text-gray-300 cursor-pointer">
|
||||
<input type="checkbox" v-model="approveForm.createFollowups"
|
||||
class="rounded border-gray-600 bg-gray-800 text-blue-500" />
|
||||
Create follow-up tasks from pipeline results
|
||||
</label>
|
||||
<p class="text-xs text-gray-500">Optionally record a decision:</p>
|
||||
<input v-model="approveForm.title" placeholder="Decision title (optional)"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
|
||||
<textarea v-if="approveForm.title" v-model="approveForm.description" placeholder="Description"
|
||||
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" rows="2"></textarea>
|
||||
<button type="submit"
|
||||
class="w-full py-2 bg-green-900/50 text-green-400 border border-green-800 rounded text-sm hover:bg-green-900">
|
||||
Approve & mark done
|
||||
<button type="submit" :disabled="approveLoading"
|
||||
class="w-full py-2 bg-green-900/50 text-green-400 border border-green-800 rounded text-sm hover:bg-green-900 disabled:opacity-50">
|
||||
{{ approveLoading ? 'Processing...' : 'Approve & mark done' }}
|
||||
</button>
|
||||
</form>
|
||||
</Modal>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue