p-wegner

board-monitor

System-level board health — conflict scan, server/frontend health check. Board operations (relaunch/merge/nudge/auto-start) are handled by the app's built-in monitor (Settings → Workflow → Board Monitoring). Run Sections 0-3 every cycle; only do Section 4 if the app monitor is OFF.

p-wegner 9 2 Actualitzat fa 1 mes
GitHub

Instal·la

npx skillscat add p-wegner/agentic-kanban/board-monitor

Instal·la des del registre de SkillsCat.

SKILL.md

You are the board monitor. The app's built-in server-side monitor (runMonitorCycle in server-start.ts) handles board operations automatically when enabled. Your job is the system-level checks it can't do itself: run Sections 0–3 every cycle; run Section 4 only when auto_monitor is false.

SECTION 0 — Capacity hold (before ANY start or relaunch)

The host's measured headroom is a brake above every WIP target (#1029). Read it once per cycle:

$proj = (Invoke-RestMethod "http://127.0.0.1:3001/api/preferences/active-project" -TimeoutSec 10).projectId
$cap = (Invoke-RestMethod "http://127.0.0.1:3001/api/projects/$proj/monitor-tunables" -TimeoutSec 10).capacity
"hold=$($cap.hold) maxNewStarts=$($cap.maxNewStarts) $($cap.reason)"
  • hold = true → start ZERO new builders this cycle and do not relaunch idle ones; let running sessions finish and keep at most ONE merge-gate run in flight. Sections 1–3 still run.
  • Otherwise cap this cycle's new starts at maxNewStarts (null = the cheap tier could not measure headroom; the objective's target applies unchanged).
  • Always write $cap.reason (tier, headroom processes / free GB, thrashing) into the cycle's state.md line, so a hold is auditable by its measured numbers.

This is the same snapshot the in-process monitor clamps to (clampWipToHeadroom); the generated CAPACITY HOLD section of objective.md carries the rule, and this route carries the live numbers. There is no hand-written capacity rule anywhere else.

SECTION 1 — Conflict marker scan & auto-fix

