mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-08-28 02:44:59 +00:00
feat(US-1611): Complete orchestrator-inspired features
- #73 Prompts registry: prompt-registry/ with 10 starter templates ✓ - #74 Doc freshness: CLAUDE.md template + SOP-documentation-freshness.md ✓ - #75 Setup wizard: vk setup command ✓ - #76 Lifecycle hooks: hook-service.ts + SOP-lifecycle-hooks.md ✓ - #77 Shared resources: SOP-shared-resources.md ✓ Credit: Inspired by Monika Voutov's BoardKit Orchestrator https://github.com/BoardKit/orchestrator Closes #73, closes #74, closes #75, closes #76, closes #77
This commit is contained in:
parent
4a1cf7d36d
commit
f176592259
9 changed files with 889 additions and 7 deletions
136
CLAUDE.md
Normal file
136
CLAUDE.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# CLAUDE.md — Agent Guidelines for Veritas Kanban
|
||||
|
||||
This file defines project-specific rules, context, and lessons learned for AI agents working on Veritas Kanban. Update it after every mistake, discovery, or workflow change.
|
||||
|
||||
> **Last updated:** 2026-02-04
|
||||
> **Freshness check:** Review monthly or after major releases
|
||||
|
||||
---
|
||||
|
||||
## Project Context
|
||||
|
||||
**Veritas Kanban** is an open-source AI-native task management system. It's designed for humans + AI agents to collaborate on work through a shared board, CLI, and API.
|
||||
|
||||
- **Primary language:** TypeScript (strict mode)
|
||||
- **Monorepo:** pnpm workspaces — `server/`, `web/`, `cli/`, `shared/`
|
||||
- **Build:** Node 22+, pnpm 9+
|
||||
- **Test:** Vitest (server), React Testing Library (web)
|
||||
- **Style:** ESLint + Prettier, conventional commits
|
||||
|
||||
---
|
||||
|
||||
## Architecture Rules
|
||||
|
||||
### Server (Express + TypeScript)
|
||||
|
||||
- All routes go through centralized middleware in `server/src/middleware/`
|
||||
- Auth: JWT + API keys, localhost bypass for dev (`VERITAS_AUTH_LOCALHOST_BYPASS=true`)
|
||||
- Storage: Abstract via `storage/interfaces.ts` — never import `fs` directly in services
|
||||
- Error handling: Use `UnauthorizedError`, `ForbiddenError`, `BadRequestError`, `InternalError`
|
||||
- Pagination: Use `sendPaginated(res, items, {page, limit, total})`
|
||||
|
||||
### Web (React + Vite)
|
||||
|
||||
- State: Zustand stores, no prop drilling past 2 levels
|
||||
- Realtime: WebSocket via `useRealtimeUpdates` hooks
|
||||
- Styling: Tailwind CSS, component-scoped styles
|
||||
|
||||
### CLI (Commander.js)
|
||||
|
||||
- Every command mirrors an API endpoint
|
||||
- JSON output via `--json` flag for scripting
|
||||
- Use `chalk` for colored output
|
||||
|
||||
---
|
||||
|
||||
## Code Quality Gates
|
||||
|
||||
1. **Cross-model review required for all code changes**
|
||||
- If Claude writes it, GPT reviews (and vice versa)
|
||||
- See `prompt-registry/cross-model-review.md`
|
||||
|
||||
2. **No hardcoded secrets** — use environment variables
|
||||
|
||||
3. **All user input validated** — use Zod schemas
|
||||
|
||||
4. **Path traversal prevention** — use `validatePathSegment()` from security module
|
||||
|
||||
5. **Tests for new features** — aim for >80% coverage on critical paths
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes (Don't Repeat These)
|
||||
|
||||
### Security
|
||||
|
||||
- ❌ Forgot global middleware — flagged missing per-route auth that was already in `app.use()`
|
||||
- ❌ Used `path.join()` without validation — allows `../` traversal
|
||||
- ✅ Always check `validatePathSegment()` for any user-supplied path component
|
||||
|
||||
### Architecture
|
||||
|
||||
- ❌ Imported `fs` directly in service files — breaks storage abstraction
|
||||
- ❌ Added polling when WebSocket hook existed — use `useRealtimeAgentStatus`
|
||||
- ✅ Check for existing hooks/services before creating new ones
|
||||
|
||||
### Testing
|
||||
|
||||
- ❌ Used wrong field in backfilled events (`status: "success"` vs `success: true`)
|
||||
- ✅ Match actual runtime schema exactly in test fixtures
|
||||
|
||||
---
|
||||
|
||||
## Conventions
|
||||
|
||||
### Naming
|
||||
|
||||
- Files: `kebab-case.ts`
|
||||
- Components: `PascalCase.tsx`
|
||||
- Variables/functions: `camelCase`
|
||||
- Constants: `UPPER_SNAKE_CASE`
|
||||
|
||||
### Git
|
||||
|
||||
- Branch: `feat/description-issue-number`, `fix/description-issue-number`
|
||||
- Commit: Conventional commits (`feat:`, `fix:`, `docs:`, `chore:`)
|
||||
- PR: Always reference issue number
|
||||
|
||||
### Task Workflow
|
||||
|
||||
1. Start timer: `vk begin <id>`
|
||||
2. Update status: `vk status <id> in-progress`
|
||||
3. Work, commit, push
|
||||
4. Cross-model review
|
||||
5. Complete: `vk done <id> "summary"`
|
||||
|
||||
---
|
||||
|
||||
## File Locations
|
||||
|
||||
| What | Where |
|
||||
| ---------------- | ---------------------- |
|
||||
| API routes | `server/src/routes/` |
|
||||
| Services | `server/src/services/` |
|
||||
| Schemas | `server/src/schemas/` |
|
||||
| Storage | `server/src/storage/` |
|
||||
| React components | `web/src/components/` |
|
||||
| Zustand stores | `web/src/stores/` |
|
||||
| CLI commands | `cli/src/commands/` |
|
||||
| Shared types | `shared/src/` |
|
||||
| Prompts | `prompt-registry/` |
|
||||
| SOPs | `docs/SOP-*.md` |
|
||||
|
||||
---
|
||||
|
||||
## When to Update This File
|
||||
|
||||
- After a bug that could have been prevented by a rule
|
||||
- After discovering a pattern that should be standard
|
||||
- After a cross-model review catches something systemic
|
||||
- Monthly freshness review (add to calendar)
|
||||
|
||||
---
|
||||
|
||||
## Credit
|
||||
|
||||
Structure inspired by Anthropic's CLAUDE.md convention and [BoardKit Orchestrator](https://github.com/BoardKit/orchestrator) by Monika Voutov.
|
||||
|
|
@ -238,19 +238,22 @@ prompt-registry/
|
|||
|
||||
Stale docs = hallucinating AI. Keep these files current:
|
||||
|
||||
| File | Purpose |
|
||||
| ------------------------------ | -------------------------------------------------------------- |
|
||||
| `AGENTS.md` | Personality, escalation rules, cross-model review requirement. |
|
||||
| `SOUL.md` | "Who are we?" - tone/voice used by agents. |
|
||||
| `CLAUDE.md` / `GPT.md` | Model-specific guardrails or lessons learned. |
|
||||
| `docs/BEST-PRACTICES.md` (new) | Patterns and anti-patterns all agents follow. |
|
||||
| File | Purpose |
|
||||
| ------------------------ | ------------------------------------------------------------------ |
|
||||
| `CLAUDE.md` | Agent rules, architecture, lessons learned. **Template included.** |
|
||||
| `AGENTS.md` | Personality, escalation rules, cross-model review requirement. |
|
||||
| `SOUL.md` | "Who are we?" - tone/voice used by agents. |
|
||||
| `GPT.md` / `CODEX.md` | Model-specific guardrails (optional). |
|
||||
| `docs/BEST-PRACTICES.md` | Patterns and anti-patterns all agents follow. |
|
||||
|
||||
**Cadence:**
|
||||
|
||||
- Update immediately after a mistake or new learning.
|
||||
- Mirror to Brain/knowledge base if you use one (see `scripts/brain-write.sh`).
|
||||
- Run monthly freshness audits (see [SOP-documentation-freshness.md](SOP-documentation-freshness.md)).
|
||||
- During sprint closure, skim the "Lessons Learned" field on each task and propagate anything evergreen into AGENTS/CLAUDE.
|
||||
|
||||
**Automation:** Future versions will include a "Doc Steward" agent that summarizes recent commits and suggests doc updates. See the [Doc Freshness SOP](SOP-documentation-freshness.md) for the roadmap.
|
||||
|
||||
---
|
||||
|
||||
## Multi-Repo / Multi-Agent Notes
|
||||
|
|
@ -283,6 +286,9 @@ This is invaluable for Champions-style research tasks or anything needing a real
|
|||
- [Sprint Planning with AI Agents](SOP-sprint-planning.md)
|
||||
- [Multi-Agent Orchestration](SOP-multi-agent-orchestration.md)
|
||||
- [Cross-Model Code Review](SOP-cross-model-code-review.md)
|
||||
- [Documentation Freshness](SOP-documentation-freshness.md)
|
||||
- [Shared Resources](SOP-shared-resources.md)
|
||||
- [Lifecycle Hooks](SOP-lifecycle-hooks.md)
|
||||
2. Align on [Best Practices](BEST-PRACTICES.md) & [Tips + Tricks](TIPS-AND-TRICKS.md).
|
||||
3. Browse [Real-world Examples](EXAMPLES-agent-workflows.md) and steal the prompts.
|
||||
4. Keep `docs/TROUBLESHOOTING.md` handy for deeper diagnostics.
|
||||
|
|
|
|||
207
docs/SOP-documentation-freshness.md
Normal file
207
docs/SOP-documentation-freshness.md
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
# SOP: Documentation Freshness
|
||||
|
||||
> "Stale docs = hallucinating AI." — Monika Voutov
|
||||
|
||||
Keep project documentation current as the codebase evolves.
|
||||
|
||||
---
|
||||
|
||||
## Why It Matters
|
||||
|
||||
AI agents rely on documentation to understand context, conventions, and constraints. Outdated docs cause:
|
||||
|
||||
- Incorrect assumptions about architecture
|
||||
- Repeated mistakes that were already solved
|
||||
- Inconsistent code patterns
|
||||
- Wasted time re-discovering known issues
|
||||
|
||||
---
|
||||
|
||||
## Core Documents
|
||||
|
||||
Every project should maintain these files:
|
||||
|
||||
| File | Purpose | Update Cadence |
|
||||
| ------------------------ | -------------------------------------- | ----------------------------- |
|
||||
| `CLAUDE.md` | Agent rules, patterns, lessons learned | After every mistake/discovery |
|
||||
| `AGENTS.md` | Agent personality, escalation rules | When workflow changes |
|
||||
| `docs/BEST-PRACTICES.md` | Team patterns and anti-patterns | Monthly or after post-mortems |
|
||||
| `prompt-registry/*.md` | Workflow prompts | When prompts drift or improve |
|
||||
| `README.md` | Project overview, quick start | After major releases |
|
||||
|
||||
### Optional Model-Specific Files
|
||||
|
||||
- `GPT.md` — GPT-specific notes (if behavior differs from Claude)
|
||||
- `GEMINI.md` — Gemini-specific notes
|
||||
- `CODEX.md` — Codex-specific notes
|
||||
|
||||
---
|
||||
|
||||
## Update Triggers
|
||||
|
||||
### Immediate Updates
|
||||
|
||||
Update docs **within the same session** when:
|
||||
|
||||
1. **A bug was caused by missing context** — Add the context to CLAUDE.md
|
||||
2. **Cross-model review catches a pattern** — Document the pattern
|
||||
3. **A workaround is discovered** — Add to Troubleshooting or CLAUDE.md
|
||||
4. **API behavior changes** — Update relevant docs
|
||||
|
||||
### Scheduled Updates
|
||||
|
||||
Review docs on a regular cadence:
|
||||
|
||||
| Cadence | Action |
|
||||
| ----------- | ---------------------------------------------------------- |
|
||||
| Weekly | Skim task "Lessons Learned" fields, propagate to CLAUDE.md |
|
||||
| Monthly | Full freshness audit (see checklist below) |
|
||||
| Per release | Update README, CHANGELOG, migration guides |
|
||||
|
||||
---
|
||||
|
||||
## Freshness Audit Checklist
|
||||
|
||||
Run this monthly or after major releases:
|
||||
|
||||
```markdown
|
||||
## Doc Freshness Audit — [DATE]
|
||||
|
||||
### CLAUDE.md
|
||||
|
||||
- [ ] "Last updated" date is within 30 days
|
||||
- [ ] Architecture section matches current code structure
|
||||
- [ ] Common mistakes section includes recent learnings
|
||||
- [ ] No outdated file paths or removed features
|
||||
|
||||
### BEST-PRACTICES.md
|
||||
|
||||
- [ ] All "Do This" items are still valid
|
||||
- [ ] All "Don't Do This" items reflect real issues
|
||||
- [ ] No references to deprecated workflows
|
||||
|
||||
### prompt-registry/
|
||||
|
||||
- [ ] Prompts reference current API endpoints
|
||||
- [ ] No prompts for removed features
|
||||
- [ ] Cross-model review prompt matches current checklist
|
||||
|
||||
### README.md
|
||||
|
||||
- [ ] Quick start instructions work on clean install
|
||||
- [ ] Badge/version numbers are current
|
||||
- [ ] Screenshots match current UI
|
||||
|
||||
### SOPs (docs/SOP-\*.md)
|
||||
|
||||
- [ ] Workflows match current implementation
|
||||
- [ ] CLI commands are correct
|
||||
- [ ] API examples return expected responses
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Automation Plan (Future)
|
||||
|
||||
### Phase 1: Manual with Reminders (Current)
|
||||
|
||||
- Monthly calendar reminder for freshness audit
|
||||
- Task "Lessons Learned" field captures immediate learnings
|
||||
- Sprint retrospectives include doc review
|
||||
|
||||
### Phase 2: Commit-Triggered Suggestions
|
||||
|
||||
Use a git hook or CI job to flag potentially stale docs:
|
||||
|
||||
```bash
|
||||
# .git/hooks/post-commit (concept)
|
||||
#!/bin/bash
|
||||
|
||||
# Check if changed files might affect docs
|
||||
changed_files=$(git diff --name-only HEAD~1)
|
||||
|
||||
if echo "$changed_files" | grep -q "server/src/routes"; then
|
||||
echo "⚠️ Routes changed — consider updating API docs"
|
||||
fi
|
||||
|
||||
if echo "$changed_files" | grep -q "server/src/services"; then
|
||||
echo "⚠️ Services changed — consider updating CLAUDE.md architecture section"
|
||||
fi
|
||||
```
|
||||
|
||||
### Phase 3: Doc Steward Agent
|
||||
|
||||
A dedicated agent task type that:
|
||||
|
||||
1. Runs weekly (cron or heartbeat)
|
||||
2. Summarizes recent commits: `git log --oneline --since="1 week ago"`
|
||||
3. Compares against doc sections
|
||||
4. Creates a task with suggested updates
|
||||
|
||||
**Prompt template:**
|
||||
|
||||
```markdown
|
||||
You are a Documentation Steward for Veritas Kanban.
|
||||
|
||||
## Recent Changes
|
||||
|
||||
[INSERT GIT LOG]
|
||||
|
||||
## Current CLAUDE.md
|
||||
|
||||
[INSERT CURRENT FILE]
|
||||
|
||||
## Task
|
||||
|
||||
1. Identify changes that might require doc updates
|
||||
2. For each, suggest specific edits
|
||||
3. Output as a markdown checklist
|
||||
|
||||
Focus on:
|
||||
|
||||
- New files/services not mentioned in architecture
|
||||
- Changed APIs not reflected in examples
|
||||
- Bug fixes that should be added to "Common Mistakes"
|
||||
```
|
||||
|
||||
### Phase 4: Automated PR Comments
|
||||
|
||||
GitHub Action that:
|
||||
|
||||
1. Triggers on PR
|
||||
2. Uses AI to compare diff against relevant docs
|
||||
3. Comments on PR if docs might need updates
|
||||
|
||||
---
|
||||
|
||||
## Integration with VK
|
||||
|
||||
### Task Type: `docs`
|
||||
|
||||
Use the `docs` task type for documentation work:
|
||||
|
||||
```bash
|
||||
vk create "Update CLAUDE.md after auth refactor" --type docs --priority high
|
||||
```
|
||||
|
||||
### Lifecycle Hook: onCompleted
|
||||
|
||||
Configure a hook to remind about docs after task completion:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"enabled": true,
|
||||
"onCompleted": {
|
||||
"enabled": true,
|
||||
"webhook": "https://your-reminder-service.com/doc-check"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Credit
|
||||
|
||||
Documentation freshness pattern inspired by [BoardKit Orchestrator](https://github.com/BoardKit/orchestrator) by Monika Voutov.
|
||||
245
docs/SOP-lifecycle-hooks.md
Normal file
245
docs/SOP-lifecycle-hooks.md
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
# SOP: Task Lifecycle Hooks
|
||||
|
||||
Automate workflows with hooks that fire on task state transitions.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Lifecycle hooks trigger actions when tasks change state:
|
||||
|
||||
| Hook | Fires When |
|
||||
| ------------- | --------------------------- |
|
||||
| `onCreated` | Task is created |
|
||||
| `onStarted` | Task moves to `in-progress` |
|
||||
| `onBlocked` | Task moves to `blocked` |
|
||||
| `onCompleted` | Task moves to `done` |
|
||||
| `onArchived` | Task is archived |
|
||||
|
||||
Each hook can:
|
||||
|
||||
- POST to a webhook URL
|
||||
- Send a notification (via configured channel)
|
||||
- Log to the activity feed
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Enable hooks via the settings API:
|
||||
|
||||
```bash
|
||||
curl -X PATCH http://localhost:3001/api/config/settings \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"hooks": {
|
||||
"enabled": true,
|
||||
"onCreated": {
|
||||
"enabled": true,
|
||||
"webhook": "https://your-server.com/webhooks/vk",
|
||||
"notify": false,
|
||||
"logActivity": true
|
||||
},
|
||||
"onStarted": {
|
||||
"enabled": true,
|
||||
"notify": true
|
||||
},
|
||||
"onBlocked": {
|
||||
"enabled": true,
|
||||
"webhook": "https://your-server.com/webhooks/vk",
|
||||
"notify": true
|
||||
},
|
||||
"onCompleted": {
|
||||
"enabled": true,
|
||||
"notify": true
|
||||
},
|
||||
"onArchived": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
| ----------------------- | ------- | ------- | ---------------------- |
|
||||
| `hooks.enabled` | boolean | false | Global enable/disable |
|
||||
| `hooks.on*.enabled` | boolean | false | Enable specific hook |
|
||||
| `hooks.on*.webhook` | string | — | URL to POST payload |
|
||||
| `hooks.on*.notify` | boolean | false | Send notification |
|
||||
| `hooks.on*.logActivity` | boolean | true | Record in activity log |
|
||||
|
||||
---
|
||||
|
||||
## Webhook Payload
|
||||
|
||||
When a webhook is configured, VK sends a POST request:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "onBlocked",
|
||||
"taskId": "task_20260204_abc123",
|
||||
"taskTitle": "Implement OAuth login",
|
||||
"previousStatus": "in-progress",
|
||||
"newStatus": "blocked",
|
||||
"project": "my-project",
|
||||
"sprint": "US-1700",
|
||||
"timestamp": "2026-02-04T15:30:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Headers
|
||||
|
||||
| Header | Value |
|
||||
| -------------- | ----------------------------------- |
|
||||
| `Content-Type` | `application/json` |
|
||||
| `X-VK-Event` | Hook event name (e.g., `onBlocked`) |
|
||||
|
||||
### Retry Behavior
|
||||
|
||||
- Initial attempt with 10-second timeout
|
||||
- Single retry after 2 seconds on failure
|
||||
- Failures are logged but don't block the operation
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Slack Alert on Blocked Tasks
|
||||
|
||||
Configure `onBlocked` to POST to a Slack incoming webhook:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"enabled": true,
|
||||
"onBlocked": {
|
||||
"enabled": true,
|
||||
"webhook": "https://hooks.slack.com/services/XXX/YYY/ZZZ"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Your Slack webhook receives the payload and can format a message.
|
||||
|
||||
### 2. Agent Wake on Task Assignment
|
||||
|
||||
Use `onCreated` to wake an AI agent via OpenClaw:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"enabled": true,
|
||||
"onCreated": {
|
||||
"enabled": true,
|
||||
"webhook": "http://localhost:8080/api/wake"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The agent receives the new task and can begin work automatically.
|
||||
|
||||
### 3. Sprint Metrics on Completion
|
||||
|
||||
Use `onCompleted` to update external dashboards:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"enabled": true,
|
||||
"onCompleted": {
|
||||
"enabled": true,
|
||||
"webhook": "https://metrics.example.com/vk/task-complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Webhook Receiver Example
|
||||
|
||||
Simple Express handler for VK hooks:
|
||||
|
||||
```typescript
|
||||
import express from 'express';
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
app.post('/webhooks/vk', (req, res) => {
|
||||
const { event, taskId, taskTitle, newStatus } = req.body;
|
||||
|
||||
console.log(`[VK Hook] ${event}: ${taskTitle} (${taskId}) → ${newStatus}`);
|
||||
|
||||
switch (event) {
|
||||
case 'onBlocked':
|
||||
// Alert the team
|
||||
notifySlack(`⚠️ Task blocked: ${taskTitle}`);
|
||||
break;
|
||||
case 'onCompleted':
|
||||
// Update metrics
|
||||
recordCompletion(taskId);
|
||||
break;
|
||||
}
|
||||
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
app.listen(3002);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Check current hooks configuration:
|
||||
|
||||
```bash
|
||||
curl http://localhost:3001/api/config/settings | jq '.data.hooks'
|
||||
```
|
||||
|
||||
Enable/disable hooks quickly:
|
||||
|
||||
```bash
|
||||
# Enable all hooks
|
||||
curl -X PATCH http://localhost:3001/api/config/settings \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"hooks": {"enabled": true}}'
|
||||
|
||||
# Disable all hooks
|
||||
curl -X PATCH http://localhost:3001/api/config/settings \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"hooks": {"enabled": false}}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Hooks Not Firing
|
||||
|
||||
1. Check `hooks.enabled` is `true`
|
||||
2. Check the specific hook (e.g., `hooks.onBlocked.enabled`) is `true`
|
||||
3. Check server logs for errors: `tail -f server/logs/server.log | grep hooks`
|
||||
|
||||
### Webhook Not Receiving
|
||||
|
||||
1. Verify the URL is reachable from the VK server
|
||||
2. Check for firewall/network issues
|
||||
3. Ensure your receiver responds within 10 seconds
|
||||
4. Look for retry attempts in logs
|
||||
|
||||
### Activity Log Missing Events
|
||||
|
||||
- `logActivity` defaults to `true`
|
||||
- Check telemetry is enabled in settings
|
||||
|
||||
---
|
||||
|
||||
## Credit
|
||||
|
||||
Lifecycle hooks pattern inspired by [BoardKit Orchestrator](https://github.com/BoardKit/orchestrator) by Monika Voutov.
|
||||
|
|
@ -2,6 +2,7 @@ import { Router, type Router as RouterType } from 'express';
|
|||
import { ConfigService } from '../services/config-service.js';
|
||||
import { getTelemetryService } from '../services/telemetry-service.js';
|
||||
import { getAttachmentService } from '../services/attachment-service.js';
|
||||
import { setHooksSettings } from '../services/hook-service.js';
|
||||
import type { FeatureSettings } from '@veritas-kanban/shared';
|
||||
import { FeatureSettingsPatchSchema } from '../schemas/feature-settings-schema.js';
|
||||
import { strictRateLimit } from '../middleware/rate-limit.js';
|
||||
|
|
@ -33,6 +34,9 @@ export function syncSettingsToServices(settings: FeatureSettings): void {
|
|||
maxFilesPerTask: settings.tasks.attachmentMaxPerTask,
|
||||
maxTotalSize: settings.tasks.attachmentMaxTotalSize,
|
||||
});
|
||||
|
||||
// Sync lifecycle hooks settings
|
||||
setHooksSettings(settings.hooks);
|
||||
}
|
||||
|
||||
// GET /api/settings/features — returns full feature settings with defaults merged
|
||||
|
|
|
|||
|
|
@ -100,6 +100,44 @@ const BudgetSettingsSchema = z
|
|||
.strict()
|
||||
.optional();
|
||||
|
||||
/**
|
||||
* Task lifecycle hooks configuration.
|
||||
*
|
||||
* Hooks are triggered on task state transitions:
|
||||
* - onCreated: Task is created
|
||||
* - onStarted: Task moves to in-progress
|
||||
* - onBlocked: Task moves to blocked
|
||||
* - onCompleted: Task moves to done
|
||||
* - onArchived: Task is archived
|
||||
*
|
||||
* Each hook can specify:
|
||||
* - enabled: Whether the hook is active
|
||||
* - webhook: URL to POST event payload (optional)
|
||||
* - notify: Send notification to configured channel (optional)
|
||||
* - logActivity: Record in activity log (default: true)
|
||||
*/
|
||||
const HookConfigSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
webhook: z.string().url().optional(),
|
||||
notify: z.boolean().optional(),
|
||||
logActivity: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
const HooksSettingsSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
onCreated: HookConfigSchema,
|
||||
onStarted: HookConfigSchema,
|
||||
onBlocked: HookConfigSchema,
|
||||
onCompleted: HookConfigSchema,
|
||||
onArchived: HookConfigSchema,
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const FeatureSettingsPatchSchema = z
|
||||
.object({
|
||||
board: BoardSettingsSchema,
|
||||
|
|
@ -109,6 +147,7 @@ export const FeatureSettingsPatchSchema = z
|
|||
notifications: NotificationSettingsSchema,
|
||||
archive: ArchiveSettingsSchema,
|
||||
budget: BudgetSettingsSchema,
|
||||
hooks: HooksSettingsSchema,
|
||||
})
|
||||
.strict()
|
||||
.refine((val) => !hasDangerousKeys(val), {
|
||||
|
|
|
|||
203
server/src/services/hook-service.ts
Normal file
203
server/src/services/hook-service.ts
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
/**
|
||||
* Task Lifecycle Hook Service
|
||||
*
|
||||
* Fires configured hooks on task state transitions:
|
||||
* - onCreated: Task is created
|
||||
* - onStarted: Task moves to in-progress
|
||||
* - onBlocked: Task moves to blocked
|
||||
* - onCompleted: Task moves to done
|
||||
* - onArchived: Task is archived
|
||||
*
|
||||
* Each hook can trigger:
|
||||
* - Webhook POST to configured URL
|
||||
* - Notification to configured channel
|
||||
* - Activity log entry
|
||||
*
|
||||
* Inspired by BoardKit Orchestrator's hook system.
|
||||
*/
|
||||
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
import type { Task } from '@veritas-kanban/shared';
|
||||
|
||||
const log = createLogger('hooks');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type HookEvent = 'onCreated' | 'onStarted' | 'onBlocked' | 'onCompleted' | 'onArchived';
|
||||
|
||||
export interface HookConfig {
|
||||
enabled?: boolean;
|
||||
webhook?: string;
|
||||
notify?: boolean;
|
||||
logActivity?: boolean;
|
||||
}
|
||||
|
||||
export interface HooksSettings {
|
||||
enabled?: boolean;
|
||||
onCreated?: HookConfig;
|
||||
onStarted?: HookConfig;
|
||||
onBlocked?: HookConfig;
|
||||
onCompleted?: HookConfig;
|
||||
onArchived?: HookConfig;
|
||||
}
|
||||
|
||||
export interface HookPayload {
|
||||
event: HookEvent;
|
||||
taskId: string;
|
||||
taskTitle: string;
|
||||
previousStatus?: string;
|
||||
newStatus?: string;
|
||||
project?: string;
|
||||
sprint?: string;
|
||||
timestamp: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings Cache
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let cachedSettings: HooksSettings | undefined;
|
||||
|
||||
/**
|
||||
* Set the hooks configuration. Called by settings service on load/change.
|
||||
*/
|
||||
export function setHooksSettings(settings: HooksSettings | undefined): void {
|
||||
cachedSettings = settings;
|
||||
log.info({ enabled: settings?.enabled ?? false }, 'Hooks settings updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current hooks configuration.
|
||||
*/
|
||||
export function getHooksSettings(): HooksSettings | undefined {
|
||||
return cachedSettings;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hook Execution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Fire a lifecycle hook for a task event.
|
||||
* Non-blocking — errors are logged but don't propagate.
|
||||
*/
|
||||
export async function fireHook(
|
||||
event: HookEvent,
|
||||
task: Pick<Task, 'id' | 'title' | 'status' | 'project' | 'sprint'>,
|
||||
previousStatus?: string
|
||||
): Promise<void> {
|
||||
const settings = cachedSettings;
|
||||
|
||||
// Check if hooks are globally enabled
|
||||
if (!settings?.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the specific hook config
|
||||
const hookConfig = settings[event];
|
||||
if (!hookConfig?.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: HookPayload = {
|
||||
event,
|
||||
taskId: task.id,
|
||||
taskTitle: task.title,
|
||||
previousStatus,
|
||||
newStatus: task.status,
|
||||
project: task.project,
|
||||
sprint: task.sprint,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
log.info({ event, taskId: task.id }, 'Firing hook');
|
||||
|
||||
// Fire webhook if configured
|
||||
if (hookConfig.webhook) {
|
||||
fireWebhook(hookConfig.webhook, payload).catch((err) => {
|
||||
log.warn({ event, taskId: task.id, error: err.message }, 'Webhook delivery failed');
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Fire notification if configured (integrate with notification-service)
|
||||
// if (hookConfig.notify) {
|
||||
// notifyHookEvent(event, payload);
|
||||
// }
|
||||
|
||||
// Activity logging is handled by the existing activity service
|
||||
// The logActivity flag could be used to suppress logging if needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Deliver a webhook payload to the configured URL.
|
||||
* Single retry after 2 seconds on failure.
|
||||
*/
|
||||
async function fireWebhook(url: string, payload: HookPayload): Promise<void> {
|
||||
const body = JSON.stringify(payload);
|
||||
|
||||
const doFetch = async (): Promise<void> => {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-VK-Event': payload.event,
|
||||
},
|
||||
body,
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await doFetch();
|
||||
log.debug({ event: payload.event, url }, 'Webhook delivered');
|
||||
} catch (err) {
|
||||
log.warn(
|
||||
{ event: payload.event, url, error: (err as Error).message },
|
||||
'Webhook failed, retrying in 2s'
|
||||
);
|
||||
|
||||
// Single retry after 2 seconds
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await doFetch();
|
||||
log.debug({ event: payload.event, url }, 'Webhook retry succeeded');
|
||||
} catch (retryErr) {
|
||||
log.error(
|
||||
{ event: payload.event, url, error: (retryErr as Error).message },
|
||||
'Webhook retry failed'
|
||||
);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Convenience Functions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Map a status change to the appropriate hook event.
|
||||
*/
|
||||
export function getHookEventForStatusChange(
|
||||
previousStatus: string | undefined,
|
||||
newStatus: string
|
||||
): HookEvent | null {
|
||||
// Status transitions that trigger hooks
|
||||
if (newStatus === 'in-progress' && previousStatus !== 'in-progress') {
|
||||
return 'onStarted';
|
||||
}
|
||||
if (newStatus === 'blocked' && previousStatus !== 'blocked') {
|
||||
return 'onBlocked';
|
||||
}
|
||||
if (newStatus === 'done' && previousStatus !== 'done') {
|
||||
return 'onCompleted';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import { getTelemetryService, type TelemetryService } from './telemetry-service.
|
|||
import { withFileLock } from './file-lock.js';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
import { ConflictError, NotFoundError, ValidationError } from '../middleware/error-handler.js';
|
||||
import { fireHook, getHookEventForStatusChange } from './hook-service.js';
|
||||
|
||||
const log = createLogger('task-cache');
|
||||
|
||||
|
|
@ -433,6 +434,11 @@ export class TaskService {
|
|||
status: task.status,
|
||||
});
|
||||
|
||||
// Fire onCreated hook
|
||||
fireHook('onCreated', task).catch((err) => {
|
||||
log.warn({ taskId: task.id }, 'onCreated hook failed: %s', err);
|
||||
});
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
|
|
@ -503,6 +509,14 @@ export class TaskService {
|
|||
status: updatedTask.status,
|
||||
previousStatus,
|
||||
});
|
||||
|
||||
// Fire lifecycle hook if applicable
|
||||
const hookEvent = getHookEventForStatusChange(previousStatus, updatedTask.status);
|
||||
if (hookEvent) {
|
||||
fireHook(hookEvent, updatedTask, previousStatus).catch((err) => {
|
||||
log.warn({ taskId: updatedTask.id, hookEvent }, 'Hook execution failed: %s', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -573,6 +587,11 @@ export class TaskService {
|
|||
status: task.status,
|
||||
});
|
||||
|
||||
// Fire onArchived hook
|
||||
fireHook('onArchived', task).catch((err) => {
|
||||
log.warn({ taskId: task.id }, 'onArchived hook failed: %s', err);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -195,6 +195,24 @@ export interface BudgetSettings {
|
|||
warningThreshold: number; // Percentage threshold for warning (0-100, default 80)
|
||||
}
|
||||
|
||||
/** Individual hook configuration */
|
||||
export interface HookConfig {
|
||||
enabled: boolean;
|
||||
webhook?: string; // URL to POST event payload
|
||||
notify?: boolean; // Send notification to configured channel
|
||||
logActivity?: boolean; // Record in activity log (default: true)
|
||||
}
|
||||
|
||||
/** Task lifecycle hooks settings */
|
||||
export interface HooksSettings {
|
||||
enabled: boolean;
|
||||
onCreated?: HookConfig;
|
||||
onStarted?: HookConfig;
|
||||
onBlocked?: HookConfig;
|
||||
onCompleted?: HookConfig;
|
||||
onArchived?: HookConfig;
|
||||
}
|
||||
|
||||
/** All feature settings combined */
|
||||
export interface FeatureSettings {
|
||||
board: BoardSettings;
|
||||
|
|
@ -204,6 +222,7 @@ export interface FeatureSettings {
|
|||
notifications: NotificationSettings;
|
||||
archive: ArchiveSettings;
|
||||
budget: BudgetSettings;
|
||||
hooks: HooksSettings;
|
||||
}
|
||||
|
||||
/** Default feature settings — matches current app behavior */
|
||||
|
|
@ -258,4 +277,8 @@ export const DEFAULT_FEATURE_SETTINGS: FeatureSettings = {
|
|||
monthlyCostLimit: 0, // 0 = no limit (dollars)
|
||||
warningThreshold: 80, // Warn at 80% of budget
|
||||
},
|
||||
hooks: {
|
||||
enabled: false, // Disabled by default
|
||||
// Individual hooks unconfigured by default
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue