Previously, if the ClaudeWorker record vanished (deleted by
prisma_workers_cleanup, manual cleanup, or a race during shutdown of a
parallel worker), the heartbeat would log a warning and stop itself
permanently. From that moment the NavBar showed 'Geen agent' for the
rest of the MCP-server process lifetime — even though the agent was
still alive and serving tools.
Fix: on result.count === 0, call registerWorker again so the record is
re-created. Heartbeat keeps ticking. Self-healing instead of self-
terminating.
startHeartbeat now also accepts userId (needed for re-registration);
caller in index.ts updated.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
server.connect(transport) on the stdio transport awaits the first MCP
handshake from the client. If that handshake stalls (or the await keeps
the process pinned to the stdio event loop), the lines that follow
never run — registerWorker / startHeartbeat / shutdown-handlers are
silently skipped.
Symptom: NavBar shows 'Geen agent' while jobs are claiming and running
(observed in production after the M13 worker-presence release).
ClaudeWorker count stays at 0 even though tools are responding.
Fix: do the presence bootstrap before opening the transport. Tools are
already registered at this point — connecting the transport just makes
them reachable. Delaying the connect by ~10ms (one DB upsert + one
pg_notify) is harmless to the client handshake.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implementeert vier open stories uit PBI 'Veilige Claude-agent-workflow':
**Branch per story (cmon11tbe001zbortx35n155c)**
- `resolveBranchForJob`: zoek sibling-job in dezelfde story; reuse z'n
branch (1 PR per story i.p.v. per task).
- Branch-naam: `feat/story-<8-char>` voor nieuwe stories.
- `createWorktreeForJob` kent nu `reuseBranch=true`: detecteert stale
sibling-worktree die de branch nog vasthoudt en verwijdert die eerst.
- `attachWorktreeToJob` neemt `storyId` mee.
**PR-hergebruik (zelfde story)**
- `maybeCreateAutoPr`: als sibling-job in story al een pr_url heeft,
hergebruik die zonder nieuwe `gh pr create`-call. PR-titel komt nu
van de story (was task) zodat het als 'story-PR' leest.
**Worktree-cleanup uitgesteld bij actieve siblings**
- `cleanupWorktreeForTerminalStatus`: count active sibling-jobs in
dezelfde story; defer als > 0 (volgende sub-task gebruikt branch).
**Worktree-cleanup logging (cmon0jc14001ubortjxf2a2ck)**
- Warning bij ontbrekende repoRoot, met productId + jobId in message.
- Warning bij removeWorktreeForJob-failure met keepBranch in message.
**resolveRepoRoot fallback (cmon0jc14001ubortjxf2a2ck)**
- Convention-based fallback: `~/Projects/<repo-name>` afgeleid uit
`product.repo_url` als noch env-var noch config-bestand iets oplevert.
- `repoNameFromUrl` helper geëxporteerd voor herbruikbaarheid.
**Verify EMPTY-detection edge-case (cmon0kdq6001xbort2kgbcqmr)**
- `classifyDiffAgainstPlan`: na file-paths-check ook content-lines
checken; als alle +/- regels alleen headers of whitespace zijn,
return EMPTY met duidelijke reasoning.
Tests: 120/120 groen (3 nieuwe), tsc clean, build clean.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New src/git/pr.ts helper wraps 'gh pr create'; returns { url } or { error }.
maybeCreateAutoPr() in update-job-status checks product.auto_pr, builds title
from story.code + task.title, writes pr_url to DB. Non-fatal: gh failure logs
a warning and leaves DONE status intact. Also syncs schema: auto_pr on Product,
pr_url on ClaudeJob.
resetStaleClaimedJobs now uses $queryRaw with RETURNING so it can send pg_notify
claude_job_status events per transitioned job. Jobs under the retry limit are
re-queued with retry_count incremented; jobs at ≥2 retries are marked FAILED.
Runs git push -u origin <branch> in the worktree. Detects no-changes
(HEAD = origin/main) before pushing. Classifies push failures into
no-credentials, conflict, or unknown via stderr pattern matching.
5 unit tests covering all paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On DONE/FAILED, resolves repoRoot and calls removeWorktreeForJob (best-effort).
keepBranch=true when status=done and agent reported a branch (push assumed);
false otherwise. Cleanup failures are logged as warnings — DB status is preserved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After claiming a job, resolves repoRoot (env SCRUM4ME_REPO_ROOT_<productId>
or ~/.scrum4me-agent-config.json), creates a git worktree, and returns
worktree_path + branch_name in the response. Rolls back claim to QUEUED
on failure. Tool description updated to instruct agent to work in worktree.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes worktree dir via `git worktree remove --force` and deletes
the local branch by default; keepBranch=true preserves the branch.
Returns { removed: false } when the worktree path doesn't exist.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Read-only tool that compares frozen plan_snapshot against current
task.implementation_plan + story logs + commits. Returns markdown report
with per-AC ✓/✗/? keyword heuristic, drift-score, and plan diff.
Demo users allowed (readOnlyHint: true).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- resetStaleClaimedJobs: also sets plan_snapshot = NULL on reset
- tryClaimJob: JOINs tasks table to read implementation_plan in the
same atomic transaction, writes it to claude_jobs.plan_snapshot
- Empty-plan edge case: NULL becomes '' (non-null) in snapshot
- Exports both functions for unit testing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
update_task_status now delegates to updateTaskStatusWithStoryPromotion
and surfaces story_status_change ('promoted' | 'demoted' | null) in the
response so Claude Code can act on story completion without a separate
read call.
get_claude_context adds an OR-filter on tasks so stories where every
task is DONE are skipped — only surfaces stories that still have work to
do (no tasks, or at least one non-DONE task).
Ported transactional story-promotion logic from Scrum4Me app. Promotes
parent story to DONE when all sibling tasks transition to DONE; demotes
back to IN_SPRINT when a task is re-opened on a DONE story. Accepts an
optional tx client to support existing transaction contexts.
- wait_for_job: blocks ≤600s, claims QUEUED job atomically via FOR UPDATE
SKIP LOCKED, resets stale CLAIMED jobs (>30min), registers ClaudeWorker
presence with heartbeat, emits worker_connected/disconnected via NOTIFY
- update_job_status: agent reports running|done|failed, validates token
ownership (claimed_by_token_id), emits claude_job_status via NOTIFY
- auth.ts extended with tokenId so tools can set claimed_by_token_id
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tot nu toe konden MCP-tools alleen bestaande stories/tasks bewerken
(update_task_status, log_*). Met deze drie tools kan Claude Code een
volledige backlog vanaf nul opbouwen — handig voor nieuwe projecten waar
het Scrum4Me-product nog leeg is.
- create_pbi: { product_id, title, description?, priority, sort_order? }
Auto sort_order = last+1 binnen prio-groep. Code-veld blijft null
(Scrum4Me-app genereert auto-codes via UI/seed).
- create_story: { pbi_id, title, description?, acceptance_criteria?, priority,
sort_order? } — product_id afgeleid uit PBI (denormalized FK conform CLAUDE.md
convention; nooit op client-input vertrouwen). Status='OPEN' default →
landt in product-backlog, niet auto in een sprint.
- create_task: { story_id, title, description?, implementation_plan?, priority,
sort_order? } — sprint_id geërfd van story. Status='TO_DO' default.
Alle drie achter `requireWriteAccess` (PERMISSION_DENIED voor demo) +
`userCanAccessProduct` op de relevante parent-product. Mirror van het
create-todo-patroon.
scripts/smoke-test.ts: tool-count check 13 → 16. README.md: tool-tabel
uitgebreid.
Quality gates: typecheck clean, build success, smoke-test toont 16 tools.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vier nieuwe tools voor het Claude vraag-antwoord-kanaal:
- ask_user_question (write): post een gestructureerde vraag aan de actieve
Scrum4Me-gebruiker over een story; default async (returnt direct met
question_id + status='open'); optionele wait_seconds (max 600) polt elke 2s
tot het antwoord er is of timeout — daarna status='pending' zodat Claude met
get_question_answer later kan ophalen
- get_question_answer (read): huidige status + antwoord van een eerder
gestelde vraag
- list_open_questions (read): eigen vragen met status open/answered, max 50,
meest recente eerst
- cancel_question (write, asker-only): atomic UPDATE WHERE asked_by + status=
'open' zodat alleen eigen open vragen geannuleerd worden
Allemaal achter access-check via userCanAccessStory/Product en demo-blok via
requireWriteAccess (volgt patroon van create-todo en bestaande log-tools).
Submodule vendor/scrum4me bumpt naar Scrum4Me commit 79367dd (M11 ST-1101) —
bevat het ClaudeQuestion-model en notify_question_change-trigger waar deze
tools tegen werken.
scripts/smoke-test.ts: 13 tools verwacht (was 9); list_open_questions
toegevoegd als read-tool-coverage. Build + tools/list groen — verdere e2e via
MCP Inspector na PR-merge omdat de seed een nieuwe API-token heeft
gegenereerd en .env een nieuwe waarde nodig heeft.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #2 merged, so the StoryLog.metadata JSONB column is live. Sync
the vendored schema and wire `metadata` through to prisma.create in
log_implementation, log_test_result and log_commit. Cast via
Prisma.InputJsonValue because Zod parses the input as a generic
record while Prisma's JSON input type is invariant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The MCP SDK rejects tools/call results where structuredContent is not a
record — array returns from list_products triggered an MCP error code
-32602. toolJson now wraps arrays/primitives as { result: <value> }.
scripts/smoke-test.ts spawns the built server over stdio, calls each
read-side tool against the live DB and asserts shape — surfaces this
bug class before regressions ship.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end workflow prompt for Claude Code: fetch context, log a plan,
walk the tasks (in_progress → done), run tests, log result, commit.
Takes product_id as the only argument.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a todo for the authenticated user with optional description (max
2000) and optional product scope. Verifies product access if a
product_id is given. Demo accounts get PERMISSION_DENIED.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three write tools that append StoryLog entries:
- log_implementation: type=IMPLEMENTATION_PLAN
- log_test_result: type=TEST_RESULT, status PASSED|FAILED
- log_commit: type=COMMIT with hash and message
All accept optional metadata in input but skip writing it for now —
the StoryLog.metadata JSONB column lands with Scrum4Me PR #2.
After that PR merges, run sync-schema and add `metadata` to each
prisma.create's data field.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/access.ts: shared product/story/task access checks via product
ownership or membership
- update_task_status accepts lowercase API values, converts to DB
enum, rejects unknown values
- update_task_plan replaces implementation_plan on a task
- Both call requireWriteAccess() so demo accounts get
PERMISSION_DENIED before any DB write
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- health: pings DB via SELECT 1 and returns status/version/time
- list_products: active products owned or shared with the auth user
- get_claude_context: bundled product + active sprint + next story
(with tasks, status mapped to lowercase) + 50 open todos
prisma.ts switches to a lazy proxy so the server bootstrap doesn't
crash before tools fire when DATABASE_URL is unset.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/status.ts: bidirectional Task/Story status mappers — DB stays
UPPER_SNAKE, MCP tools expose lowercase (matches REST API contract)
- src/errors.ts: formatZodError, toolError, toolJson and the
withToolErrors() wrapper so each tool turns thrown exceptions
(PermissionDenied, ZodError, generic) into structured MCP errors
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/prisma.ts: PrismaClient via PrismaPg adapter and pg.Pool,
same pattern as Scrum4Me's lib/prisma.ts
- src/auth.ts: getAuth() resolves SCRUM4ME_TOKEN once, caches
{ userId, username, isDemo }. requireWriteAccess() throws
PermissionDeniedError for demo tokens — write tools call this
before any DB mutation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Initial repo with TypeScript strict, MCP SDK 1.29, Prisma 7,
zod and tsx. Stdio-transport bootstrap in src/index.ts boots
without crashing. Tools and prompts to be added in ST-705..ST-709.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>