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:
Brad Groux 2026-02-04 09:47:38 -06:00
parent 4a1cf7d36d
commit f176592259
9 changed files with 889 additions and 7 deletions

136
CLAUDE.md Normal file
View 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.

View file

@ -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.

View 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
View 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.

View file

@ -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

View file

@ -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), {

View 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;
}

View file

@ -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;
}

View file

@ -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
},
};