feat(CF-762): Add Jira integration for session tracking

Sessions now auto-create CF Jira issues on start and post full session
output as comments on end, transitioning the issue to Done.

- Add src/services/jira.ts with createSessionIssue, addComment, transitionToDone
- Update session_start to create CF Jira issue and store key in sessions table
- Update session_end to post session output and close Jira issue
- Add migration 031 to archive local task tables (moved to Jira Cloud)
- Update .env.example with Jira Cloud env vars

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-02-08 07:23:18 +02:00
parent 1227e5b339
commit 63cba97b56
4 changed files with 462 additions and 8 deletions

View File

@@ -1,7 +1,9 @@
// Session management operations for database-driven session tracking
// Sessions auto-create CF Jira issues and post output on close (CF-762)
import { query, queryOne, execute } from '../db.js';
import { getEmbedding, formatEmbedding } from '../embeddings.js';
import { createSessionIssue, addComment, transitionToDone, updateIssueDescription } from '../services/jira.js';
interface SessionStartArgs {
session_id?: string;
@@ -58,8 +60,9 @@ interface Session {
}
/**
* Start a new session with metadata tracking
* Returns session_id and session_number
* Start a new session with metadata tracking.
* Auto-creates a CF Jira issue for session tracking.
* Returns session_id, session_number, and Jira issue key.
*/
export async function sessionStart(args: SessionStartArgs): Promise<string> {
const { session_id, project, working_directory, git_branch, initial_prompt, jira_issue_key } = args;
@@ -81,7 +84,32 @@ export async function sessionStart(args: SessionStartArgs): Promise<string> {
const session_number = result?.session_number || null;
return `Session started: ${id} (${project} #${session_number})`;
// Auto-create CF Jira issue for session tracking (non-blocking)
let sessionJiraKey: string | null = jira_issue_key || null;
if (!sessionJiraKey) {
try {
const jiraResult = await createSessionIssue({
sessionNumber: session_number,
project,
parentIssueKey: jira_issue_key || undefined,
branch: git_branch || undefined,
workingDirectory: working_directory || undefined,
});
if (jiraResult) {
sessionJiraKey = jiraResult.key;
// Store the auto-created Jira issue key
await execute(
`UPDATE sessions SET jira_issue_key = $1 WHERE id = $2`,
[sessionJiraKey, id]
);
}
} catch (err) {
console.error('session-mcp: Failed to create session Jira issue:', err);
}
}
const jiraInfo = sessionJiraKey ? ` [${sessionJiraKey}]` : '';
return `Session started: ${id} (${project} #${session_number})${jiraInfo}`;
}
/**
@@ -123,7 +151,8 @@ export async function sessionUpdate(args: SessionUpdateArgs): Promise<string> {
}
/**
* End session and generate summary with embedding
* End session and generate summary with embedding.
* Posts full session output as Jira comment and transitions session issue to Done.
*/
export async function sessionEnd(args: SessionEndArgs): Promise<string> {
const { session_id, summary, status = 'completed' } = args;
@@ -156,8 +185,8 @@ export async function sessionEnd(args: SessionEndArgs): Promise<string> {
}
// Get session details
const session = await queryOne<Session>(
`SELECT id, project, session_number, duration_minutes
const session = await queryOne<Session & { jira_issue_key: string | null }>(
`SELECT id, project, session_number, duration_minutes, jira_issue_key
FROM sessions WHERE id = $1`,
[session_id]
);
@@ -166,7 +195,100 @@ export async function sessionEnd(args: SessionEndArgs): Promise<string> {
return `Session ended: ${session_id}`;
}
return `Session ended: ${session.project} #${session.session_number} (${session.duration_minutes || 0}m)`;
// Post session output to Jira and close the session issue (non-blocking)
let jiraStatus = '';
if (session.jira_issue_key) {
try {
// Collect session output for Jira comment
const sessionOutput = await buildSessionOutput(session_id, session, summary);
// Post as comment
const commented = await addComment(session.jira_issue_key, sessionOutput);
// Update issue description with final summary
const descriptionUpdate = [
`## Session ${session.project} #${session.session_number}`,
`**Duration:** ${session.duration_minutes || 0} minutes`,
`**Status:** ${status}`,
`**Session ID:** ${session_id}`,
'',
`## Summary`,
summary,
].join('\n');
await updateIssueDescription(session.jira_issue_key, descriptionUpdate);
// Transition to Done
const transitioned = await transitionToDone(session.jira_issue_key);
jiraStatus = commented && transitioned
? ` [${session.jira_issue_key} → Done]`
: commented
? ` [${session.jira_issue_key} commented]`
: ` [${session.jira_issue_key} Jira update partial]`;
} catch (err) {
console.error('session-mcp: Failed to update session Jira issue:', err);
jiraStatus = ` [${session.jira_issue_key} Jira update failed]`;
}
}
return `Session ended: ${session.project} #${session.session_number} (${session.duration_minutes || 0}m)${jiraStatus}`;
}
/**
* Build full session output markdown for Jira comment.
*/
async function buildSessionOutput(
session_id: string,
session: { project: string | null; session_number: number | null; duration_minutes: number | null },
summary: string
): Promise<string> {
const lines: string[] = [];
lines.push(`# Session ${session.project} #${session.session_number}`);
lines.push(`Duration: ${session.duration_minutes || 0} minutes`);
lines.push('');
lines.push(`## Summary`);
lines.push(summary);
lines.push('');
// Get session notes
const notes = await query<{ note_type: string; content: string }>(
`SELECT note_type, content FROM session_notes WHERE session_id = $1 ORDER BY created_at`,
[session_id]
);
if (notes.length > 0) {
const grouped: Record<string, string[]> = {};
for (const n of notes) {
if (!grouped[n.note_type]) grouped[n.note_type] = [];
grouped[n.note_type].push(n.content);
}
for (const [type, items] of Object.entries(grouped)) {
const label = type.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
lines.push(`## ${label}`);
for (const item of items) {
lines.push(`- ${item}`);
}
lines.push('');
}
}
// Get commits
const commits = await query<{ commit_sha: string; repo: string; commit_message: string | null }>(
`SELECT commit_sha, repo, commit_message FROM session_commits WHERE session_id = $1 ORDER BY committed_at DESC`,
[session_id]
);
if (commits.length > 0) {
lines.push(`## Commits (${commits.length})`);
for (const c of commits) {
const msg = c.commit_message ? c.commit_message.split('\n')[0] : 'No message';
lines.push(`- ${c.commit_sha.substring(0, 7)} (${c.repo}): ${msg}`);
}
lines.push('');
}
return lines.join('\n');
}
/**