ktworkflow-mcp/src/tools/task.ts

182 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import type { CommitChecklistInput, PrepareTaskInput, TaskRecordInput, WorkflowContextInput } from '../types.js';
import { projectAliases } from '../core/constants.js';
import { detectRepoType } from '../core/project.js';
import { formatDateInShanghai, getRecentTaskRecords, getTaskContextIndex, listWorkspaceSkills, readText, resolveInsideRoot, resolveProject, workspaceRoot } from '../core/workspace.js';
import { buildGuardrails } from './guardrails.js';
import { inspectProject } from './inspect.js';
import { karpathyCommitChecklistItem, karpathyPrepareTaskStep } from './karpathy.js';
import { scanTaskRisk } from './envRisk.js';
import { createPageTestCase } from './testing.js';
export async function prepareTask(input: PrepareTaskInput): Promise<Record<string, unknown>> {
const project = resolveProject(input.project);
const taskType = input.taskType || 'general';
const inspection = await inspectProject({
includeGit: input.includeGit !== false,
project: input.project,
});
const guardrails = buildGuardrails(input);
const riskScan = await scanTaskRisk({
changedFiles: input.changedFiles || [],
project: input.project,
});
const pageTestCase =
input.includePageTest || taskType === 'page'
? createPageTestCase({
entryUrl: input.entryUrl,
project: input.project,
title: input.userRequest || `${project.label} 页面级测试`,
})
: null;
return {
guardrails,
inspection,
nextSteps: [
'阅读 workPacket.readFirst 中的上下文文件。',
'按 guardrails.beforeEdit 做开工前检查。',
karpathyPrepareTaskStep,
'按最小范围修改。',
'按 verification.commands 和 notes 做轻量验证。',
'有文件改动时更新 TASKS.md。',
'最终回复或提交前跑全局 review默认只深扫变更文件findings 先判定真实风险或工具误报,误报要沉淀到 ktWorkflow。',
],
pageTestCase,
project,
readFirst: [
'AGENTS.md',
'SKILLS.md',
'TASKS.md',
`${project.relativePath}/AGENTS.md`,
`${project.relativePath}/package.json`,
],
riskScan,
taskRecordDraft: buildTaskRecord({
content: input.userRequest || '待补充',
scope: [project.relativePath],
testCase: pageTestCase ? pageTestCase.title : '本次无页面测试或待补充。',
title: input.recordTitle || input.userRequest || `${project.label} 任务记录`,
verification: guardrails.guardrails.verification.commands.join('') || '待补充。',
}),
taskType,
userRequest: input.userRequest || '',
};
}
export function buildTaskRecord(input: TaskRecordInput): string {
const date = input.date || formatDateInShanghai();
const scope = (input.scope?.length ?? 0) > 0 ? input.scope!.join('、') : '待补充';
const content = input.content || '待补充';
const verification = input.verification || '待补充。';
const optionalTestCase = input.testCase ? `- 页面测试用例:${input.testCase}\n` : '';
return `### ${date}${input.title}\n\n- 范围:${scope}\n- 关键词:${content}\n${optionalTestCase}- 验证:${verification}\n`;
}
export function appendTaskRecord(input: TaskRecordInput): Record<string, unknown> {
const record = buildTaskRecord(input);
const tasksPath = resolveInsideRoot('TASKS.md');
const current = readFileSync(tasksPath, 'utf8');
const marker = current.includes('## 最近记录') ? '## 最近记录' : '## 改动记录';
const markerIndex = current.indexOf(marker);
if (markerIndex === -1) {
throw new Error('TASKS.md does not contain ## 最近记录 or ## 改动记录');
}
const insertAt = current.indexOf('\n', markerIndex);
const next = `${current.slice(0, insertAt + 1)}\n${record}\n${current.slice(insertAt + 1)}`;
if (!input.dryRun) {
writeFileSync(tasksPath, next, 'utf8');
}
return {
dryRun: input.dryRun,
record,
target: tasksPath,
};
}
export async function createCommitChecklist(input: CommitChecklistInput): Promise<Record<string, unknown>> {
const inspection = await inspectProject({
includeGit: true,
project: input.project,
});
const message = input.message || '';
const messageOk =
!message ||
/^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\([^)]+\))?: .*[-]/u.test(
message,
);
return {
checks: [
'确认只包含本次任务相关文件。',
karpathyCommitChecklistItem,
'先运行对应轻量验证命令。',
'提交前已跑全局 review且 findings 已修复;确认误报时优先修 ktWorkflow 过滤规则,避免污染后续上下文。',
'每个独立 Git 仓库单独 commit。',
'没有用户明确要求时不 push。',
],
inspection,
message,
messageOk,
messageRule: '英文 type 前缀 + 中文描述,例如 feat: 功能性提交',
};
}
export function readWorkflowContext(input: WorkflowContextInput): Record<string, unknown> {
const includeFullDocs = input.includeFullDocs === true;
const taskRecordCount = input.taskRecordCount ?? 3;
const projects = Object.keys(projectAliases).map((key) => {
const project = resolveProject(key);
return {
...project,
exists: existsSync(project.path),
repoType: existsSync(project.path) ? detectRepoType(project.path) : 'missing',
};
});
return {
docs: {
agents: readText('AGENTS.md'),
detailDocs: [
'docs/kt-context-semantics.md',
'docs/kt-workspace-structure.md',
'docs/kt-testing-workflows.md',
],
recentTasks: getRecentTaskRecords(taskRecordCount),
skills: readText('SKILLS.md'),
skillPackages: listWorkspaceSkills(),
taskIndex: getTaskContextIndex(),
tasks: includeFullDocs ? readText('TASKS.md') : undefined,
},
includeFullDocs,
obsidianIndex: {
note: 'Obsidian 只做文档/MOC 索引;代码上下文仍以 kt_inspect_project、kt_risk_scan、kt_suggest_verification 等工具锚定。',
tools: [
{
input: {
maxDocuments: 8,
},
name: 'kt_obsidian_context',
purpose: '按模块或关键词定位 docs/obsidian 索引,并返回对应 codeAnchors。',
},
{
input: {
includeGit: true,
project: '<codeAnchors.alias>',
},
name: 'kt_inspect_project',
purpose: '读取被 Obsidian 索引锚定的真实仓库状态和代码入口。',
},
],
},
projects,
workspaceRoot,
};
}