M13: Veilige Claude-agent-workflow (Scrum4Me-side) (#26)
* feat: add pushed_at field to ClaudeJob schema Nullable DateTime column to record when the agent's feature branch was pushed to origin. Enables the UI to show a 'pushed' state independently of DONE status. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: GitHub-link op DONE-card + pushed_at doorvoer - lib/job-status-url.ts: getBranchUrl(repoUrl, branch) → GitHub tree URL - JobState + ClaudeJobEvent: pushed_at? veld toegevoegd - realtime/solo/route.ts: pushed_at in Prisma-select, JobPayload en mapping - SoloBoardProps + TaskDetailDialog: repoUrl prop doorgevoerd - task-detail-dialog: "Open op GitHub"-link als done + pushed_at + branch + repoUrl - 3 unit-tests voor getBranchUrl; totaal 261 tests groen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add VerifyResult enum, verify_only on Task, verify_result on ClaudeJob Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add verify_result+pushed_at to JobState, VerifyResultApi type, SSE payload Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: verify_only field on SoloTask, PATCH route saves verify_only Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: TaskDetailDialog — verify_result display + verify_only checkbox Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: verify_only PATCH + verify_result dialog render + store fix Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: document VerifyResult enum, verify_only task field, pushed_at in architecture Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(M13): cron /api/cron/cleanup-agent-artifacts — hard-delete FAILED/CANCELLED jobs >7 days * feat(M13): add auto_pr field to Product schema + migration * feat(M13): auto_pr toggle in product settings — server action + UI component + tests * feat(M13): add pr_url to ClaudeJob schema + migration * feat(M13): UI — 'Open PR' link on DONE-card; pr_url in JobState + SSE + task-dialog * feat(M13): add retry_count migration + regen erd - Migration ALTER TABLE claude_jobs ADD COLUMN retry_count INT DEFAULT 0 (schema.prisma was reeds bijgewerkt in eerdere commits) - docs/erd.svg geregenereerd voor de complete M13-schema-wijzigingen (verify_result, verify_only, pushed_at, pr_url, auto_pr, retry_count) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
acb591266f
commit
9794a9baef
26 changed files with 725 additions and 20 deletions
|
|
@ -7,6 +7,7 @@ import { ProductForm } from '@/components/products/product-form'
|
|||
import { ArchiveProductButton } from '@/components/products/archive-product-button'
|
||||
import { TeamManager } from '@/components/products/team-manager'
|
||||
import { updateProductAction } from '@/actions/products'
|
||||
import { AutoPrToggle } from '@/components/products/auto-pr-toggle'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface Props {
|
||||
|
|
@ -55,6 +56,16 @@ export default async function ProductSettingsPage({ params }: Props) {
|
|||
}}
|
||||
/>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-border space-y-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-foreground">Agent-instellingen</h2>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Automatiseer acties na een succesvolle agent-job.
|
||||
</p>
|
||||
</div>
|
||||
<AutoPrToggle productId={id} initialValue={product.auto_pr} />
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-border space-y-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-foreground">Team</h2>
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ export default async function SoloProductPage({ params }: Props) {
|
|||
priority: t.priority,
|
||||
sort_order: t.sort_order,
|
||||
status: t.status as SoloTask['status'],
|
||||
verify_only: t.verify_only,
|
||||
story_id: t.story.id,
|
||||
story_code: t.story.code,
|
||||
story_title: t.story.title,
|
||||
|
|
@ -110,6 +111,7 @@ export default async function SoloProductPage({ params }: Props) {
|
|||
unassignedStories={unassignedStories}
|
||||
isDemo={session.isDemo ?? false}
|
||||
currentUserId={session.userId}
|
||||
repoUrl={product.repo_url}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
24
app/api/cron/cleanup-agent-artifacts/route.ts
Normal file
24
app/api/cron/cleanup-agent-artifacts/route.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
const CUTOFF_DAYS = 7
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const auth = request.headers.get('authorization')
|
||||
const expected = process.env.CRON_SECRET
|
||||
if (!expected || auth !== `Bearer ${expected}`) {
|
||||
return Response.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const cutoff = new Date(Date.now() - CUTOFF_DAYS * 24 * 60 * 60 * 1000)
|
||||
|
||||
const { count: deleted } = await prisma.claudeJob.deleteMany({
|
||||
where: {
|
||||
status: { in: ['FAILED', 'CANCELLED'] },
|
||||
finished_at: { lt: cutoff },
|
||||
},
|
||||
})
|
||||
|
||||
return Response.json({ deleted, ran_at: new Date().toISOString() })
|
||||
}
|
||||
|
|
@ -45,6 +45,9 @@ type JobPayload = {
|
|||
product_id: string
|
||||
status: string
|
||||
branch?: string
|
||||
pushed_at?: string
|
||||
pr_url?: string
|
||||
verify_result?: string
|
||||
summary?: string
|
||||
error?: string
|
||||
}
|
||||
|
|
@ -258,7 +261,7 @@ async function prisma_jobs_findActive(userId: string, productId: string) {
|
|||
],
|
||||
},
|
||||
select: {
|
||||
id: true, task_id: true, status: true, branch: true, summary: true, error: true,
|
||||
id: true, task_id: true, status: true, branch: true, pushed_at: true, pr_url: true, verify_result: true, summary: true, error: true,
|
||||
},
|
||||
orderBy: { created_at: 'asc' },
|
||||
})
|
||||
|
|
@ -267,6 +270,9 @@ async function prisma_jobs_findActive(userId: string, productId: string) {
|
|||
task_id: j.task_id,
|
||||
status: jobStatusToApi(j.status),
|
||||
branch: j.branch ?? undefined,
|
||||
pushed_at: j.pushed_at?.toISOString() ?? undefined,
|
||||
pr_url: j.pr_url ?? undefined,
|
||||
verify_result: j.verify_result?.toLowerCase() as import('@/stores/solo-store').VerifyResultApi | undefined,
|
||||
summary: j.summary ?? undefined,
|
||||
error: j.error ?? undefined,
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -14,10 +14,15 @@ const patchSchema = z
|
|||
.object({
|
||||
status: z.enum(PATCHABLE_TASK_STATUS as [string, ...string[]]).optional(),
|
||||
implementation_plan: z.string().optional(),
|
||||
verify_only: z.boolean().optional(),
|
||||
})
|
||||
.refine((data) => data.status !== undefined || data.implementation_plan !== undefined, {
|
||||
message: 'Geef minimaal status of implementation_plan mee',
|
||||
})
|
||||
.refine(
|
||||
(data) =>
|
||||
data.status !== undefined ||
|
||||
data.implementation_plan !== undefined ||
|
||||
data.verify_only !== undefined,
|
||||
{ message: 'Geef minimaal status, implementation_plan of verify_only mee' },
|
||||
)
|
||||
|
||||
export async function PATCH(
|
||||
request: Request,
|
||||
|
|
@ -83,12 +88,19 @@ export async function PATCH(
|
|||
}
|
||||
}
|
||||
|
||||
// Combine simple field writes (plan, verify_only) into one update call
|
||||
const simpleData: { implementation_plan?: string; verify_only?: boolean } = {}
|
||||
if (parsed.data.implementation_plan !== undefined)
|
||||
simpleData.implementation_plan = parsed.data.implementation_plan
|
||||
if (parsed.data.verify_only !== undefined)
|
||||
simpleData.verify_only = parsed.data.verify_only
|
||||
|
||||
const updated = await prisma.$transaction(async (tx) => {
|
||||
const planUpdate = parsed.data.implementation_plan !== undefined
|
||||
const simpleUpdate = Object.keys(simpleData).length > 0
|
||||
? await tx.task.update({
|
||||
where: { id },
|
||||
data: { implementation_plan: parsed.data.implementation_plan },
|
||||
select: { id: true, status: true, implementation_plan: true },
|
||||
data: simpleData,
|
||||
select: { id: true, status: true, implementation_plan: true, verify_only: true },
|
||||
})
|
||||
: null
|
||||
|
||||
|
|
@ -98,12 +110,13 @@ export async function PATCH(
|
|||
id: result.task.id,
|
||||
status: result.task.status,
|
||||
implementation_plan: result.task.implementation_plan,
|
||||
verify_only: simpleUpdate?.verify_only,
|
||||
}
|
||||
}
|
||||
|
||||
if (planUpdate) return planUpdate
|
||||
if (simpleUpdate) return simpleUpdate
|
||||
|
||||
// Should not reach here — patchSchema rejects bodies without status or implementation_plan.
|
||||
// Should not reach here — patchSchema rejects bodies without recognized fields.
|
||||
throw new Error('Geen wijzigingen')
|
||||
})
|
||||
|
||||
|
|
@ -111,5 +124,6 @@ export async function PATCH(
|
|||
id: updated.id,
|
||||
status: taskStatusToApi(updated.status),
|
||||
implementation_plan: updated.implementation_plan,
|
||||
...(updated.verify_only !== undefined && { verify_only: updated.verify_only }),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue