From fc2f819645b9f5010a3a8494f45e40935373eff1 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Tue, 5 May 2026 23:05:38 +0200 Subject: [PATCH 1/5] feat(T-571): voeg SKIPPED toe aan ClaudeJobStatus enum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reactie op PBI-33 batch waar worker correct detecteerde dat werk al gemerged was, maar geen passende status had om dat uit te drukken. SKIPPED is bedoeld voor jobs met verify=EMPTY/DIVERGENT waar de diff t.o.v. origin/main leeg is — geen FAILED (geen fout), geen DONE (geen netto-output). Migratie: ALTER TYPE ClaudeJobStatus ADD VALUE 'SKIPPED'. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../migration.sql | 6 ++++++ prisma/schema.prisma | 1 + 2 files changed, 7 insertions(+) create mode 100644 prisma/migrations/20260505230512_add_skipped_to_claude_job_status/migration.sql diff --git a/prisma/migrations/20260505230512_add_skipped_to_claude_job_status/migration.sql b/prisma/migrations/20260505230512_add_skipped_to_claude_job_status/migration.sql new file mode 100644 index 0000000..c4d9d47 --- /dev/null +++ b/prisma/migrations/20260505230512_add_skipped_to_claude_job_status/migration.sql @@ -0,0 +1,6 @@ +-- Add SKIPPED to ClaudeJobStatus enum. +-- Used for jobs where the worker correctly detects that the work was already +-- merged before the job ran (verify=EMPTY/DIVERGENT with no net diff). +-- Distinct from FAILED (genuine errors) and DONE (new commit produced). + +ALTER TYPE "ClaudeJobStatus" ADD VALUE 'SKIPPED'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f7d32b3..87f12af 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -37,6 +37,7 @@ enum ClaudeJobStatus { DONE FAILED CANCELLED + SKIPPED } enum VerifyResult { From deb70a9e20d85d0c038227f6e4d31ded16b65ca0 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Tue, 5 May 2026 23:10:14 +0200 Subject: [PATCH 2/5] feat(T-572): map SKIPPED in lib/job-status + alle terminal-checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lib/job-status.ts: SKIPPED ↔ 'skipped' mapping in beide richtingen - components/shared/job-status.ts: label "Overgeslagen" + neutrale italic styling - actions/admin/jobs.ts: cancel-guard erkent SKIPPED als eindstatus - app/api/cron/cleanup-agent-artifacts: SKIPPED ook opruimen na 7d - lib/insights/agent-throughput: SKIPPED telt mee als terminal ACTIVE_JOB_STATUSES bewust ongewijzigd — SKIPPED is afgerond. Co-Authored-By: Claude Opus 4.7 (1M context) --- actions/admin/jobs.ts | 2 +- app/api/cron/cleanup-agent-artifacts/route.ts | 2 +- components/shared/job-status.ts | 2 + docs/INDEX.md | 3 +- docs/erd.svg | 2 +- docs/plans/auto-pr-deploy-sync.md | 486 ++++++++++++++++++ lib/insights/agent-throughput.ts | 4 +- lib/job-status.ts | 2 + 8 files changed, 497 insertions(+), 6 deletions(-) create mode 100644 docs/plans/auto-pr-deploy-sync.md diff --git a/actions/admin/jobs.ts b/actions/admin/jobs.ts index 9c9ac14..e6a81e0 100644 --- a/actions/admin/jobs.ts +++ b/actions/admin/jobs.ts @@ -19,7 +19,7 @@ export async function cancelJobAction(jobId: string) { }) if (!job) throw new Error('Job niet gevonden') - if (job.status === 'DONE' || job.status === 'FAILED' || job.status === 'CANCELLED') { + if (job.status === 'DONE' || job.status === 'FAILED' || job.status === 'CANCELLED' || job.status === 'SKIPPED') { throw new Error('Job is al in eindstatus') } diff --git a/app/api/cron/cleanup-agent-artifacts/route.ts b/app/api/cron/cleanup-agent-artifacts/route.ts index 7dae4c4..a923867 100644 --- a/app/api/cron/cleanup-agent-artifacts/route.ts +++ b/app/api/cron/cleanup-agent-artifacts/route.ts @@ -15,7 +15,7 @@ export async function POST(request: Request) { const { count: deleted } = await prisma.claudeJob.deleteMany({ where: { - status: { in: ['FAILED', 'CANCELLED'] }, + status: { in: ['FAILED', 'CANCELLED', 'SKIPPED'] }, finished_at: { lt: cutoff }, }, }) diff --git a/components/shared/job-status.ts b/components/shared/job-status.ts index 06b8ecf..065c60f 100644 --- a/components/shared/job-status.ts +++ b/components/shared/job-status.ts @@ -7,6 +7,7 @@ export const JOB_STATUS_LABELS: Record = { done: 'Klaar', failed: 'Mislukt', cancelled: 'Geannuleerd', + skipped: 'Overgeslagen', } export const JOB_STATUS_COLORS: Record = { @@ -16,6 +17,7 @@ export const JOB_STATUS_COLORS: Record = { done: 'bg-status-done/15 text-status-done border-status-done/30', failed: 'bg-status-blocked/15 text-status-blocked border-status-blocked/30', cancelled: 'bg-muted text-muted-foreground border-border', + skipped: 'bg-muted/50 text-muted-foreground border-border italic', } export const JOB_STATUS_ACTIVE = new Set(['queued', 'claimed', 'running']) diff --git a/docs/INDEX.md b/docs/INDEX.md index a4e7051..78e2edf 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -2,7 +2,7 @@ # Documentation Index -Auto-generated on 2026-05-04 from front-matter and headings. +Auto-generated on 2026-05-05 from front-matter and headings. ## Architecture Decision Records @@ -39,6 +39,7 @@ Auto-generated on 2026-05-04 from front-matter and headings. | Title | Status | Updated | |---|---|---| +| [Plan — Auto-PR + selectieve deploy-controle + sync-zicht (end-to-end batch flow)](./plans/auto-pr-deploy-sync.md) | — | — | | [Docs-restructuur — geoptimaliseerd voor AI-lookup](./plans/docs-restructure-ai-lookup.md) | proposal | 2026-05-02 | | [PBI Bulk-Create Spec — Docs-Restructure for AI-Optimized Lookup](./plans/docs-restructure-pbi-spec.md) | done | 2026-05-03 | | [Landing v2 — lokaal & veilig + architectuurdiagram](./plans/landing-local-first.md) | active | 2026-05-03 | diff --git a/docs/erd.svg b/docs/erd.svg index ff9fef4..dc4f289 100644 --- a/docs/erd.svg +++ b/docs/erd.svg @@ -1 +1 @@ -

active_product

user

enum:role

user

user

product

enum:status

pbi

product

sprint

assignee

enum:status

story

enum:type

enum:status

product

enum:status

story

product

sprint

enum:status

enum:verify_required

user

product

task

idea

enum:kind

enum:status

claimed_by_token

enum:verify_result

user

token

product

user

user

product

user

product

pbi

enum:status

idea

enum:type

user

story

task

idea

product

asker

answerer

Role

PRODUCT_OWNER

PRODUCT_OWNER

SCRUM_MASTER

SCRUM_MASTER

DEVELOPER

DEVELOPER

StoryStatus

OPEN

OPEN

IN_SPRINT

IN_SPRINT

DONE

DONE

PbiStatus

READY

READY

BLOCKED

BLOCKED

DONE

DONE

ClaudeJobStatus

QUEUED

QUEUED

CLAIMED

CLAIMED

RUNNING

RUNNING

DONE

DONE

FAILED

FAILED

CANCELLED

CANCELLED

VerifyResult

ALIGNED

ALIGNED

PARTIAL

PARTIAL

EMPTY

EMPTY

DIVERGENT

DIVERGENT

VerifyRequired

ALIGNED

ALIGNED

ALIGNED_OR_PARTIAL

ALIGNED_OR_PARTIAL

ANY

ANY

TaskStatus

TO_DO

TO_DO

IN_PROGRESS

IN_PROGRESS

REVIEW

REVIEW

DONE

DONE

LogType

IMPLEMENTATION_PLAN

IMPLEMENTATION_PLAN

TEST_RESULT

TEST_RESULT

COMMIT

COMMIT

TestStatus

PASSED

PASSED

FAILED

FAILED

SprintStatus

ACTIVE

ACTIVE

COMPLETED

COMPLETED

IdeaStatus

DRAFT

DRAFT

GRILLING

GRILLING

GRILL_FAILED

GRILL_FAILED

GRILLED

GRILLED

PLANNING

PLANNING

PLAN_FAILED

PLAN_FAILED

PLAN_READY

PLAN_READY

PLANNED

PLANNED

ClaudeJobKind

TASK_IMPLEMENTATION

TASK_IMPLEMENTATION

IDEA_GRILL

IDEA_GRILL

IDEA_MAKE_PLAN

IDEA_MAKE_PLAN

IdeaLogType

DECISION

DECISION

NOTE

NOTE

GRILL_RESULT

GRILL_RESULT

PLAN_RESULT

PLAN_RESULT

STATUS_CHANGE

STATUS_CHANGE

JOB_EVENT

JOB_EVENT

users

String

id

🗝️

String

username

String

email

String

password_hash

Boolean

is_demo

String

bio

String

bio_detail

Bytes

avatar_data

Int

idea_code_counter

DateTime

created_at

DateTime

updated_at

user_roles

String

id

🗝️

Role

role

api_tokens

String

id

🗝️

String

token_hash

String

label

DateTime

created_at

DateTime

revoked_at

products

String

id

🗝️

String

name

String

code

String

description

String

repo_url

String

definition_of_done

Boolean

auto_pr

Boolean

archived

DateTime

created_at

DateTime

updated_at

pbis

String

id

🗝️

String

code

String

title

String

description

Int

priority

Float

sort_order

PbiStatus

status

String

pr_url

DateTime

pr_merged_at

DateTime

created_at

DateTime

updated_at

stories

String

id

🗝️

String

code

String

title

String

description

String

acceptance_criteria

Int

priority

Float

sort_order

StoryStatus

status

DateTime

created_at

DateTime

updated_at

story_logs

String

id

🗝️

LogType

type

String

content

TestStatus

status

String

commit_hash

String

commit_message

Json

metadata

DateTime

created_at

sprints

String

id

🗝️

String

sprint_goal

SprintStatus

status

DateTime

start_date

DateTime

end_date

DateTime

created_at

DateTime

completed_at

tasks

String

id

🗝️

String

code

String

title

String

description

String

implementation_plan

Int

priority

Float

sort_order

TaskStatus

status

Boolean

verify_only

VerifyRequired

verify_required

String

repo_url

DateTime

created_at

DateTime

updated_at

claude_jobs

String

id

🗝️

ClaudeJobKind

kind

ClaudeJobStatus

status

DateTime

claimed_at

DateTime

started_at

DateTime

finished_at

DateTime

pushed_at

VerifyResult

verify_result

String

plan_snapshot

String

branch

String

pr_url

String

summary

String

error

Int

retry_count

DateTime

created_at

DateTime

updated_at

claude_workers

String

id

🗝️

String

product_id

DateTime

started_at

DateTime

last_seen_at

product_members

String

id

🗝️

DateTime

created_at

todos

String

id

🗝️

String

title

String

description

Boolean

done

Boolean

archived

DateTime

created_at

DateTime

updated_at

ideas

String

id

🗝️

String

code

String

title

String

description

String

grill_md

String

plan_md

IdeaStatus

status

Boolean

archived

DateTime

created_at

DateTime

updated_at

idea_logs

String

id

🗝️

IdeaLogType

type

String

content

Json

metadata

DateTime

created_at

login_pairings

String

id

🗝️

String

secret_hash

String

desktop_token_hash

String

status

String

desktop_ua

String

desktop_ip

DateTime

created_at

DateTime

expires_at

DateTime

approved_at

DateTime

consumed_at

claude_questions

String

id

🗝️

String

question

Json

options

String

status

String

answer

DateTime

answered_at

DateTime

created_at

DateTime

expires_at

\ No newline at end of file +

active_product

user

enum:role

user

user

product

enum:status

pbi

product

sprint

assignee

enum:status

story

enum:type

enum:status

product

enum:status

story

product

sprint

enum:status

enum:verify_required

user

product

task

idea

enum:kind

enum:status

claimed_by_token

enum:verify_result

user

token

product

user

user

product

user

product

pbi

enum:status

idea

enum:type

enum:status

idea

user

story

task

idea

product

asker

answerer

Role

PRODUCT_OWNER

PRODUCT_OWNER

SCRUM_MASTER

SCRUM_MASTER

DEVELOPER

DEVELOPER

ADMIN

ADMIN

StoryStatus

OPEN

OPEN

IN_SPRINT

IN_SPRINT

DONE

DONE

PbiStatus

READY

READY

BLOCKED

BLOCKED

DONE

DONE

ClaudeJobStatus

QUEUED

QUEUED

CLAIMED

CLAIMED

RUNNING

RUNNING

DONE

DONE

FAILED

FAILED

CANCELLED

CANCELLED

VerifyResult

ALIGNED

ALIGNED

PARTIAL

PARTIAL

EMPTY

EMPTY

DIVERGENT

DIVERGENT

VerifyRequired

ALIGNED

ALIGNED

ALIGNED_OR_PARTIAL

ALIGNED_OR_PARTIAL

ANY

ANY

TaskStatus

TO_DO

TO_DO

IN_PROGRESS

IN_PROGRESS

REVIEW

REVIEW

DONE

DONE

LogType

IMPLEMENTATION_PLAN

IMPLEMENTATION_PLAN

TEST_RESULT

TEST_RESULT

COMMIT

COMMIT

TestStatus

PASSED

PASSED

FAILED

FAILED

SprintStatus

ACTIVE

ACTIVE

COMPLETED

COMPLETED

IdeaStatus

DRAFT

DRAFT

GRILLING

GRILLING

GRILL_FAILED

GRILL_FAILED

GRILLED

GRILLED

PLANNING

PLANNING

PLAN_FAILED

PLAN_FAILED

PLAN_READY

PLAN_READY

PLANNED

PLANNED

ClaudeJobKind

TASK_IMPLEMENTATION

TASK_IMPLEMENTATION

IDEA_GRILL

IDEA_GRILL

IDEA_MAKE_PLAN

IDEA_MAKE_PLAN

PLAN_CHAT

PLAN_CHAT

IdeaLogType

DECISION

DECISION

NOTE

NOTE

GRILL_RESULT

GRILL_RESULT

PLAN_RESULT

PLAN_RESULT

STATUS_CHANGE

STATUS_CHANGE

JOB_EVENT

JOB_EVENT

UserQuestionStatus

pending

pending

answered

answered

users

String

id

🗝️

String

username

String

email

String

password_hash

Boolean

is_demo

String

bio

String

bio_detail

Boolean

must_reset_password

Bytes

avatar_data

Int

idea_code_counter

DateTime

created_at

DateTime

updated_at

user_roles

String

id

🗝️

Role

role

api_tokens

String

id

🗝️

String

token_hash

String

label

DateTime

created_at

DateTime

revoked_at

products

String

id

🗝️

String

name

String

code

String

description

String

repo_url

String

definition_of_done

Boolean

auto_pr

Boolean

archived

DateTime

created_at

DateTime

updated_at

pbis

String

id

🗝️

String

code

String

title

String

description

Int

priority

Float

sort_order

PbiStatus

status

String

pr_url

DateTime

pr_merged_at

DateTime

created_at

DateTime

updated_at

stories

String

id

🗝️

String

code

String

title

String

description

String

acceptance_criteria

Int

priority

Float

sort_order

StoryStatus

status

DateTime

created_at

DateTime

updated_at

story_logs

String

id

🗝️

LogType

type

String

content

TestStatus

status

String

commit_hash

String

commit_message

Json

metadata

DateTime

created_at

sprints

String

id

🗝️

String

sprint_goal

SprintStatus

status

DateTime

start_date

DateTime

end_date

DateTime

created_at

DateTime

completed_at

tasks

String

id

🗝️

String

code

String

title

String

description

String

implementation_plan

Int

priority

Float

sort_order

TaskStatus

status

Boolean

verify_only

VerifyRequired

verify_required

String

repo_url

DateTime

created_at

DateTime

updated_at

claude_jobs

String

id

🗝️

ClaudeJobKind

kind

ClaudeJobStatus

status

DateTime

claimed_at

DateTime

started_at

DateTime

finished_at

DateTime

pushed_at

VerifyResult

verify_result

String

plan_snapshot

String

branch

String

pr_url

String

summary

String

error

Int

retry_count

DateTime

created_at

DateTime

updated_at

claude_workers

String

id

🗝️

String

product_id

DateTime

started_at

DateTime

last_seen_at

product_members

String

id

🗝️

DateTime

created_at

todos

String

id

🗝️

String

title

String

description

Boolean

done

Boolean

archived

DateTime

created_at

DateTime

updated_at

ideas

String

id

🗝️

String

code

String

title

String

description

String

grill_md

String

plan_md

IdeaStatus

status

Boolean

archived

DateTime

created_at

DateTime

updated_at

idea_logs

String

id

🗝️

IdeaLogType

type

String

content

Json

metadata

DateTime

created_at

user_questions

String

id

🗝️

String

user_id

String

question

String

answer

UserQuestionStatus

status

DateTime

created_at

DateTime

updated_at

login_pairings

String

id

🗝️

String

secret_hash

String

desktop_token_hash

String

status

String

desktop_ua

String

desktop_ip

DateTime

created_at

DateTime

expires_at

DateTime

approved_at

DateTime

consumed_at

claude_questions

String

id

🗝️

String

question

Json

options

String

status

String

answer

DateTime

answered_at

DateTime

created_at

DateTime

expires_at

\ No newline at end of file diff --git a/docs/plans/auto-pr-deploy-sync.md b/docs/plans/auto-pr-deploy-sync.md new file mode 100644 index 0000000..9375c46 --- /dev/null +++ b/docs/plans/auto-pr-deploy-sync.md @@ -0,0 +1,486 @@ +# Plan — Auto-PR + selectieve deploy-controle + sync-zicht (end-to-end batch flow) + +> Bij merge: dit plan verplaatsen naar `docs/plans/auto-pr-deploy-sync.md` +> conform feedback-memory (plans in `docs/plans/`). + +## Context + +Drie samenhangende problemen rond de "idee → uitvoeren"-keten: + +1. **Worker stopt bij `commit`.** De Scrum4Me NAS-worker werkt lokaal: + commits blijven op de machine staan totdat de gebruiker zelf pusht en + een PR aanmaakt. Voor batch-uitvoer van story-jobs is dit een harde + menselijke gate. +2. **Deploy is alles-of-niets.** `.github/workflows/ci.yml` deployt nu + **elke** push naar `main` automatisch naar productie en **elke** PR + naar preview. `vercel.json` heeft geen `git.deploymentEnabled: false`, + dus Vercel's eigen Git-integratie deployt waarschijnlijk parallel mee + → dubbele deploys en geen selectieve controle. +3. **Geen zicht op voortgang per Idea/PBI.** Concreet getest geval: + PBI-33 wordt nu de eerste sprint-batch — er is **geen git-voetafdruk** + (geen branch/commit/PR met "PBI-33"), **geen activiteitenlog-entry**, + en geen UI-pagina die per Story toont of er een ClaudeJob loopt, een + commit gepusht is, of een PR open/merged is. De data zit in + `Story.status`, `ClaudeJob.pushed_at/branch/pr_url`, + `Pbi.pr_url/pr_merged_at` — er is alleen geen view die het joint. + +Doel: de complete keten **plan → job → commit → push → PR → auto-merge → +deploy** in één coherent ontwerp leggen, met (a) selectieve +deploy-controle als veiligheidsklep en (b) een sync-tab die per Idea +laat zien wat er werkelijk in git/PR-land gebeurd is. + +## Vastgelegde keuzes + +### Deploy-controle +1. **Mechanisme**: PR-labels (B) + path-filter (C) gecombineerd. +2. **Eigenaar**: GitHub Actions-workflow (A). Vercel Git-integratie uit. +3. **Defaults**: PR → preview, push naar `main` → productie. +4. **Override-richtingen**: + - `skip-deploy` label: voorkomt preview-deploy op een PR. + - `force-deploy` label: forceert deploy ook als path-filter doc-only + zegt. + +### Auto-PR (uit IDEA-007-grill) +5. **Triggers in worker**: na elke succesvolle `update_job_status('done')` + pusht de worker; na laatste story van een PBI maakt de worker een PR + aan en activeert auto-merge (SQUASH). +6. **Auth**: `GITHUB_TOKEN` als omgevingsvariabele op de worker; geen UI + of GitHub App in v1. +7. **Foutafhandeling**: push/PR-aanmaak-fail → `update_job_status('failed', + error: …)`; geen force-push, geen automatische retry. + +### Interactie tussen beide +8. **Worker-PRs gebruiken hetzelfde labelsysteem als alle andere PRs.** + Default = preview deploy, auto-merge wacht op CI groen, na merge + prod-deploy (mits path-filter zegt "code"). De worker zet **geen** + labels automatisch — als je batch-output zonder preview wilt mergen + moet je `skip-deploy` zelf toevoegen, of preview later uitzetten via + een product-instelling (out-of-scope v1). +9. **Implementatievolgorde**: eerst deploy-controle (infra, + onafhankelijk), daarna auto-PR (afhankelijk van stabiele deploy-flow). + +## Architectuur in één plaat + +``` + auto-merge wacht op +[story-job DONE] ─push branch─┐ deploy-preview groen + ▼ │ +[laatste story?]──ja──[PR + auto-merge]──CI──┴──merge naar main + │ + [job: ci] altijd + │ + [paths-filter] + │ + ├ PR → deploy-preview + │ if code && !skip-deploy + │ || force-deploy + │ + └ push → deploy-production + if code +``` + +--- + +## Deel A — Deploy-controle + +### A.1 `vercel.json` — Vercel Git-deploy uitzetten + +```json +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "git": { "deploymentEnabled": false }, + "crons": [ + { "path": "/api/cron/expire-questions", "schedule": "0 4 * * *" }, + { "path": "/api/cron/cleanup-agent-artifacts", "schedule": "0 3 * * *" } + ] +} +``` + +Effect: Vercel deployt niet meer automatisch op git-events. Alleen +`vercel deploy` vanuit de workflow (met `VERCEL_TOKEN`) maakt nog +deployments. + +### A.2 `.github/workflows/ci.yml` — path-filter + label-checks + +Triggers uitbreiden met `workflow_dispatch`: + +```yaml +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + inputs: + target: + type: choice + description: Deploy target + options: [preview, production] + default: preview +``` + +Nieuwe job vóór de deploy-jobs: + +```yaml + changes: + name: Detect deploy-relevant changes + runs-on: ubuntu-latest + needs: ci + outputs: + code: ${{ steps.filter.outputs.code }} + steps: + - uses: actions/checkout@v5 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + code: + - 'app/**' + - 'components/**' + - 'lib/**' + - 'actions/**' + - 'stores/**' + - 'prisma/**' + - 'public/**' + - 'package.json' + - 'package-lock.json' + - 'next.config.ts' + - 'tsconfig.json' + - 'vercel.json' + - 'proxy.ts' + - 'middleware.ts' + - '.github/workflows/**' +``` + +`deploy-preview` if-conditie aanpassen: + +```yaml + deploy-preview: + needs: [ci, changes] + if: | + github.event_name == 'pull_request' && ( + (needs.changes.outputs.code == 'true' + && !contains(github.event.pull_request.labels.*.name, 'skip-deploy')) + || contains(github.event.pull_request.labels.*.name, 'force-deploy') + ) +``` + +`deploy-production` if-conditie aanpassen: + +```yaml + deploy-production: + needs: [ci, changes] + if: | + github.ref == 'refs/heads/main' + && github.event_name == 'push' + && needs.changes.outputs.code == 'true' +``` + +Nieuwe `deploy-manual` job voor `workflow_dispatch` met `inputs.target` +→ `vercel deploy` of `vercel deploy --prod`. + +### A.3 GitHub-labels aanmaken + +```bash +gh label create skip-deploy --color BFBFBF --description "Preview-deploy overslaan" +gh label create force-deploy --color 0E8A16 --description "Forceer deploy ondanks path-filter" +``` + +### A.4 Documentatie + +`docs/runbooks/deploy-control.md` — triggers, labels, path-filter, +voorbeelden. `CLAUDE.md` § Deployment-regel verwijst naar runbook. + +--- + +## Deel B — Auto-PR (worker → GitHub) + +### B.1 Acceptatiecriteria (uit IDEA-007) + +- **AC 1 — Push per story**: Na succesvolle `update_job_status('done')` + pusht de worker via HTTPS (`https://$GITHUB_TOKEN@github.com/…`) naar + origin. Push-timestamp via nieuwe MCP-call in `ClaudeJob.pushed_at`. +- **AC 2 — Detectie laatste story**: Nieuwe MCP-call `check_pbi_complete` + retourneert `{ complete: boolean, pbi_id }`. +- **AC 3 — PR aanmaken**: Op `complete: true` POST naar + `/repos/{owner}/{repo}/pulls`; titel/body uit PBI-naam + voltooide + stories; PR-URL via `set_pbi_pr`. +- **AC 4 — Auto-merge activeren**: Direct na PR-aanmaak GraphQL + `enablePullRequestAutoMerge` (SQUASH). +- **AC 5 — Foutafhandeling**: push/PR-fail → + `update_job_status('failed', error)`; PR-URL blijft bewaard voor + handmatige inspectie. + +### B.2 Server-side wijzigingen (Scrum4Me-repo) + +Velden bestaan al in schema: +- `Product.auto_pr Boolean @default(false)` (regel 176) +- `Pbi.pr_url String?` + `Pbi.pr_merged_at DateTime?` (regel 207–208) +- `ClaudeJob.pushed_at DateTime?` + `ClaudeJob.pr_url String?` + + `ClaudeJob.branch String?` (regel 335, 338, 339) + +Geen migratie nodig. + +Server actions / REST: bestaande `set_pbi_pr` en `mark_pbi_pr_merged` +MCP-tools blijven. Nieuwe action: +- `actions/jobs.ts` → `recordJobPushedAtAction(jobId)` voor + `pushed_at`-write (als die nog niet via MCP gaat). + +### B.3 MCP-laag (`scrum4me-mcp`-repo) + +Nieuwe tool: +- `check_pbi_complete(pbi_id) → { complete: boolean, pbi_id }`. Leest + alle ClaudeJobs gelinkt aan PBI; aggregeert status. `complete = true` + als **alle** story-jobs status DONE hebben. + +Uitbreiding bestaande tool: +- `update_job_status`: bij `status: 'done'` ook `pushed_at` accepteren + (worker geeft timestamp door). +- `set_pbi_pr`: ongewijzigd, bestaat al. + +Schema-drift watchdog (`docs/runbooks/mcp-integration.md`) moet groen +voor merge. + +### B.4 Worker-laag (lokaal Claude-CLI worker) + +Nieuwe stappen na elke story: + +``` +1. update_job_status('done', pushed_at: null) ← bestaand +2. git push https://$GITHUB_TOKEN@github.com/$OWNER/$REPO.git $BRANCH +3. record_pushed_at(job_id, now) ← nieuwe MCP-call +4. { complete } = check_pbi_complete(pbi_id) +5. if complete: + prNumber = POST /repos/.../pulls + set_pbi_pr(pbi_id, pr_url) + enablePullRequestAutoMerge(prNumber, MERGE_METHOD: SQUASH) +6. on any HTTP/git failure → update_job_status('failed', error) +``` + +GITHUB_TOKEN-scope: `repo` voor private, `public_repo` voor public. +Documenteer in worker-readme. + +### B.5 Repo-instellingen (handmatig, one-time) + +- GitHub repo Settings → General → "Allow auto-merge" → **aanvinken**. +- Branch protection op `main`: required CI checks = `ci`, + `deploy-preview` is **niet** required (kan skipped zijn door label). + +--- + +## Deel C — Interactie & demo-policy + +### C.1 Interactie deploy-controle ↔ auto-PR + +| Scenario | Preview-deploy | Prod-deploy bij merge | +|--------------------------------------------------|----------------|------------------------| +| Worker maakt PR met code-changes (default) | ✅ runt | ✅ runt | +| Worker maakt PR met `skip-deploy` (manueel toegevoegd) | ❌ skipped | ✅ runt | +| Worker maakt PR met enkel docs-changes (path-filter) | ❌ skipped | ❌ skipped | +| User voegt `force-deploy` toe aan doc-only PR | ✅ runt | ✅ runt (path-filter) of ❌ (doc-only push) | + +Auto-merge wacht op required CI checks. `deploy-preview` mag skipped +zijn — branch protection markeert hem niet als required. + +### C.2 Demo-policy + +Auto-PR-flow draait op de worker, niet vanuit de webapp. Geen +demo-sessie kan deze code triggeren — geen extra proxy.ts of +`session.isDemo`-guards nodig. Wel: `check_pbi_complete` MCP-call moet +`requireWriteAccess` doen (consistent met andere write-MCP-tools), zodat +demo-tokens hem niet kunnen aanroepen. + +--- + +--- + +## Deel D — Sync-tab op Idea-detail (zicht op voortgang) + +### D.1 Wat bestaat al + +- `model StoryLog` (`prisma/schema.prisma:251`) met types + `IMPLEMENTATION_PLAN | TEST_RESULT | COMMIT`, plus `commit_hash`, + `commit_message`, `metadata`. **Dit is de activiteitenlog.** +- MCP-tools `log_implementation`, `log_commit`, `log_test_result` + schrijven naar deze tabel. +- UI-component `components/shared/story-log.tsx` rendert + `StoryLogEntry[]` met type-styling. +- `Story.status`, `ClaudeJob.pushed_at/branch/pr_url`, + `Pbi.pr_url/pr_merged_at` zijn al gevuld door bestaande flows. + +Geen nieuwe tabellen, geen migraties. + +### D.2 Nieuwe tab op `/ideas/[id]` + +Voeg vijfde tab **Sync** toe (naast Idee · Grill · Plan · Timeline) op +Idea-detail-page. Alleen zichtbaar als `Idea.status === 'PLANNED'` en +`pbi_id` gevuld. + +Layout per tab-content: +- Header: PBI-link + `pr_url` + `pr_merged_at` als badge. +- Per Story (volgorde uit PBI): collapsible card met: + - **Story-header**: code · titel · status-badge. + - **Job-rij**: voor elke `ClaudeJob` (kind=TASK_IMPLEMENTATION) gelinkt + aan een Task van deze Story → status, `branch`, `pushed_at`, + `pr_url`. Toont "geen job" als nog niets gequeued. + - **Activity-log**: `` + — bestaande component, ongewijzigd. + +### D.3 Server-laag + +Nieuwe loader in `app/(app)/ideas/[id]/page.tsx` (of nieuw +`sync-tab-server.ts`): + +```ts +async function loadIdeaSyncData(ideaId: string, userId: string) { + // Auth-scope: idea.user_id === userId (M12-keuze 2) + return prisma.idea.findFirst({ + where: { id: ideaId, user_id: userId }, + include: { + pbi: { + include: { + stories: { + orderBy: { sort_order: 'asc' }, + include: { + tasks: { include: { claude_jobs: true } }, + logs: { orderBy: { created_at: 'desc' } }, + }, + }, + }, + }, + }, + }) +} +``` + +Server-only. Nooit importeren in client component (zie hardstop +`*-server.ts` regel). + +### D.4 Realtime refresh + +Sync-tab abonneert op bestaande SSE-streams: +- `app/api/realtime/solo/route.ts` — `JobPayload` voor job-status-updates + (al uitgebreid met `kind` en `idea_id` per Deel B). +- `app/api/realtime/notifications/route.ts` — voor StoryLog-inserts; als + story_logs nog geen pg_notify-trigger heeft, voeg er een toe (nieuwe + migratie, payload `{op: 'INSERT', entity: 'story_log', id, story_id}`). + +Op event → `router.refresh()` of `revalidate` van Sync-tab data. + +### D.5 PBI-33 als live testgeval + +PBI-33 is **nu** in TODO + gequeued als ClaudeJobs (gebruiker bevestigt: +"taken op TODO gezet en claude-job aangemaakt"). Verwacht gedrag zodra +deze sprint live is: + +| Moment | Sync-tab toont | +|----------------------------|-----------------------------------------------| +| Job QUEUED | "Wachtend op worker" | +| Job RUNNING | Status RUNNING + log-entry IMPLEMENTATION_PLAN| +| Worker commit | log-entry COMMIT (hash + message) | +| Worker test | log-entry TEST_RESULT (status) | +| Worker push (Deel B AC 1) | `branch` + `pushed_at` zichtbaar | +| Laatste story → PR | PBI.`pr_url` zichtbaar | +| Auto-merge | PBI.`pr_merged_at` zichtbaar | + +Als één van deze niet verschijnt: bug in MCP-tool of worker (niet in +sync-tab zelf). + +--- + +## Bestanden + +| Wijziging | Pad | +|-------------------|--------------------------------------------------| +| Edit | `vercel.json` | +| Edit | `.github/workflows/ci.yml` | +| Nieuw | `docs/runbooks/deploy-control.md` | +| Edit | `CLAUDE.md` (verwijzing toevoegen) | +| Nieuw (mcp-repo) | `src/tools/check-pbi-complete.ts` | +| Edit (mcp-repo) | `src/tools/update-job-status.ts` (pushed_at) | +| Edit | `actions/jobs.ts` (optioneel: record-pushed-at) | +| Edit | Worker-script (post-story-hook + PR-aanmaak) | +| Doc | `docs/runbooks/auto-pr-flow.md` (worker-flow) | +| Nieuw | `app/(app)/ideas/[id]/sync-tab-server.ts` | +| Nieuw | `components/ideas/idea-sync-tab.tsx` | +| Edit | `app/(app)/ideas/[id]/page.tsx` (5e tab toevoegen) | +| Migratie | `prisma/migrations/_story_logs_notify/migration.sql` (pg_notify-trigger op story_logs) | +| Edit | `app/api/realtime/notifications/route.ts` (story_log-payload doorlaten) | +| GitHub (extern) | Labels `skip-deploy`, `force-deploy` aanmaken | +| GitHub (extern) | Repo Settings → "Allow auto-merge" aan | +| Vercel-dashboard | `git.deploymentEnabled: false` actief verifiëren | + +## Implementatievolgorde + +1. **Deel A — Deploy-controle** + 1. `vercel.json` aanpassen + 2. `ci.yml` uitbreiden (path-filter, labels, dispatch) + 3. Labels op GitHub aanmaken + 4. Runbook + CLAUDE.md-verwijzing + 5. Test-PR voor elk scenario (zie Verificatie) + +2. **Deel D — Sync-tab** (kan parallel met B; alleen DB-reads + UI) + 1. `loadIdeaSyncData` server-loader + 2. `idea-sync-tab.tsx` component met ``-hergebruik + 3. 5e tab in `app/(app)/ideas/[id]/page.tsx` + 4. pg_notify-trigger op `story_logs` + SSE-route uitbreiden + 5. **Live test op PBI-33** (sprint loopt al — check of activity + verschijnt zodra worker logs schrijft) + +3. **Deel B — Auto-PR** + 1. MCP `check_pbi_complete` + `update_job_status(pushed_at)` PR + (parallel-repo, schema-drift-watchdog groen) + 2. Worker-hook: push na done, PR + auto-merge bij complete + 3. Repo-instelling "Allow auto-merge" aan + 4. End-to-end smoke met één test-PBI + +## Verificatie + +Lokaal: + +```bash +npm run lint && npm test && npm run build +``` + +Workflow-syntax: + +```bash +gh workflow view ci.yml +``` + +End-to-end deploy-controle: + +1. **Doc-only PR** → `deploy-preview` skipped. +2. **Doc-only PR + `force-deploy`** → `deploy-preview` runt. +3. **Code-PR + `skip-deploy`** → `deploy-preview` skipped. +4. **Code-PR zonder labels** → `deploy-preview` runt. +5. **Push naar `main` met code-change** → `deploy-production` runt. +6. **Push naar `main` doc-only** → `deploy-production` skipped. +7. **`workflow_dispatch` target=production** → manuele prod. +8. **Vercel dashboard** → geen auto-deploy bij geforceerde test-push. + +End-to-end auto-PR: + +9. Maak een test-PBI met 1 story + 1 task. +10. Worker draait → na `done`: `pushed_at` gevuld, branch op origin + zichtbaar. +11. `check_pbi_complete` → `complete: true`. +12. PR verschijnt op GitHub met titel = PBI-naam, body = story-list. +13. Auto-merge actief; CI groen → squash-merge. +14. `mark_pbi_pr_merged` getriggerd door `pull_request: closed`-webhook + (al bestaand) → `Pbi.pr_merged_at` gevuld. +15. Push-event op `main` → `deploy-production` runt (path-filter ja). +16. **Failure-test**: revoke GITHUB_TOKEN tijdelijk → push faalt → + `update_job_status('failed')` met error; geen PR aangemaakt. + +## Out-of-scope (v1) + +- UI-toggle voor `auto_pr` per product (veld bestaat, geen UI-wiring). +- GitHub App-installatie (per-repo tokens, scopes-finetuning). +- Multi-repo PBI's (huidig ontwerp: één `repo_url` per PBI). +- Force-push / non-fast-forward retry-flow. +- Notificaties (Slack, e-mail) bij merge of CI-failure. +- Rollback-flow bij gemergende regressie. +- Migratie naar `vercel.ts` (knowledge-update beveelt het aan; later). +- Auto-skip preview-deploy specifiek voor worker-PRs op basis van + product-instelling. diff --git a/lib/insights/agent-throughput.ts b/lib/insights/agent-throughput.ts index ecc97ac..2a44340 100644 --- a/lib/insights/agent-throughput.ts +++ b/lib/insights/agent-throughput.ts @@ -59,7 +59,7 @@ export async function getJobsPerDay( SELECT COUNT(*) FILTER (WHERE DATE(created_at) = CURRENT_DATE) AS today_count, COUNT(*) FILTER (WHERE status = 'DONE' AND created_at > NOW() - INTERVAL '7 days') AS done_7d, - COUNT(*) FILTER (WHERE status IN ('DONE','FAILED','CANCELLED') AND created_at > NOW() - INTERVAL '7 days') AS terminal_7d, + COUNT(*) FILTER (WHERE status IN ('DONE','FAILED','CANCELLED','SKIPPED') AND created_at > NOW() - INTERVAL '7 days') AS terminal_7d, AVG(EXTRACT(EPOCH FROM (finished_at - claimed_at))) FILTER (WHERE status = 'DONE' AND created_at > NOW() - INTERVAL '7 days') AS avg_seconds FROM claude_jobs WHERE user_id = ${userId} @@ -69,7 +69,7 @@ export async function getJobsPerDay( SELECT COUNT(*) FILTER (WHERE DATE(created_at) = CURRENT_DATE) AS today_count, COUNT(*) FILTER (WHERE status = 'DONE' AND created_at > NOW() - INTERVAL '7 days') AS done_7d, - COUNT(*) FILTER (WHERE status IN ('DONE','FAILED','CANCELLED') AND created_at > NOW() - INTERVAL '7 days') AS terminal_7d, + COUNT(*) FILTER (WHERE status IN ('DONE','FAILED','CANCELLED','SKIPPED') AND created_at > NOW() - INTERVAL '7 days') AS terminal_7d, AVG(EXTRACT(EPOCH FROM (finished_at - claimed_at))) FILTER (WHERE status = 'DONE' AND created_at > NOW() - INTERVAL '7 days') AS avg_seconds FROM claude_jobs WHERE user_id = ${userId} diff --git a/lib/job-status.ts b/lib/job-status.ts index f6ac4ee..43bb498 100644 --- a/lib/job-status.ts +++ b/lib/job-status.ts @@ -7,6 +7,7 @@ const JOB_DB_TO_API = { DONE: 'done', FAILED: 'failed', CANCELLED: 'cancelled', + SKIPPED: 'skipped', } as const satisfies Record const JOB_API_TO_DB: Record = { @@ -16,6 +17,7 @@ const JOB_API_TO_DB: Record = { done: 'DONE', failed: 'FAILED', cancelled: 'CANCELLED', + skipped: 'SKIPPED', } export type ClaudeJobStatusApi = typeof JOB_DB_TO_API[ClaudeJobStatus] From 273735384a8c00aeb59917769a48bb5b2fe57277 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Tue, 5 May 2026 23:10:48 +0200 Subject: [PATCH 3/5] feat(T-573): SKIPPED-badge styling in admin jobs-table Italic, neutrale grijstint die SKIPPED visueel onderscheidt van CANCELLED (zelfde grijstint, maar non-italic) en van FAILED (rood). Co-Authored-By: Claude Opus 4.7 (1M context) --- components/admin/jobs-table.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/components/admin/jobs-table.tsx b/components/admin/jobs-table.tsx index faca242..a241549 100644 --- a/components/admin/jobs-table.tsx +++ b/components/admin/jobs-table.tsx @@ -32,6 +32,7 @@ const STATUS_CLASS: Record = { DONE: 'bg-status-done text-white border-transparent', FAILED: 'bg-priority-high text-white border-transparent', CANCELLED: 'bg-muted text-muted-foreground', + SKIPPED: 'bg-muted/60 text-muted-foreground italic border-transparent', } const KIND_LABEL: Record = { From ca1a89ca041937d7bb27aa29f2c4f25b8c6cbec8 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Tue, 5 May 2026 23:12:11 +0200 Subject: [PATCH 4/5] test(T-574): cron-cleanup test verwacht SKIPPED in deleteMany filter Bijgewerkt na uitbreiding van cleanup-criteria met SKIPPED-jobs in T-572. Co-Authored-By: Claude Opus 4.7 (1M context) --- __tests__/api/cron-cleanup-agent-artifacts.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__tests__/api/cron-cleanup-agent-artifacts.test.ts b/__tests__/api/cron-cleanup-agent-artifacts.test.ts index 188c558..bd86923 100644 --- a/__tests__/api/cron-cleanup-agent-artifacts.test.ts +++ b/__tests__/api/cron-cleanup-agent-artifacts.test.ts @@ -41,7 +41,7 @@ describe('POST /api/cron/cleanup-agent-artifacts', () => { expect(mockPrisma.claudeJob.deleteMany).not.toHaveBeenCalled() }) - it('200 met juiste secret + deleteMany aangeroepen voor FAILED/CANCELLED ouder dan 7 dagen', async () => { + it('200 met juiste secret + deleteMany aangeroepen voor FAILED/CANCELLED/SKIPPED ouder dan 7 dagen', async () => { mockPrisma.claudeJob.deleteMany.mockResolvedValue({ count: 5 }) const res = await POST(makeReq({ authorization: 'Bearer ' + SECRET })) @@ -51,7 +51,7 @@ describe('POST /api/cron/cleanup-agent-artifacts', () => { expect(body.ran_at).toMatch(/^\d{4}-\d{2}-\d{2}T/) const arg = mockPrisma.claudeJob.deleteMany.mock.calls[0][0] - expect(arg.where.status).toEqual({ in: ['FAILED', 'CANCELLED'] }) + expect(arg.where.status).toEqual({ in: ['FAILED', 'CANCELLED', 'SKIPPED'] }) expect(arg.where.finished_at.lt).toBeInstanceOf(Date) // cutoff should be approximately 7 days ago From 084ca810907948e9233b344da67136edfe70a9ff Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Tue, 5 May 2026 23:13:49 +0200 Subject: [PATCH 5/5] docs(T-575): worker-idempotency runbook + CLAUDE.md verwijzing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Beschrijft beslissingsboom verify_result × diff-staat × branch-staat → JobStatus, met SKIPPED gereserveerd voor al-gemergd werk en FAILED voor échte fouten. Plus StoryLog-verplichting (log_implementation, log_commit, log_test_result) en idempotency-protocol vóór schrijven. PBI-33 batch (5-5 22:22) gedocumenteerd als case-study: drie protocol-overtredingen die deze runbook + de nieuwe SKIPPED-status aanpakken. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 2 + docs/INDEX.md | 1 + docs/runbooks/worker-idempotency.md | 122 ++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 docs/runbooks/worker-idempotency.md diff --git a/CLAUDE.md b/CLAUDE.md index 566c755..0b85bf0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -115,3 +115,5 @@ PBI (niet: Feature/Epic) · Story (niet: Ticket) · Sprint Goal (niet: Objective ```bash npm run lint && npm test && npm run build ``` + +Worker job-status protocol (wanneer `DONE` / `SKIPPED` / `FAILED`): zie [docs/runbooks/worker-idempotency.md](./docs/runbooks/worker-idempotency.md). diff --git a/docs/INDEX.md b/docs/INDEX.md index 78e2edf..e575498 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -114,6 +114,7 @@ Auto-generated on 2026-05-05 from front-matter and headings. | [Vercel Deployment](./runbooks/deploy-vercel.md) | `runbooks/deploy-vercel.md` | active | 2026-05-03 | | [MCP Integration — Scrum4Me Tools](./runbooks/mcp-integration.md) | `runbooks/mcp-integration.md` | active | 2026-05-03 | | [v1.0 Smoke Test Checklist](./runbooks/v1-smoke-test.md) | `runbooks/v1-smoke-test.md` | active | 2026-05-04 | +| [Worker idempotency & job-status protocol](./runbooks/worker-idempotency.md) | `runbooks/worker-idempotency.md` | active | 2026-05-05 | | [StoryDialog Profiel](./story-dialog.md) | `story-dialog.md` | active | 2026-05-03 | | [TaskDialog Profiel](./task-dialog.md) | `task-dialog.md` | active | 2026-05-03 | | [Scrum4Me — API Test Plan](./test-plan.md) | `test-plan.md` | active | 2026-05-03 | diff --git a/docs/runbooks/worker-idempotency.md b/docs/runbooks/worker-idempotency.md new file mode 100644 index 0000000..38e023b --- /dev/null +++ b/docs/runbooks/worker-idempotency.md @@ -0,0 +1,122 @@ +--- +title: "Worker idempotency & job-status protocol" +status: active +audience: [ai-agent, contributor] +language: nl +last_updated: 2026-05-05 +when_to_read: "Vóór het implementeren of debuggen van Claude-CLI-worker logica die `update_job_status` aanroept." +--- + +# Worker idempotency & job-status protocol + +Beschrijft hoe de Scrum4Me-worker `ClaudeJob.status` moet zetten op basis +van `VerifyResult` × git-diff-staat × branch-staat. Doel: voorkom +status-divergentie zoals geconstateerd in de **PBI-33 batch (5-5-2026 +22:22)** waarin werk dat al gemerged was via PR #102/#103/#104 leidde +tot inconsistente combinaties van `verify=EMPTY → FAILED` en +`verify=DIVERGENT → DONE`. + +--- + +## Beslissingsboom + +Aan het einde van een story-job, ná `verify`-pass: + +| `verify_result` | netto diff t.o.v. `origin/main` | branch al gemerged | → `ClaudeJob.status` | `Task.status` | +|---|---|---|---|---| +| `ALIGNED` of `PARTIAL` | nieuwe commit aanwezig | n.v.t. | **`DONE`** | `DONE` | +| `EMPTY` | leeg (niets gewijzigd) | werk zit al op `origin/main` | **`SKIPPED`** | `DONE` | +| `EMPTY` | leeg, maar werk staat **niet** op origin | n.v.t. | **`FAILED`** (`error: "verify produced no output"`) | `IN_PROGRESS` (handmatig onderzoeken) | +| `DIVERGENT` | aanwezig, maar identiek aan al-gemergde branch | ja (PR closed/merged) | **`SKIPPED`** | `DONE` | +| `DIVERGENT` | aanwezig, niet matchend met main | nee | **`FAILED`** (`error: "verify divergent — handmatige review"`) | `IN_PROGRESS` | +| (compile-fail, test-fail, push-fail, exception) | n.v.t. | n.v.t. | **`FAILED`** met concrete `error` | `IN_PROGRESS` | +| (gebruiker drukt cancel) | n.v.t. | n.v.t. | **`CANCELLED`** | `TO_DO` | + +### Vuistregels + +- **`SKIPPED`** = "geen netto-output, maar geen fout" — werk was al + gedaan vóór deze job draaide. Task mag op `DONE` omdat het beoogde + resultaat in main aanwezig is. +- **`FAILED`** is gereserveerd voor échte fouten: code-fouten, + test-failures, push-fouten, onverklaarde diff. Niet voor + "implementatie was al gedaan". +- **`DONE`** alleen bij `ALIGNED`/`PARTIAL` mét nieuwe commit op de + feature-branch. Een lege `DIVERGENT` op een al-gemergde branch is + géén `DONE`. + +--- + +## StoryLog-verplichting + +Tijdens elke job moet de worker `story_logs`-entries schrijven via de +MCP-tools, anders is de Sync-tab leeg: + +| Wanneer | MCP-tool | Inhoud | +|---|---|---| +| Bij claim | `log_implementation` | "Start implementatie van T-XXX. Branch X. Plan: …" | +| Per commit | `log_commit` | hash + message + samenvatting van wijzigingen | +| Na verify | `log_test_result` | status `PASSED` of `FAILED` + samenvatting van checks | + +In **PBI-33 batch** zijn deze tools **niet** aangeroepen — `story_logs` +voor ST-1208/1209/1210 is leeg. Worker MAG geen job afronden zonder +minimaal één `log_implementation` (start) en één `log_test_result` +(eind). + +--- + +## Idempotency-protocol (vóór schrijven) + +Bij claim van een job: + +1. Lees `Task.implementation_plan` — beschrijft expliciet welke files + gewijzigd moeten worden. +2. Vergelijk de huidige `origin/main`-staat met die plan-instructies: + - Bestaat het bestand al met de beoogde inhoud? + - Bestaat de migratie al? + - Bevat de relevante codepad de nieuwe symbolen/types? +3. Bij **volledige hit**: roep `log_implementation` met inhoud "Werk + reeds aanwezig op origin/main vanaf commit X (Y)." Sla + verify-stap over en zet `JobStatus.SKIPPED`. Task naar `DONE`. +4. Bij **gedeeltelijke hit**: log de bevindingen via + `log_implementation` en doe alleen het resterende werk. Eindig met + `DONE` (`ALIGNED` of `PARTIAL`) als je netto-output hebt. + +Dit voorkomt dubbele commits op al-gemergde branches en houdt +`pushed_at` semantisch correct (alleen gevuld als er werkelijk +gepusht is). + +--- + +## Case-study: PBI-33 (5-5-2026 22:22) + +PBI-33 ("PLAN_CHAT — gebruikersvragen over plan") werd opnieuw aangemaakt +nadat de feature al via een eerdere batch was gemerged onder cuid-style +story-codes (`ST-bsjoqjnr`, `ST-p6d1odh0`, …). De worker draaide om +22:22 en zag: + +- **T-533** (`ST-1208` schema-werk): diff = leeg → `verify=EMPTY` → + `Job.FAILED` met error "Implementatie reeds voltooid en gemerged". + Volgens het nieuwe protocol had dit **`SKIPPED`** moeten zijn. +- **T-534…538**: diff niet leeg op feature-branches `feat/story-7pl4dsb6` + en `feat/story-0vtnydpi` (al-gemergde branches uit eerdere PR's) → + `verify=DIVERGENT` → `Job.DONE` met `pushed_at=now()`. Volgens het + nieuwe protocol had dit ook **`SKIPPED`** moeten zijn — branch was + al closed/merged, geen nieuwe commit. +- **`story_logs` voor ST-1208/1209/1210 is leeg** — geen + `log_implementation`, geen `log_commit`, geen `log_test_result`. + +Drie protocol-overtredingen die we met deze runbook + de nieuwe +`SKIPPED`-status aanpakken. + +--- + +## Referenties + +- Enum: `prisma/schema.prisma` → `enum ClaudeJobStatus` +- Mapping: `lib/job-status.ts` (DB↔API) en + `components/shared/job-status.ts` (label + kleur) +- Status-data-cleanup: `app/api/cron/cleanup-agent-artifacts/route.ts` +- KPI-aggregatie: `lib/insights/agent-throughput.ts` (terminal_7d + inclusief SKIPPED) +- Gerelateerd plan: `docs/plans/auto-pr-deploy-sync.md` Deel D + (Sync-tab toont per-Story job-status incl. SKIPPED)