关于 discover-tasks
用于开发工作流的任务发现技能,可从已配置来源收集任务、筛选并评分,再呈现候选项供用户选择。
主要功能
- 01从已配置来源获取任务
- 02排除已认领任务和关联 PR 的问题
- 03应用优先级筛选与评分
- 04呈现最多五项任务供选择
- 05将所选任务写入工作流状态
适用场景
- 01为工作流选择 GitHub 问题
- 02从 GitLab 或本地 Markdown 文件查找任务
- 03从已配置的自定义来源选择工作项
MCP 相关
自定义来源可使用 CLI、MCP 或技能工具列出问题。
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ agent-sh/ agentsys --skill discover-tasks安装后在 Claude Code 中输入/discover-tasks确认已加载。
详细介绍
discover-tasks
从已配置的来源中发现任务,验证任务,并供用户选择。
使用时机
在 /next-task 工作流的第 2 阶段(选择策略之后)调用。用户也可以在希望从已配置的来源中发现并选择任务时单独使用。
工作流
第 1 阶段:加载策略和已认领任务
// Use relative path from skill directory to plugin lib
// Path: skills/discover-tasks/ -> ../../lib/state/workflow-state.js
const workflowState = require('../../lib/state/workflow-state.js');
const state = workflowState.readState();
const policy = state.policy;
// Load claimed task IDs from registry (tasks[] contains worktree-manager claims)
const tasksRegistry = workflowState.readTasks();
const claimedIds = new Set(tasksRegistry.tasks.map(t => t.id));
第 2 阶段:按来源获取任务
来源类型:
github/gh-issues:GitHub CLIgh-projects:GitHub Projects(v2 看板)gitlab:GitLab CLIlocal/tasks-md:本地 Markdown 文件custom:CLI/MCP/Skill 工具other:由 Agent 解读描述
GitHub Issues:
# Fetch with pagination awareness
gh issue list --state open \
--json number,title,body,labels,assignees,createdAt,url \
--limit 100 > /tmp/gh-issues.json
GitLab Issues:
glab issue list --state opened --output json --per-page 100 > /tmp/glab-issues.json
本地 tasks.md:
for f in PLAN.md tasks.md TODO.md; do
[ -f "$f" ] && grep -n '^\s*- \[ \]' "$f"
done
GitHub Projects(v2):
// Extract gh-projects parameters from policy
const projectNumber = policy.taskSource.projectNumber;
const owner = policy.taskSource.owner;
if (!projectNumber || !owner) {
throw new Error('gh-projects source missing projectNumber or owner in policy.taskSource');
}
# Requires 'project' token scope. If permission error: gh auth refresh -s project
gh project item-list "$PROJECT_NUMBER" --owner "$OWNER" --format json --limit 100 > /tmp/gh-project-items.json
const fs = require('fs');
const raw = JSON.parse(fs.readFileSync('/tmp/gh-project-items.json', 'utf8'));
const items = (raw.items || []);
// Filter to ISSUE type only (exclude PULL_REQUEST, DRAFT_ISSUE)
const issues = items
.filter(item => item.content && item.content.type === 'ISSUE')
.map(item => ({
number: item.content.number,
title: item.content.title,
body: item.content.body || '',
labels: (item.content.labels || []).map(l => typeof l === 'object' ? l.name || '' : l).filter(Boolean),
url: item.content.url,
createdAt: item.content.createdAt
}));
[WARN] 如果 gh project item-list 返回权限错误,请告知用户:
Run: gh auth refresh -s project
自定义来源:
const { sources } = require('../../lib');
const capabilities = sources.getToolCapabilities(toolName);
// Execute capabilities.commands.list_issues
第 2.5 阶段:收集关联 PR 的 Issue(仅限 GitHub)
// Default for non-GitHub sources - always defined so Phase 3 filter is safe
let prLinkedIssues = new Set();
对于 GitHub 来源(policy.taskSource?.source === 'github'、'gh-issues' 或 'gh-projects'),获取所有开放的 PR,并构建一个包含已有相关 PR 的 Issue 编号的 Set。其他所有来源均跳过此步骤,直接进入第 3 阶段。
# Only run when policy.taskSource?.source is 'github', 'gh-issues', or 'gh-projects'
# Note: covers up to 100 open PRs. If repo has more, some linked issues may not be excluded.
gh pr list --state open --json number,title,body,headRefName --limit 100 > /tmp/gh-prs.json
const fs = require('fs');
try {
const prs = JSON.parse(fs.readFileSync('/tmp/gh-prs.json', 'utf8') || '[]');
for (const pr of prs) {
// 1. Branch name suffix: fix/some-thing-123 extracts 123
// Note: heuristic - branches like "release-2026" will false-positive on issue #2026.
// Patterns 2 and 3 are more precise; this is a best-effort supplement.
const branchMatch = (pr.headRefName || '').match(/-(\d+)$/);
if (branchMatch) prLinkedIssues.add(branchMatch[1]);
// 2. PR body closing keywords (GitHub's full keyword set, with word boundary)
if (pr.body) {
const bodyMatches = pr.body.matchAll(/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi);
for (const m of bodyMatches) prLinkedIssues.add(m[1]);
}
// 3. PR title (#N) convention - capture all occurrences
const titleMatches = (pr.title || '').matchAll(/\(#(\d+)\)/g);
for (const m of titleMatches) prLinkedIssues.add(m[1]);
}
} catch (e) {
console.log('[WARN] Could not parse open PRs, skipping PR-link filter:', e.message);
prLinkedIssues = new Set();
}
第 3 阶段:筛选和评分
排除已认领的任务:
const available = tasks.filter(t => !claimedIds.has(String(t.number || t.id)));
排除有开放 PR 的 Issue(仅限 GitHub):
const filtered = available.filter(t => {
const id = String(t.number || t.id);
if (prLinkedIssues.has(id)) {
console.log(`[INFO] Skipping #${id} - already has an open PR`);
return false;
}
return true;
});
应用优先级筛选条件(将 filtered 传入评分流程):
const LABEL_MAPS = {
bugs: ['bug', 'fix', 'error', 'defect'],
security: ['security', 'vulnerability', 'cve'],
features: ['enhancement', 'feature', 'improvement']
};
function filterByPriority(tasks, filter) {
if (filter === 'continue' || filter === 'all') return tasks;
const targetLabels = LABEL_MAPS[filter] || [];
return tasks.filter(t => {
const labels = (t.labels || []).map(l => (l.name || l).toLowerCase());
return targetLabels.some(target => labels.some(l => l.includes(target)));
});
}
const prioritized = filterByPriority(filtered, policy.priorityFilter);
// Assign score to each task so it is available for display in the UI
const topTasks = prioritized.map(t => ({ ...t, score: scoreTask(t) })).sort((a, b) => b.score - a.score);
为任务评分:
function scoreTask(task) {
let score = 0;
const labels = (task.labels || []).map(l => (l.name || l).toLowerCase());
// Priority labels
if (labels.some(l => l.includes('critical') || l.includes('p0'))) score += 100;
if (labels.some(l => l.includes('high') || l.includes('p1'))) score += 50;
if (labels.some(l => l.includes('security'))) score += 40;
// Quick wins
if (labels.some(l => l.includes('small') || l.includes('quick'))) score += 20;
// Age (older bugs get priority)
if (task.createdAt) {
const ageInDays = (Date.now() - new Date(task.createdAt)) / 86400000;
if (labels.includes('bug') && ageInDays > 30) score += 10;
}
return score;
}
第 4 阶段:通过 AskUserQuestion 呈现给用户
重要:标签最长不得超过 30 个字符(OpenCode 限制)。
function truncateLabel(num, title) {
const prefix = `#${num}: `;
const maxLen = 30 - prefix.length;
return title.length > maxLen
? prefix + title.substring(0, maxLen - 1) + '...'
: prefix + title;
}
const options = topTasks.slice(0, 5).map(task => ({
label: truncateLabel(task.number, task.title),
description: `Score: ${task.score} | ${(task.labels || []).slice(0, 2).join(', ')}`
}));
AskUserQuestion({
questions: [{
header: "Select Task",
question: "Which task should I work on?",
options,
multiSelect: false
}]
});
第 5 阶段:更新状态
workflowState.updateState({
task: {
id: String(selectedTask.number),
source: policy.taskSource?.source || policy.taskSource,
title: selectedTask.title,
description: selectedTask.body || '',
labels: selectedTask.labels?.map(l => l.name || l) || [],
url: selectedTask.url
}
});
workflowState.completePhase({
tasksAnalyzed: tasks.length,
selectedTask: selectedTask.number
});
第 6 阶段:发布评论(仅限 GitHub)
对于非 GitHub 来源(GitLab、本地、自定义),完全跳过此阶段。 对 github、gh-issues 和 gh-projects 来源运行此阶段。
# Only run for GitHub sources (github, gh-issues, gh-projects). Use policy.taskSource?.source from Phase 1 to check.
gh issue comment "$TASK_ID" --body "[BOT] Workflow started for this issue."
输出格式
## Task Selected
**Task**: #{id} - {title}
**Source**: {source}
**URL**: {url}
Proceeding to worktree setup...
错误处理
如果未找到任务:
- 建议创建 Issue
- 建议运行 /audit-project
- 建议使用 'all' 优先级筛选条件
约束
- 必须使用 AskUserQuestion 选择任务(不能使用纯文本)
- 标签最长不得超过 30 个字符
- 排除已被其他工作流认领的任务
- 排除已有开放 PR 的 Issue(GitHub 和 GitHub Projects 来源)
- PR 关联检测最多覆盖 100 个开放 PR(
--limit 100是获取上限) - 仅显示排名前 5 的任务
评论
支持 Markdown