mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IJPL-224042 codex task skill
GitOrigin-RevId: 892892aac5d4a0306e288fa73c82eb29b857ba86
This commit is contained in:
committed by
intellij-monorepo-bot
parent
1345781da8
commit
46e72a8397
+108
-110
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user