Scan for <<<<<<< HEAD markers injected by tsx hot-reload (exclude this skill's own dir — its SKILL.md contains the literal string):

grep -rl "<<<<<<< HEAD" packages/server/src/ packages/client/src/ packages/shared/src/ \
  packages/shared/drizzle/meta/_journal.json 2>/dev/null

For each file found:

  1. Resolve keeping HEAD only:
    node -e "const fs=require('fs'),f=process.argv[1];let c=fs.readFileSync(f,'utf-8').replace(/\r\n/g,'\n');const r=c.replace(/<<<<<<< HEAD\n([\s\S]*?)\n=======\n[\s\S]*?\n>>>>>>> [^\n]+/g,(_,h)=>h);fs.writeFileSync(f,r.replace(/\n/g,'\r\n'),'utf-8');console.log(f+': fixed');" <file>
  2. If _journal.json was fixed, verify valid JSON: node -e "JSON.parse(require('fs').readFileSync('packages/shared/drizzle/meta/_journal.json','utf-8'));console.log('OK')"
  3. git add the fixed files, commit "fix: resolve tsx hot-reload conflict markers [monitor auto-fix]", set $conflictsFixed = $true.

SECTION 2 — Server health check & restart

try { Invoke-RestMethod "http://127.0.0.1:3001/health" -TimeoutSec 5 | Out-Null; "UP" } catch { "DOWN" }

If DOWN:

  1. Restart via Bash: cd packages/server && pnpm dev > /tmp/kanban-monitor-srv.log 2>&1 &
  2. Poll /health until UP — never assume-up after a fixed sleep. A cold start binds slower than any constant sleep; a blind Start-Sleep then single recheck races the bind and makes every REST call this cycle fail with connection-refused. Poll:
    $up = $false
    foreach ($i in 1..20) {
      Start-Sleep -Seconds 2
      try { Invoke-RestMethod "http://127.0.0.1:3001/health" -TimeoutSec 3 | Out-Null; $up = $true; break } catch {}
    }
    if ($up) { "UP after $($i*2)s" } else { "STILL DOWN after 40s" }
  3. Only proceed when $up. Still down after the poll → read /tmp/kanban-monitor-srv.log, report the error, and skip Sections 3–4 (don't fire REST at a server that never bound).

If UP but $conflictsFixed = $true: the tsx watcher may be restarting after the edits — re-run the poll loop (it returns immediately once /health answers), then continue.

SECTION 3 — Frontend smoke check (real render, not just HTTP)

HTTP 200 is not enough: a blank page from a React crash (ReferenceError, missing useState) still returns 200. Read rendered DOM:

.\scripts\board-monitor\frontend-smoke.ps1 -Url "http://127.0.0.1:5173"
  • Contains Todo, In Progress, No issues, or No projects registered before timeout → OK.
  • Times out with empty/irrelevant content → app crashed or failed to hydrate (the smoke command already printed console output + rendered snippets). Diagnose:
    1. Vite module errors: GET http://127.0.0.1:5173/src/routes/BoardPage.tsx — HTTP 500 = client compile error.
    2. Server watcher log: tail -20 /tmp/kanban-monitor-srv.log for SyntaxError, Cannot find, error TS.
    3. Re-run Section 1 (markers may have slipped through).
    4. Check the captured console errors from the timeout output.
    5. Report findings — do not proceed to Section 4 if the server is broken.

SECTION 4 — Board operations (only if auto_monitor is false)

When enabled, the app's built-in monitor already handles: idle→relaunch, reviewing→merge, active+stopped→mark_idle, dead process→mark_idle, running>5min→nudge, auto-start with WIP limit, dependency check, branch naming (feature/ak-{N}-{slug}), and ghost detection (empty workingDir → delete + reset to In Progress). Run this section manually only when auto_monitor is off.

Get the board:

$proj = (Invoke-RestMethod "http://127.0.0.1:3001/api/preferences/active-project" -TimeoutSec 10).projectId
if (-not $proj) { throw "Active project is not set" }
$board = Invoke-RestMethod "http://127.0.0.1:3001/api/projects/$proj/board" -TimeoutSec 10
@($board) | Where-Object { $_.name -in @("Backlog","In Progress","In Review") } |
  ForEach-Object { "COLUMN $($_.name): $($_.issues.Count)" }

For each issue in In Progress / In Review, check workspaceSummary.main. The action depends on the column — idle means "continue work" in In Progress but "ready to land" in In Review:

Column Workspace status Session status Action
In Review idle or active+stopped, committed diff — Merge if prefs allow — see Auto-merge decision table
In Review reviewing stopped Merge (if auto_merge on): POST /merge (60s) — see Merge procedure
In Progress idle any Relaunch: POST /api/workspaces/:id/launch
any reviewing, workingDir empty — Ghost: delete workspace, reset issue to In Progress, create fresh
In Progress active stopped Mark idle: PATCH /api/workspaces/:id {"status":"idle"}
In Progress active running, age >5min Nudge: POST /api/workspaces/:id/turn {"content":"Please continue..."} (field is content, not message)
In Progress active running, age >25min, same lastAssistantMessage 2 cycles Stop the loop: POST /api/workspaces/:id/stop (graceful; only when diff committed / tree clean), then it falls to the In-Review rule once moved
In Progress active running, age ≤5min Leave — too fresh

Auto-merge In Review (configurable)

Two app-owned prefs decide whether the monitor lands not-yet-ready In-Review work — read both every cycle (also in GET /api/preferences/settings; unset → defaults):

$autoMerge        = (Invoke-RestMethod "http://127.0.0.1:3001/api/preferences/auto_merge" -EA SilentlyContinue).value           # default "true"
$autoMergeInReview = (Invoke-RestMethod "http://127.0.0.1:3001/api/preferences/auto_merge_in_review" -EA SilentlyContinue).value # default "false"

For an In Review issue whose main is idle (or active+stopped) with a committed diff (diffStats.filesChanged > 0):

auto_merge auto_merge_in_review readyForMerge Action
false any any Do nothing — operator froze merging. Report under "Idle / awaiting".
true any true Merge — agent marked it ready.
true true false Merge — policy lands committed work without the ready gate.
true false (default) false Do nothing — report under "Idle / awaiting".

User override: if the /board-monitor argument asks to merge In-Review work ("merge in review to master", "no human gating"), treat as auto_merge_in_review=true for this run regardless of the stored pref, and persist it so the app monitor agrees:

Invoke-RestMethod "http://127.0.0.1:3001/api/preferences/settings" -Method Put -ContentType "application/json" -Body '{"auto_merge_in_review":"true"}'

Conversely "stop auto-merging review" / "require manual merge" → set it "false".

Merge procedure (the app owns all merging)

Never run git merge/cherry-pick/rebase/checkout/reset/stash in the main checkout to land a branch — that corrupts the DB and abandons half-finished merges (see end note). The endpoints do the same work safely in the worktree. Always this order:

  1. Merge: POST /api/workspaces/:id/merge (90s).
    • 200 → merged. Verify master advanced (git log --oneline -3 master shows the merge commit — a "merged" response doesn't guarantee code on master) and the issue left In Review (→ Done / AI Reviewed).
    • 409 conflictingFiles → POST /api/workspaces/:id/fix-and-merge {"mergeError":"<the 409 body>"} — launches an agent inside the worktree to rebase/resolve/merge (returns 201 + sessionId). Wait for that session, re-check next cycle. Never resolve by hand.
    • 5xx / timeout → retry once; still failing → check workingDir (empty = ghost), else report under "Needs attention". Never fall back to manual git.
  2. Never relaunch an idle In-Review workspace — its work is already committed; relaunching just restarts whatever loop stopped it.

Two branches that both edit one file (e.g. both append to CLAUDE.md) conflict even when neither is wrong; fix-and-merge handles these, and for additive changes (docs, tables, lists) the right resolution is keep both. Optional pre-merge for a long-lived branch: POST /api/workspaces/:id/update-base {"mode":"rebase"} (shrinks conflict surface; POST .../abort-rebase if it sticks).

This mirrors the app's built-in monitor exactly (monitor-cycle.ts reads the same two prefs) — same policy, same gates.

Auto-start: if fewer than 3 issues In Progress, start the highest-priority Todo by POSTing
/api/workspaces with { issueId } only:

# POST /api/workspaces with { issueId }  -- do NOT send a `branch`

Never derive the branch name here. The server derives it from the issue with the ONE canonical
producer (suggestBranchName, packages/shared/src/lib/branch.ts) whenever branch is omitted
(workspace-provision.service.ts:96). This step used to build its own slug in PowerShell, with a
different sanitizer than the server's — and that divergence is a measured defect, not a theoretical
one (#220, and #361 Observation C):

producer PM pipeline 6/9: Code Generation (MVP skeleton)
server (suggestBranchName): [^a-z0-9]+ → - feature/ak-6-pm-pipeline-6-9-code-generation-mvp-skel
the removed one-liner: [^a-zA-Z0-9\s] → "" feature/ak-6-pm-pipeline-69-code-generation-mvp-skele

Both of those names existed at once for kassenbuch issue #6: the work landed and merged on the
second, while the first stayed on a worktree "created from master" that never received any of it and
was then orphaned. Two slugs for one issue also defeat any duplicate-start guard that compares
branch names, which is how #220's two concurrent workspaces on one ticket happened.

SECTION 5 — Summary

Write a concise Slack-style summary so someone without board access understands the state at a glance.

[monitor] {yyyy-MM-ddTHH:mm}
🟢 Server OK  |  ✅ Merged: N  |  🚀 Started: N  |  🔁 Relaunched: N  |  ⚠️ Conflicts: N

▶ Started:
  • #N  <one-line title, max ~60 chars>
✅ Merged:
  • #N  <one-line title>
🔁 Relaunched / nudged:
  • #N  <one-line title>  (reason: idle / stuck Xmin)
⚠️ Needs attention:
  • #N  <title>  — <what's wrong, e.g. "merge conflicts in Layout.tsx">
💤 Idle / stopped:
  • #N  <title>  — <last agent message, first line, max ~80 chars>

Rules:

  • Always include ticket number AND a short title (never numbers alone). Strip boilerplate ("E2E: cover", "Add", "Show") to keep the core noun phrase — "#253 Estimate badge on issue cards", not "#253 Show estimate badge on kanban issue cards".
  • Omit empty sections; keep the whole summary under ~25 lines.
  • For idle/stopped: include the last: line from pnpm cli -- status. For full context on a stop, pnpm cli -- issue status <N>.

Known patterns & fixes

Pattern Symptom Fix
ZAI / glm-5.1 Session stops immediately, exitCode: null Relaunch every cycle
Ghost workspace status: reviewing, workingDir: "", merge fails "not something we can merge" Delete workspace, reset issue to In Progress, create fresh
Merge conflict (409) POST /merge returns 409 conflictingFiles POST /fix-and-merge (resolves in worktree) — never by hand
Merge timeout No conflicts but merge times out Retry once; still failing → "Needs attention", no manual git
tsx conflict injection <<<<<<< HEAD in .ts/.tsx after a cherry-pick Section 1 auto-fix; then check if shared needs rebuild (pnpm --filter @agentic-kanban/shared build)
React blank page HTTP 200 but main empty, no console errors Missing useState — check recent conflict resolutions in BoardPage.tsx
scheduledRuns not exported Crash: SyntaxError: does not provide an export named 'scheduledRuns' pnpm --filter @agentic-kanban/shared build
Active project not set "No projects registered" despite projects existing GET /api/preferences/active-project; fix via PUT /api/preferences {"active_project":"<id>"}

Why manual merges are banned

On 2026-05-26 the monitor fell back to manual git merge/checkout/reset in the main checkout and caused two failures: (1) git touched the live SQLite WAL/SHM sidecars the running server held open → kanban.db corruption ("disk image is malformed") → full DB reset + data loss; (2) a two-branch CLAUDE.md conflict was left with MERGE_HEAD + markers at cycle end. Both are impossible if you only ever merge through the app endpoints (/merge → /fix-and-merge), which operate in the worktree and never leave dangling state.