feat: Add version management, session tracking, and commit linking

Phase 1: Version Management
- Migration 006: Add git_tag, git_sha columns to versions
- New tools: version_add, version_list, version_show, version_update,
  version_release, version_assign_task
- Links versions to git tags for release tracking

Phase 2: Session/Task Activity Tracking
- Migration 007: Create task_activity table
- Track task changes per session (created, updated, status_change, closed)
- Session ID from env/cache file (usage-stats format)
- New tool: session_tasks to query tasks by session

Phase 3: Commit-Task Linking
- Migration 008: Create task_commits junction table
- New tools: task_commit_add, task_commit_remove, task_commits_list,
  task_link_commits (batch parsing)
- Stores SHA references only (Gitea MCP owns full data)
- Commits shown in task_show output

17 new MCP tools total. All migrations applied to docker-host postgres.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-01-10 10:28:21 +02:00
parent e12cccf718
commit 5015b1416f
9 changed files with 930 additions and 0 deletions

View File

@@ -4,6 +4,56 @@ import { query, queryOne, execute, getNextTaskId, getProjectKey } from '../db.js
import { getEmbedding, formatEmbedding } from '../embeddings.js';
import type { Task, ChecklistItem, TaskLink } from '../types.js';
import { getRecentDelegations } from './delegations.js';
import { getTaskCommits } from './commits.js';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
/**
* Get current session ID from environment or cache file
*/
function getSessionId(): string {
// Check environment first
if (process.env.CLAUDE_SESSION_ID) {
return process.env.CLAUDE_SESSION_ID;
}
// Try to read from cache file (usage-stats format)
const cacheFile = path.join(os.homedir(), '.claude', 'cache', '.current_session_id');
try {
const sessionId = fs.readFileSync(cacheFile, 'utf-8').trim();
if (sessionId) return sessionId;
} catch {
// File doesn't exist or can't be read
}
// Generate a new session ID
const now = new Date();
const timestamp = now.toISOString().replace(/[-:T]/g, '').slice(0, 15);
return `session_${timestamp}`;
}
/**
* Record task activity for session tracking
*/
async function recordActivity(
task_id: string,
activity_type: 'created' | 'updated' | 'status_change' | 'closed',
old_value?: string,
new_value?: string
): Promise<void> {
const session_id = getSessionId();
try {
await execute(
`INSERT INTO task_activity (task_id, session_id, activity_type, old_value, new_value)
VALUES ($1, $2, $3, $4, $5)`,
[task_id, session_id, activity_type, old_value || null, new_value || null]
);
} catch {
// Don't fail the main operation if activity tracking fails
console.error('Failed to record task activity');
}
}
interface TaskAddArgs {
title: string;
@@ -61,6 +111,9 @@ export async function taskAdd(args: TaskAddArgs): Promise<string> {
);
}
// Record activity for session tracking
await recordActivity(taskId, 'created', undefined, 'open');
return `Created: ${taskId}\n Title: ${title}\n Type: ${type}\n Priority: ${priority}\n Project: ${projectKey}${embedding ? '\n (embedded for semantic search)' : ''}`;
}
@@ -226,6 +279,12 @@ export async function taskShow(id: string): Promise<string> {
}
}
// Get commits
const commitHistory = await getTaskCommits(id);
if (commitHistory) {
output += commitHistory;
}
// Get recent delegations
const delegationHistory = await getRecentDelegations(id);
if (delegationHistory) {
@@ -239,6 +298,9 @@ export async function taskShow(id: string): Promise<string> {
* Close a task
*/
export async function taskClose(id: string): Promise<string> {
// Get current status for activity tracking
const task = await queryOne<{ status: string }>(`SELECT status FROM tasks WHERE id = $1`, [id]);
const result = await execute(
`UPDATE tasks
SET status = 'completed', completed_at = NOW(), updated_at = NOW()
@@ -250,6 +312,9 @@ export async function taskClose(id: string): Promise<string> {
return `Task not found: ${id}`;
}
// Record activity
await recordActivity(id, 'closed', task?.status, 'completed');
return `Closed: ${id}`;
}
@@ -259,6 +324,12 @@ export async function taskClose(id: string): Promise<string> {
export async function taskUpdate(args: TaskUpdateArgs): Promise<string> {
const { id, status, priority, type, title } = args;
// Get current values for activity tracking
const task = await queryOne<{ status: string }>(`SELECT status FROM tasks WHERE id = $1`, [id]);
if (!task) {
return `Task not found: ${id}`;
}
const updates: string[] = [];
const params: unknown[] = [];
let paramIndex = 1;
@@ -299,5 +370,12 @@ export async function taskUpdate(args: TaskUpdateArgs): Promise<string> {
return `Task not found: ${id}`;
}
// Record activity
if (status && status !== task.status) {
await recordActivity(id, 'status_change', task.status, status);
} else {
await recordActivity(id, 'updated');
}
return `Updated: ${id}`;
}