IJPL-224042 codex task skill

GitOrigin-RevId: 892892aac5d4a0306e288fa73c82eb29b857ba86
This commit is contained in:
Vladimir Krivosheev
2026-01-15 20:43:22 +00:00
committed by intellij-monorepo-bot
parent 1345781da8
commit 46e72a8397
2 changed files with 137 additions and 175 deletions
+108 -110
View File
@@ -5,7 +5,7 @@ import {createMcpServer} from '../shared/mcp-rpc.mjs'
import {bd, bdJson, bdShowOne} from './bd-client.mjs'
import {addSectionComments, buildPendingNotes, parseNotes, prepareSectionUpdates} from './notes.mjs'
// Fetch ready children for an epic (used in task_status and task_epic resume)
// Fetch ready children for an epic (used in task_status and task_start resume)
async function getReadyChildren(epicId) {
const readyChildren = await bdJson(['ready', '--parent', epicId])
if (readyChildren.length > 0) {
@@ -23,6 +23,12 @@ function needsReview(issue) {
return priority <= 1 || issue.issue_type === 'bug' || issue.issue_type === 'feature' || issue.issue_type === 'epic'
}
async function createEpic(title, description) {
const id = await bd(['create', '--title', title, '--type', 'epic', '--description', description, '--acceptance', 'PENDING', '--design', 'PENDING', '--silent'])
await bd(['update', id, '--status', 'in_progress'])
return id
}
const tools = [
{
name: 'task_status',
@@ -36,14 +42,13 @@ const tools = [
}
},
{
name: 'task_epic',
description: 'Create or resume epic',
name: 'task_start',
description: 'Start task workflow (status + optional epic creation)',
inputSchema: {
type: 'object',
properties: {
title: {type: 'string', description: 'Epic title'},
description: {type: 'string', description: 'WHAT and WHY'},
resume: {type: 'string', description: 'Resume by ID'}
id: {type: 'string', description: 'Issue ID for full details'},
user_request: {type: 'string', description: 'User task description'}
}
}
},
@@ -126,121 +131,114 @@ const tools = [
}
]
const toolHandlers = {
task_status: async (args) => {
// Specific issue query - return full details (replaces task_show)
if (args.id) {
const issue = await bdShowOne(args.id)
if (!issue) {
return {error: `Issue ${args.id} not found`}
}
const parsedNotes = parseNotes(issue.notes, issue['comments'])
if (parsedNotes) {
issue.notes = parsedNotes
} else {
delete issue.notes
}
return issue
async function handleTaskStatus(args, options = {}) {
const shouldResume = options.resume === true
// Specific issue query - return full details (replaces task_show)
if (args.id) {
const issue = await bdShowOne(args.id)
if (!issue) {
return {error: `Issue ${args.id} not found`}
}
// Overview query - selection-oriented responses
const inProgress = await bdJson(['list', '--status', 'in_progress'])
const ready = await bdJson(['ready', '--limit', '5'])
// No in-progress issues
if (inProgress.length === 0) {
if (args.user_request) {
return {user_request: args.user_request}
}
if (ready.length > 0) {
return {
askUser: {
question: 'Which task would you like to work on?',
header: 'Task',
options: ready.map(r => ({label: r.title, description: r.id})),
multiSelect: false
}
}
}
return {empty: true}
}
// Single in-progress, no new request - just continue
if (inProgress.length === 1 && !args.user_request) {
const issue = inProgress[0]
const result = {id: issue.id, title: issue.title, status: issue.status, type: issue.issue_type}
// For epics, include ready children so Claude knows what to work on
if (issue.issue_type === 'epic') {
const readyChildren = await getReadyChildren(issue.id)
if (readyChildren) {
result.ready_children = readyChildren
}
}
return result
}
// Multiple in-progress OR conflict with user_request - user must select
const questionText = args.user_request
? 'New request - which task?'
: 'Which task to work on?'
// Find parent epic - only if unambiguous (single epic context)
const parentIds = [...new Set(inProgress.map(i => i.parent).filter(Boolean))]
const epicIds = inProgress.filter(i => i.issue_type === 'epic').map(i => i.id)
const parentEpic = parentIds.length === 1 ? parentIds[0]
: (parentIds.length === 0 && epicIds.length === 1 ? epicIds[0] : null)
return {
askUser: {
question: questionText,
header: 'Task',
options: [
...inProgress.map(i => ({label: i.title, description: i.id})),
...(parentEpic ? [{label: 'Create sub-task', description: `Under ${parentEpic}`}] : []),
{label: 'Start new task', description: 'Create a new epic'}
],
multiSelect: false
}
}
},
task_epic: async (args) => {
if (args.resume) {
const issue = await bdShowOne(args.resume)
if (!issue) {
return {error: `Issue ${args.resume} not found`}
}
await bd(['update', args.resume, '--status', 'in_progress'])
// Return full issue details (same as task_status(id)) to avoid redundant follow-up call
const parsedNotes = parseNotes(issue.notes, issue['comments'])
if (parsedNotes) {
issue.notes = parsedNotes
} else {
delete issue.notes
}
if (shouldResume) {
await bd(['update', args.id, '--status', 'in_progress'])
issue.is_new = false
// For epics, include ready children so Claude knows what to work on
issue.status = 'in_progress'
if (issue.issue_type === 'epic') {
const readyChildren = await getReadyChildren(args.resume)
const readyChildren = await getReadyChildren(args.id)
if (readyChildren) {
issue.ready_children = readyChildren
}
}
return issue
}
if (!args.title || !args.description) {
throw new Error('title and description required for new epic')
const parsedNotes = parseNotes(issue.notes, issue['comments'])
if (parsedNotes) {
issue.notes = parsedNotes
} else {
delete issue.notes
}
return issue
}
const id = await bd(['create', '--title', args.title, '--type', 'epic', '--description', args.description, '--acceptance', 'PENDING', '--design', 'PENDING', '--silent'])
await bd(['update', id, '--status', 'in_progress'])
// Overview query - selection-oriented responses
const inProgress = await bdJson(['list', '--status', 'in_progress'])
const ready = await bdJson(['ready', '--limit', '5'])
return {id, is_new: true}
},
// No in-progress issues
if (inProgress.length === 0) {
if (args.user_request) {
const title = args.user_request.trim()
if (title) {
const description = `USER REQUEST: ${args.user_request}`
const id = await createEpic(title, description)
const issue = await bdShowOne(id)
if (!issue) {
return {id, is_new: true}
}
const parsedNotes = parseNotes(issue.notes, issue['comments'])
if (parsedNotes) {
issue.notes = parsedNotes
} else {
delete issue.notes
}
issue.is_new = true
return issue
}
}
if (ready.length > 0) {
return {
askUser: {
question: 'Which task would you like to work on?',
header: 'Task',
options: ready.map(r => ({label: r.title, description: r.id})),
multiSelect: false
}
}
}
return {empty: true}
}
// Single in-progress, no new request - just continue
if (inProgress.length === 1 && !args.user_request) {
const issue = inProgress[0]
const result = {id: issue.id, title: issue.title, status: issue.status, type: issue.issue_type}
// For epics, include ready children so Claude knows what to work on
if (issue.issue_type === 'epic') {
const readyChildren = await getReadyChildren(issue.id)
if (readyChildren) {
result.ready_children = readyChildren
}
}
return result
}
// Multiple in-progress OR conflict with user_request - user must select
const questionText = args.user_request
? 'New request - which task?'
: 'Which task to work on?'
// Find parent epic - only if unambiguous (single epic context)
const parentIds = [...new Set(inProgress.map(i => i.parent).filter(Boolean))]
const epicIds = inProgress.filter(i => i.issue_type === 'epic').map(i => i.id)
const parentEpic = parentIds.length === 1 ? parentIds[0]
: (parentIds.length === 0 && epicIds.length === 1 ? epicIds[0] : null)
return {
askUser: {
question: questionText,
header: 'Task',
options: [
...inProgress.map(i => ({label: i.title, description: i.id})),
...(parentEpic ? [{label: 'Create sub-task', description: `Under ${parentEpic}`}] : []),
{label: 'Start new task', description: 'Create a new epic'}
],
multiSelect: false
}
}
}
const toolHandlers = {
task_status: handleTaskStatus,
task_start: (args) => handleTaskStatus(args, {resume: true}),
task_progress: async (args) => {
const issue = await bdShowOne(args.id)
+29 -65
View File
@@ -146,45 +146,35 @@ describe('task MCP integration', {timeout: 30000}, () => {
assert.deepEqual(result, {empty: true})
})
it('returns user_request when provided and no in-progress issues', async () => {
it('creates epic when user_request provided and no in-progress issues', async () => {
const result = await client.callTool('task_status', {user_request: 'test task'})
assert.deepEqual(result, {user_request: 'test task'})
assert.ok(result.id, 'should return id')
assert.ok(result.title === 'test task')
assert.ok(result.status === 'in_progress')
assert.ok(result.is_new === true)
})
})
describe('task_epic', () => {
it('creates new epic', async () => {
const result = await client.callTool('task_epic', {
title: 'Test Epic',
description: 'Test description'
})
describe('task_start', () => {
it('creates epic when user_request provided and no in-progress issues', async () => {
const result = await client.callTool('task_start', {user_request: 'start task'})
assert.ok(result.id, 'should return id')
assert.equal(result.is_new, true)
assert.ok(result.title === 'start task')
assert.ok(result.status === 'in_progress')
assert.ok(result.is_new === true)
})
it('resumes existing epic', async () => {
// Create epic first
const created = await client.callTool('task_epic', {
title: 'Resume Test',
description: 'Test'
})
it('returns issue for explicit id', async () => {
const epic = await client.callTool('task_start', {user_request: 'Start by id'})
// Close it via bd command
execSync(`bd close ${created.id} --reason "test"`, {cwd: testDir, stdio: 'pipe'})
// Resume it
const resumed = await client.callTool('task_epic', {resume: created.id})
assert.equal(resumed.id, created.id)
assert.equal(resumed.is_new, false)
const result = await client.callTool('task_start', {id: epic.id})
assert.ok(result.id === epic.id)
assert.ok(result.status === 'in_progress')
assert.ok(result.is_new === false)
})
it('resumes epic with ready_children', async () => {
// Create epic with sub-issues
const epic = await client.callTool('task_epic', {
title: 'Resume with Children',
description: 'Test'
})
const epic = await client.callTool('task_start', {user_request: 'Resume with Children'})
await client.callTool('task_decompose', {
epic_id: epic.id,
@@ -194,13 +184,11 @@ describe('task MCP integration', {timeout: 30000}, () => {
]
})
// Close epic
execSync(`bd close ${epic.id} --reason "test"`, {cwd: testDir, stdio: 'pipe'})
// Resume it - should include ready_children
const resumed = await client.callTool('task_epic', {resume: epic.id})
assert.equal(resumed.id, epic.id)
assert.equal(resumed.is_new, false)
const resumed = await client.callTool('task_start', {id: epic.id})
assert.ok(resumed.id === epic.id)
assert.ok(resumed.is_new === false)
assert.ok(resumed.ready_children, 'should have ready_children')
assert.equal(resumed.ready_children.length, 2)
})
@@ -208,10 +196,7 @@ describe('task MCP integration', {timeout: 30000}, () => {
describe('task_status with in-progress epic', () => {
it('returns single in-progress issue', async () => {
const epic = await client.callTool('task_epic', {
title: 'In Progress Epic',
description: 'Test'
})
const epic = await client.callTool('task_start', {user_request: 'In Progress Epic'})
const status = await client.callTool('task_status', {})
assert.equal(status.id, epic.id)
@@ -219,10 +204,7 @@ describe('task MCP integration', {timeout: 30000}, () => {
})
it('shows Create sub-task option for single epic', async () => {
await client.callTool('task_epic', {
title: 'Parent Epic',
description: 'Test'
})
await client.callTool('task_start', {user_request: 'Parent Epic'})
const status = await client.callTool('task_status', {user_request: 'new task'})
assert.ok(status.askUser, 'should return askUser')
@@ -233,10 +215,7 @@ describe('task MCP integration', {timeout: 30000}, () => {
})
it('returns ready_children for epic with decomposed sub-issues', async () => {
const epic = await client.callTool('task_epic', {
title: 'Epic with children',
description: 'Test'
})
const epic = await client.callTool('task_start', {user_request: 'Epic with children'})
await client.callTool('task_decompose', {
epic_id: epic.id,
@@ -255,10 +234,7 @@ describe('task MCP integration', {timeout: 30000}, () => {
describe('task_decompose', () => {
it('creates sub-issues under epic', async () => {
const epic = await client.callTool('task_epic', {
title: 'Decompose Test',
description: 'Epic to decompose'
})
const epic = await client.callTool('task_start', {user_request: 'Decompose Test'})
const result = await client.callTool('task_decompose', {
epic_id: epic.id,
@@ -273,10 +249,7 @@ describe('task MCP integration', {timeout: 30000}, () => {
})
it('can mark a child in progress on create', async () => {
const epic = await client.callTool('task_epic', {
title: 'Decompose Start Test',
description: 'Epic to decompose'
})
const epic = await client.callTool('task_start', {user_request: 'Decompose Start Test'})
const result = await client.callTool('task_decompose', {
epic_id: epic.id,
@@ -297,10 +270,7 @@ describe('task MCP integration', {timeout: 30000}, () => {
describe('task_progress', () => {
it('adds findings and decisions', async () => {
const epic = await client.callTool('task_epic', {
title: 'Progress Test',
description: 'Test'
})
const epic = await client.callTool('task_start', {user_request: 'Progress Test'})
const result = await client.callTool('task_progress', {
id: epic.id,
@@ -320,10 +290,7 @@ describe('task MCP integration', {timeout: 30000}, () => {
describe('task_done', () => {
it('closes epic with review prompt', async () => {
const epic = await client.callTool('task_epic', {
title: 'Close Test',
description: 'Test'
})
const epic = await client.callTool('task_start', {user_request: 'Close Test'})
// First call should prompt for review (epics need review)
const first = await client.callTool('task_done', {
@@ -361,10 +328,7 @@ describe('task MCP integration', {timeout: 30000}, () => {
describe('sub-task option ambiguity', () => {
it('hides sub-task option when multiple epics in progress', async () => {
// Create first epic
await client.callTool('task_epic', {
title: 'Epic 1',
description: 'First epic'
})
await client.callTool('task_start', {user_request: 'Epic 1'})
// Create second epic (also becomes in_progress)
const epic2Id = execSync(