@luckychen1993/pm-mcp 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -18
- package/dist/acceptance-tools.js +15 -10
- package/dist/audit-tools.js +28 -18
- package/dist/caller-workspace.js +244 -0
- package/dist/cli.js +11 -1
- package/dist/doctor.js +19 -16
- package/dist/git-state.js +41 -2
- package/dist/governance-audit.js +10 -0
- package/dist/governance-tools.js +54 -28
- package/dist/index-store.js +2 -1
- package/dist/index.js +12 -207
- package/dist/knowledge-tools.js +29 -20
- package/dist/paths.js +61 -3
- package/dist/project-tools.js +100 -22
- package/dist/prompts.js +91 -0
- package/dist/qoder-launch.js +131 -0
- package/dist/resources.js +116 -0
- package/dist/scan.js +4 -0
- package/dist/setup-project.js +1 -1
- package/dist/setup.js +49 -6
- package/dist/task-evidence.js +9 -9
- package/dist/task-tools.js +15 -10
- package/dist/tool-base.js +32 -7
- package/dist/typescript-semantic.js +28 -10
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/setup.js
CHANGED
|
@@ -11,15 +11,15 @@ import { PACKAGE_SPEC, SERVER_NAME } from "./version.js";
|
|
|
11
11
|
export { PACKAGE_SPEC, SERVER_NAME } from "./version.js";
|
|
12
12
|
import { appendCodexProjectConfig } from "./setup-project.js";
|
|
13
13
|
export { appendCodexProjectConfig, codexProjectServerKey } from "./setup-project.js";
|
|
14
|
-
const CLIENTS = ["codex", "claude", "zcode", "cursor", "vscode", "workbuddy"];
|
|
14
|
+
const CLIENTS = ["codex", "claude", "zcode", "cursor", "vscode", "workbuddy", "qoder"];
|
|
15
15
|
export function parseSetupArgs(argv) {
|
|
16
16
|
const options = { client: "auto", force: false, dryRun: false, help: false, project: false };
|
|
17
17
|
for (let index = 0; index < argv.length; index += 1) {
|
|
18
18
|
const arg = argv[index];
|
|
19
19
|
if (arg === "--client") {
|
|
20
20
|
const value = argv[index + 1];
|
|
21
|
-
if (!value || !["auto", "all", "codex", "claude", "zcode", "cursor", "vscode", "workbuddy", "print"].includes(value)) {
|
|
22
|
-
throw new Error("--client must be auto, all, codex, claude, zcode, cursor, vscode, workbuddy, or print");
|
|
21
|
+
if (!value || !["auto", "all", "codex", "claude", "zcode", "cursor", "vscode", "workbuddy", "qoder", "print"].includes(value)) {
|
|
22
|
+
throw new Error("--client must be auto, all, codex, claude, zcode, cursor, vscode, workbuddy, qoder, or print");
|
|
23
23
|
}
|
|
24
24
|
options.client = value;
|
|
25
25
|
index += 1;
|
|
@@ -48,8 +48,8 @@ export function parseSetupArgs(argv) {
|
|
|
48
48
|
throw new Error(`unknown setup argument: ${arg}`);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
if (options.local &&
|
|
52
|
-
throw new Error("--local 目前只支持 --client codex、workbuddy 或 print");
|
|
51
|
+
if (options.local && !["codex", "workbuddy", "qoder", "print"].includes(options.client))
|
|
52
|
+
throw new Error("--local 目前只支持 --client codex、workbuddy、qoder 或 print");
|
|
53
53
|
if (options.local && options.client === "codex" && !options.project)
|
|
54
54
|
throw new Error("Codex --local 需要 --project 明确项目根");
|
|
55
55
|
return options;
|
|
@@ -99,6 +99,9 @@ export function detectClients(context) {
|
|
|
99
99
|
// WorkBuddy 桌面版:`codebuddy`/`workbuddy` 命令、WORKBUDDY_CONFIG_DIR,或既有配置目录任一命中即视为已安装。
|
|
100
100
|
if (context.commandExists("workbuddy") || context.commandExists("codebuddy") || Boolean(env.WORKBUDDY_CONFIG_DIR) || exists(path.join(home, ".workbuddy-ai")) || exists(path.join(home, ".workbuddy")))
|
|
101
101
|
found.push("workbuddy");
|
|
102
|
+
// Qoder:CLI 命令、QODER_CONFIG_DIR,或既有数据目录任一命中即视为已安装。
|
|
103
|
+
if (context.commandExists("qoder") || context.commandExists("qoderclicn") || Boolean(env.QODER_CONFIG_DIR) || exists(path.join(home, ".qoder-cn")) || exists(path.join(home, ".qoder")))
|
|
104
|
+
found.push("qoder");
|
|
102
105
|
return found;
|
|
103
106
|
}
|
|
104
107
|
function asObject(value, label) {
|
|
@@ -191,6 +194,30 @@ export function workbuddyConfigPath(env, home, exists) {
|
|
|
191
194
|
return path.join(legacy, "mcp.json");
|
|
192
195
|
return path.join(current, "mcp.json");
|
|
193
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Qoder 用户级 MCP 配置路径:`QODER_CONFIG_DIR` → `~/.qoder-cn` → `~/.qoder`。
|
|
199
|
+
*
|
|
200
|
+
* Qoder 拉起 MCP 子进程时 cwd 是安装目录,所以条目指向 `dist/qoder-launch.js` 适配器,
|
|
201
|
+
* 由它按会话解析工作区后再钉 `--root`;配置本身同样不钉根,一条服务所有工作区。
|
|
202
|
+
*/
|
|
203
|
+
export function qoderConfigPath(env, home, exists) {
|
|
204
|
+
if (env.QODER_CONFIG_DIR)
|
|
205
|
+
return path.join(env.QODER_CONFIG_DIR, "settings.json");
|
|
206
|
+
const cn = path.join(home, ".qoder-cn");
|
|
207
|
+
if (exists(cn))
|
|
208
|
+
return path.join(cn, "settings.json");
|
|
209
|
+
const intl = path.join(home, ".qoder");
|
|
210
|
+
if (exists(intl))
|
|
211
|
+
return path.join(intl, "settings.json");
|
|
212
|
+
return path.join(cn, "settings.json");
|
|
213
|
+
}
|
|
214
|
+
/** 本包已构建的 Qoder 适配器入口 */
|
|
215
|
+
export function localQoderLaunch() {
|
|
216
|
+
const entry = fileURLToPath(new URL("../dist/qoder-launch.js", import.meta.url));
|
|
217
|
+
if (!fs.existsSync(entry) || !fs.lstatSync(entry).isFile())
|
|
218
|
+
throw new Error("本地入口未构建,请先 npm run build。");
|
|
219
|
+
return { command: process.execPath, args: [entry] };
|
|
220
|
+
}
|
|
194
221
|
function hasCodexServer(text) {
|
|
195
222
|
return /^\s*\[mcp_servers\.(?:pm-mcp|"pm-mcp")\]\s*$/m.test(text);
|
|
196
223
|
}
|
|
@@ -276,6 +303,16 @@ function configureClient(client, options, context, log, projectRoot) {
|
|
|
276
303
|
log(`[backup] ${backup}`);
|
|
277
304
|
return;
|
|
278
305
|
}
|
|
306
|
+
if (client === "qoder") {
|
|
307
|
+
const file = qoderConfigPath(context.env, context.home, context.exists);
|
|
308
|
+
log(`[${options.dryRun ? "plan" : "write"}] qoder: ${file}`);
|
|
309
|
+
// 发布版走 `pm-mcp launch` 适配器入口;--local 绑定本包已构建的 dist/qoder-launch.js。
|
|
310
|
+
const launch = options.local ? localQoderLaunch() : { command: "npx", args: ["-y", PACKAGE_SPEC, "launch"] };
|
|
311
|
+
const backup = writeJsonClientConfig(file, "standard", options.dryRun, launch);
|
|
312
|
+
if (backup)
|
|
313
|
+
log(`[backup] ${backup}`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
279
316
|
if (!context.commandExists("code"))
|
|
280
317
|
throw new Error("code was not found in PATH");
|
|
281
318
|
const definition = JSON.stringify({ name: SERVER_NAME, command: "npx", args: ["-y", PACKAGE_SPEC] });
|
|
@@ -286,11 +323,12 @@ function configureClient(client, options, context, log, projectRoot) {
|
|
|
286
323
|
function printHelp(log) {
|
|
287
324
|
log("pm-mcp setup — configure local AI coding clients");
|
|
288
325
|
log("");
|
|
289
|
-
log(`Usage: npx -y ${PACKAGE_SPEC} setup [--client auto|all|codex|claude|zcode|cursor|vscode|workbuddy|print] [--project [dir]] [--force] [--dry-run] [--local]`);
|
|
326
|
+
log(`Usage: npx -y ${PACKAGE_SPEC} setup [--client auto|all|codex|claude|zcode|cursor|vscode|workbuddy|qoder|print] [--project [dir]] [--force] [--dry-run] [--local]`);
|
|
290
327
|
log("Default auto mode configures every detected supported client.");
|
|
291
328
|
log("--project [dir]: 在 Codex 把该目录注册为钉定项目并初始化 .pm(新项目一条命令上手;默认当前目录)");
|
|
292
329
|
log("--local: 使用本包已构建的本地入口,适合未发布候选;--force 仅升级已确认项目的 command/args。");
|
|
293
330
|
log("workbuddy: 写入 <WorkBuddy 数据目录>/mcp.json(标准 mcpServers),不钉根——服务端按会话工作区解析项目根。");
|
|
331
|
+
log("qoder: 写入 <QODER_CONFIG_DIR|~/.qoder-cn>/settings.json,条目指向 qoder-launch 适配器;它按会话取工作区,一条配置服务所有项目。");
|
|
294
332
|
log("升级后运行 pm-mcp doctor --root <项目目录> 核验;现有客户端仍需重新连接。");
|
|
295
333
|
}
|
|
296
334
|
export function runSetup(argv, overrides = {}) {
|
|
@@ -360,11 +398,16 @@ export function runSetup(argv, overrides = {}) {
|
|
|
360
398
|
else if (selected.length === 1 && selected[0] === "workbuddy") {
|
|
361
399
|
log("完成。请在 WorkBuddy 的「连接器管理 → 自定义连接器」中确认信任 pm-mcp,再重开会话调用 get_status 核验。");
|
|
362
400
|
}
|
|
401
|
+
else if (selected.length === 1 && selected[0] === "qoder") {
|
|
402
|
+
log("完成。请在 Qoder 里执行 /mcp reload(或重开会话)并确认信任提示,再调用 get_status 核验项目根与解析依据。");
|
|
403
|
+
}
|
|
363
404
|
else {
|
|
364
405
|
// 多客户端时逐条给出各自的重连/授权要求,避免只提其中一个造成误导。
|
|
365
406
|
const notes = [];
|
|
366
407
|
if (selected.includes("workbuddy"))
|
|
367
408
|
notes.push("WorkBuddy 需在「连接器管理 → 自定义连接器」确认信任 pm-mcp");
|
|
409
|
+
if (selected.includes("qoder"))
|
|
410
|
+
notes.push("Qoder 需 /mcp reload 并确认信任");
|
|
368
411
|
if (selected.includes("codex"))
|
|
369
412
|
notes.push("Codex 需完全退出并重启");
|
|
370
413
|
log(notes.length > 0
|
package/dist/task-evidence.js
CHANGED
|
@@ -23,12 +23,12 @@ export function verifiedTaskEvidence(root, requested, files = []) {
|
|
|
23
23
|
const absolute = fs.realpathSync.native(path.resolve(root));
|
|
24
24
|
const anchors = files.map(projectFilePath);
|
|
25
25
|
if (anchors.length === 0)
|
|
26
|
-
throw new Error("完成开发/修复任务需要通过 files
|
|
26
|
+
throw new Error("完成开发/修复任务需要通过 files 关联实际实现或测试文件。请在 update_task 里传 files: [\"src/…\", \"test/…\"](相对路径);未验证的功能/杂项任务请改用 type=chore 或 checkpoint 存档。");
|
|
27
27
|
const excluded = ignoreMatcher(projectScanIgnores(root));
|
|
28
28
|
for (const rel of anchors) {
|
|
29
29
|
const parts = rel.split("/");
|
|
30
30
|
if (rel === "PROJECT.md" || parts.some((part, index) => (index < parts.length - 1 && SCAN_IGNORE_DIRS.has(part)) || excluded(parts.slice(0, index + 1).join("/"), index < parts.length - 1))) {
|
|
31
|
-
throw new Error(`任务文件不在源码验证范围内: ${rel}
|
|
31
|
+
throw new Error(`任务文件不在源码验证范围内: ${rel}。请改用 src/ 或 test/ 内的相对路径;.pm/、PROJECT.md、文档与 scan_ignore 排除项不能作为任务文件。`);
|
|
32
32
|
}
|
|
33
33
|
for (let index = 0; index < parts.length; index++) {
|
|
34
34
|
const target = path.join(absolute, ...parts.slice(0, index + 1));
|
|
@@ -40,7 +40,7 @@ export function verifiedTaskEvidence(root, requested, files = []) {
|
|
|
40
40
|
if (error.code !== "ENOENT")
|
|
41
41
|
throw error;
|
|
42
42
|
if (!changedGitFiles(absolute).includes(rel))
|
|
43
|
-
throw new Error(`任务关联文件不存在,也没有 Git 删除记录: ${rel}
|
|
43
|
+
throw new Error(`任务关联文件不存在,也没有 Git 删除记录: ${rel}。请核对路径拼写(相对路径、/ 分隔),确认文件真实存在于本仓库。`);
|
|
44
44
|
break;
|
|
45
45
|
}
|
|
46
46
|
if (stat.isSymbolicLink() || (index === parts.length - 1 ? !stat.isFile() : !stat.isDirectory())) {
|
|
@@ -51,7 +51,7 @@ export function verifiedTaskEvidence(root, requested, files = []) {
|
|
|
51
51
|
const directory = path.join(absolute, ".pm", "quality-runs");
|
|
52
52
|
for (const part of [path.join(absolute, ".pm"), directory]) {
|
|
53
53
|
if (!fs.existsSync(part))
|
|
54
|
-
throw new Error("完成开发/修复任务前请执行 run_quality_matrix
|
|
54
|
+
throw new Error("完成开发/修复任务前请执行 run_quality_matrix,缺少质量验证报告。先调用 run_quality_matrix(confirm_execute: true) 生成报告,全绿后再 update_task(done)。");
|
|
55
55
|
const stat = fs.lstatSync(part);
|
|
56
56
|
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
57
57
|
throw new Error("质量证据目录必须是项目内的普通目录。");
|
|
@@ -70,9 +70,9 @@ export function verifiedTaskEvidence(root, requested, files = []) {
|
|
|
70
70
|
}).sort((a, b) => b.runAt.localeCompare(a.runAt) || b.relative.localeCompare(a.relative));
|
|
71
71
|
const latest = reports[0];
|
|
72
72
|
if (!latest)
|
|
73
|
-
throw new Error("缺少质量验证报告,请先执行 run_quality_matrix。");
|
|
73
|
+
throw new Error("缺少质量验证报告,请先执行 run_quality_matrix(confirm_execute: true),全绿后再 update_task(done)。");
|
|
74
74
|
if (requested !== undefined && projectFilePath(requested) !== latest.relative)
|
|
75
|
-
throw new Error("
|
|
75
|
+
throw new Error("必须使用最新质量报告,不能绕过后来出现的验证结果。请省略 verification_run 参数(自动选最新),或用最新报告的路径重试。");
|
|
76
76
|
const parsed = reportSchema.safeParse(latest.value);
|
|
77
77
|
if (!parsed.success)
|
|
78
78
|
throw new Error("最新质量报告无效或格式过旧,请重新执行验证。");
|
|
@@ -80,7 +80,7 @@ export function verifiedTaskEvidence(root, requested, files = []) {
|
|
|
80
80
|
if (!report.ok || !report.execute || !report.source?.stable || report.source.before.sha256 !== report.source.after.sha256 ||
|
|
81
81
|
report.results.some((item) => item.status !== "passed" || item.exit_code !== 0 || item.signal !== null || item.output_truncated ||
|
|
82
82
|
[item.output_summary.failed, item.output_summary.skipped, item.output_summary.cancelled, item.output_summary.todo].some((n) => n !== null && n > 0))) {
|
|
83
|
-
throw new Error("
|
|
83
|
+
throw new Error("最新质量验证失败、未执行、输出不完整或存在跳过项,不能标记完成。请先修复失败原因,重跑 run_quality_matrix 直到全绿,再 update_task(done)。");
|
|
84
84
|
}
|
|
85
85
|
const tests = report.results.filter((item) => item.kind === "test" || item.kind === "coverage");
|
|
86
86
|
if (tests.length === 0 || tests.some((item) => item.output_summary.tests === 0))
|
|
@@ -100,11 +100,11 @@ export function verifiedTaskEvidence(root, requested, files = []) {
|
|
|
100
100
|
if (unitRoots.some((unit) => !within(absolute, unit)))
|
|
101
101
|
throw new Error("质量报告的执行目录不属于当前项目。");
|
|
102
102
|
if (anchors.some((file) => !unitRoots.some((unit) => within(unit, path.join(absolute, file))))) {
|
|
103
|
-
throw new Error("
|
|
103
|
+
throw new Error("质量报告的测试单元未覆盖任务关联文件所在目录。请把关联文件纳入测试覆盖,或将 files 改为质量报告已覆盖目录内的文件后重试。");
|
|
104
104
|
}
|
|
105
105
|
const current = fingerprintProject(absolute);
|
|
106
106
|
if (current.sha256 !== report.source.after.sha256)
|
|
107
|
-
throw new Error("
|
|
107
|
+
throw new Error("质量验证对应的源码已变化,证据过期,请重新验证。重新执行 run_quality_matrix 刷新证据,全绿后再 update_task(done)。");
|
|
108
108
|
const digest = createHash("sha256").update(latest.raw).digest("hex");
|
|
109
109
|
return `质量报告: ${latest.relative}\n报告 SHA-256: ${digest}\n源码 SHA-256: ${current.sha256}\n执行时间: ${report.run_at}\n证据等级: ${level}${policy ? `\n项目策略: ${policy.minimum};${policy.reason}` : "\n项目策略: tests_observed(默认)"}`;
|
|
110
110
|
}
|
package/dist/task-tools.js
CHANGED
|
@@ -10,8 +10,8 @@ import { appendSession } from "./session-log.js";
|
|
|
10
10
|
import { verifiedTaskEvidence } from "./task-evidence.js";
|
|
11
11
|
import { taskPage, taskPageText, TaskPageSchema } from "./task-pagination.js";
|
|
12
12
|
const stepSchema = z.object({ text: z.string(), done: z.boolean().optional() });
|
|
13
|
-
export function registerTaskTools(server,
|
|
14
|
-
toolW(server,
|
|
13
|
+
export function registerTaskTools(server, startupRoot) {
|
|
14
|
+
toolW(server, startupRoot, "add_task", "添加任务。长任务务必给 steps(显式步骤清单,断点续做的载体);走了捷径 type=debt 登记(债务账);修 bug type=fix。", {
|
|
15
15
|
title: z.string().min(1),
|
|
16
16
|
detail: z.string().optional(),
|
|
17
17
|
type: z.enum(["feature", "refactor", "fix", "chore", "debt"]).optional().describe("默认 feature"),
|
|
@@ -21,7 +21,8 @@ export function registerTaskTools(server, root) {
|
|
|
21
21
|
files: z.array(z.string()).optional().describe("关联文件(相对路径)"),
|
|
22
22
|
acceptance: z.string().optional().describe("验收标准"),
|
|
23
23
|
steps: z.array(stepSchema).optional().describe("步骤清单(长任务必填)"),
|
|
24
|
-
}, (args) => {
|
|
24
|
+
}, (args, ctx) => {
|
|
25
|
+
const root = ctx.root;
|
|
25
26
|
requireInitialized(root);
|
|
26
27
|
const roadmap = loadRoadmap(root);
|
|
27
28
|
if (args.milestone && !roadmap.milestones.some((m) => m.id === args.milestone)) {
|
|
@@ -59,7 +60,7 @@ export function registerTaskTools(server, root) {
|
|
|
59
60
|
: "";
|
|
60
61
|
return `✅ 任务 ${id}「${args.title}」已创建(${args.type ?? "feature"}/${args.milestone ?? "无里程碑"})。${tip}`;
|
|
61
62
|
});
|
|
62
|
-
toolR(server,
|
|
63
|
+
toolR(server, startupRoot, "list_tasks", "分页列出任务(默认不含 done/cancelled,每页25)。按原过滤条件传 next_cursor 可取下一页;数据变化会使游标失效。文字摘要与结构化分页结果同时返回,不漏掉折叠的中间任务。", {
|
|
63
64
|
status: z.enum(["backlog", "todo", "in_progress", "blocked", "done", "cancelled"]).optional(),
|
|
64
65
|
type: z.enum(["feature", "refactor", "fix", "chore", "debt"]).optional(),
|
|
65
66
|
milestone: z.string().optional(),
|
|
@@ -67,11 +68,12 @@ export function registerTaskTools(server, root) {
|
|
|
67
68
|
include_done: z.boolean().optional().describe("默认 false"),
|
|
68
69
|
page_size: z.number().int().min(1).max(100).optional().describe("默认25,受项目输出预算限制"),
|
|
69
70
|
cursor: z.string().min(1).max(1024).optional().describe("上页返回的 next_cursor;保留相同过滤条件"),
|
|
70
|
-
}, (args) => {
|
|
71
|
+
}, (args, ctx) => {
|
|
72
|
+
const root = ctx.root;
|
|
71
73
|
requireInitialized(root);
|
|
72
74
|
return JSON.stringify(taskPage(root, args, budgetLines(root) - 4));
|
|
73
75
|
}, { outputSchema: TaskPageSchema.shape, decode: (value) => { const page = TaskPageSchema.parse(JSON.parse(value)); return { text: taskPageText(page), data: page }; } });
|
|
74
|
-
toolW(server,
|
|
76
|
+
toolW(server, startupRoot, "update_task", "一次更新任务、checkpoint 与会话。checkpoint 默认记会话;完成时用 record_session:true 一并记录,不再另调 log_session;files 给实际关联文件。feature/fix 转 done 必须有最新成功且源码匹配的测试报告,verification_run 可指明报告。文字 verification 不能代替执行证据。", {
|
|
75
77
|
id: z.string(),
|
|
76
78
|
status: z.enum(["backlog", "todo", "in_progress", "blocked", "done", "cancelled"]).optional(),
|
|
77
79
|
title: z.string().optional(),
|
|
@@ -89,7 +91,8 @@ export function registerTaskTools(server, root) {
|
|
|
89
91
|
steps: z.array(stepSchema).optional().describe("整体替换步骤清单"),
|
|
90
92
|
step_done: z.number().int().min(1).optional().describe("勾选完成第 N 步"),
|
|
91
93
|
author: z.string().optional(),
|
|
92
|
-
}, (args, prepared) => {
|
|
94
|
+
}, (args, ctx, prepared) => {
|
|
95
|
+
const root = ctx.root;
|
|
93
96
|
requireInitialized(root);
|
|
94
97
|
const data = loadTasks(root);
|
|
95
98
|
const t = data.tasks.find((x) => x.id === args.id);
|
|
@@ -174,7 +177,8 @@ export function registerTaskTools(server, root) {
|
|
|
174
177
|
refreshDerived(root);
|
|
175
178
|
const stepsInfo = t.steps.length > 0 ? `,步骤 ${t.steps.filter((s) => s.done).length}/${t.steps.length}` : "";
|
|
176
179
|
return [`✅ ${t.id} 已更新(${t.status}${stepsInfo})。`, ...notes].join("\n");
|
|
177
|
-
}, (args) => {
|
|
180
|
+
}, (args, ctx) => {
|
|
181
|
+
const root = ctx.root;
|
|
178
182
|
requireInitialized(root);
|
|
179
183
|
const task = loadTasks(root).tasks.find((item) => item.id === args.id);
|
|
180
184
|
if (!task)
|
|
@@ -187,11 +191,12 @@ export function registerTaskTools(server, root) {
|
|
|
187
191
|
const record = args.record_session ?? (args.checkpoint !== undefined);
|
|
188
192
|
return { evidence, taskSnapshot: JSON.stringify(task), hashes: record ? captureFileHashes(root, args.files ?? []) : {} };
|
|
189
193
|
});
|
|
190
|
-
toolW(server,
|
|
194
|
+
toolW(server, startupRoot, "checkpoint", "断点存档:长任务做到一半(或上下文快满)时随手保存「当前进度 + 下一步动作」。新会话 get_status 会直接给出恢复点,不用靠回忆。", {
|
|
191
195
|
task_id: z.string(),
|
|
192
196
|
note: z.string().describe("当前进展到哪了"),
|
|
193
197
|
next_step: z.string().describe("下一个具体动作(新会话从这里续做)"),
|
|
194
|
-
}, (args) => {
|
|
198
|
+
}, (args, ctx) => {
|
|
199
|
+
const root = ctx.root;
|
|
195
200
|
requireInitialized(root);
|
|
196
201
|
const data = loadTasks(root);
|
|
197
202
|
const t = data.tasks.find((x) => x.id === args.task_id);
|
package/dist/tool-base.js
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import { loadProject } from "./store.js";
|
|
2
3
|
import { IdempotencyKeySchema, runCoalescedRead, runIdempotentWrite, runIdempotentWriteSync } from "./idempotency.js";
|
|
3
4
|
import { drainFoldSavings } from "./budget.js";
|
|
4
5
|
import { estimateTokens, logRuntime, logUsage } from "./usage-log.js";
|
|
6
|
+
import { resolveRequestRoot } from "./caller-workspace.js";
|
|
7
|
+
import { rootResolution } from "./paths.js";
|
|
8
|
+
/** 客户端共用一个常驻服务进程时,启动根只是默认值;每次调用都要重新认领调用方工作区 */
|
|
9
|
+
export function requestContext(tool, args, startupRoot) {
|
|
10
|
+
const resolution = resolveRequestRoot({
|
|
11
|
+
tool,
|
|
12
|
+
args,
|
|
13
|
+
startupRoot,
|
|
14
|
+
startupSource: rootResolution()?.source ?? "unknown",
|
|
15
|
+
});
|
|
16
|
+
return { root: resolution.root, rootSource: resolution.source, ...(resolution.candidates ? { rootCandidates: resolution.candidates } : {}) };
|
|
17
|
+
}
|
|
18
|
+
/** 无法自动认领工作区时的显式逃生口:任何工具都可以直接指定项目根 */
|
|
19
|
+
const rootOverride = z.string().optional().describe("项目根绝对路径。仅当服务端报告无法确定调用方工作区(root_source 含「未猜测」)时才需要传。");
|
|
20
|
+
function readSchema(schema) {
|
|
21
|
+
return { ...schema, root: rootOverride };
|
|
22
|
+
}
|
|
5
23
|
/** 工具/资源调用结束后写使用日志(含本次折叠省下的 token 估算);绝不抛错。 */
|
|
6
24
|
export function recordUsage(root, tool, kind, started, outcome) {
|
|
7
25
|
try {
|
|
@@ -25,11 +43,13 @@ export function recordUsage(root, tool, kind, started, outcome) {
|
|
|
25
43
|
}
|
|
26
44
|
}
|
|
27
45
|
/** 只读工具:跨 Agent/进程合并完全相同的并行请求,短窗复用结果。 */
|
|
28
|
-
export function toolR(server,
|
|
29
|
-
server.registerTool(name, { description, inputSchema: schema, ...(presentation ? { outputSchema: presentation.outputSchema } : {}), annotations: { readOnlyHint: true } }, (async (args) => {
|
|
46
|
+
export function toolR(server, startupRoot, name, description, schema, fn, presentation) {
|
|
47
|
+
server.registerTool(name, { description, inputSchema: readSchema(schema), ...(presentation ? { outputSchema: presentation.outputSchema } : {}), annotations: { readOnlyHint: true } }, (async (args) => {
|
|
48
|
+
const ctx = requestContext(name, (args ?? {}), startupRoot);
|
|
49
|
+
const root = ctx.root;
|
|
30
50
|
const started = Date.now();
|
|
31
51
|
try {
|
|
32
|
-
const result = await runCoalescedRead(root, presentation ? `${name}:${presentation.cacheVersion ?? "structured-v1"}` : name, args, fn);
|
|
52
|
+
const result = await runCoalescedRead(root, presentation ? `${name}:${presentation.cacheVersion ?? "structured-v1"}` : name, args, () => fn(args, ctx));
|
|
33
53
|
const decoded = presentation?.decode(result.text);
|
|
34
54
|
const text = decoded?.text ?? result.text;
|
|
35
55
|
recordUsage(root, name, "read", started, {
|
|
@@ -53,15 +73,18 @@ const idempotencyDescription = "多 Agent 执行同一业务时传相同 idempot
|
|
|
53
73
|
function writeSchema(schema) {
|
|
54
74
|
return {
|
|
55
75
|
...schema,
|
|
76
|
+
root: rootOverride,
|
|
56
77
|
idempotency_key: IdempotencyKeySchema.optional().describe("跨 Agent 业务幂等键,建议格式 task-id:operation,如 T-123:add-task"),
|
|
57
78
|
};
|
|
58
79
|
}
|
|
59
80
|
/** 同步写工具:业务幂等占位 + 跨进程账本锁覆盖完整读-改-写。 */
|
|
60
|
-
export function toolW(server,
|
|
81
|
+
export function toolW(server, startupRoot, name, description, schema, fn, prepare) {
|
|
61
82
|
server.registerTool(name, { description: `${description} ${idempotencyDescription}`, inputSchema: writeSchema(schema), annotations: { readOnlyHint: false } }, ((args) => {
|
|
83
|
+
const ctx = requestContext(name, (args ?? {}), startupRoot);
|
|
84
|
+
const root = ctx.root;
|
|
62
85
|
const started = Date.now();
|
|
63
86
|
try {
|
|
64
|
-
const result = runIdempotentWriteSync(root, name, args, fn, prepare);
|
|
87
|
+
const result = runIdempotentWriteSync(root, name, args, (value, prepared) => fn(value, ctx, prepared), prepare ? (value) => prepare(value, ctx) : undefined);
|
|
65
88
|
const prefix = result.replayed ? `↩️ 幂等复用 ${result.key}\n` : result.pending ? `⏳ 幂等占位 ${result.key}\n` : "";
|
|
66
89
|
const text = prefix + result.text;
|
|
67
90
|
recordUsage(root, name, "write", started, {
|
|
@@ -82,11 +105,13 @@ export function toolW(server, root, name, description, schema, fn, prepare) {
|
|
|
82
105
|
}));
|
|
83
106
|
}
|
|
84
107
|
/** 已自带内部锁或异步执行的写工具:先跨进程占位,完成后缓存结果。 */
|
|
85
|
-
export function toolI(server,
|
|
108
|
+
export function toolI(server, startupRoot, name, description, schema, fn) {
|
|
86
109
|
server.registerTool(name, { description: `${description} ${idempotencyDescription}`, inputSchema: writeSchema(schema), annotations: { readOnlyHint: false } }, (async (args) => {
|
|
110
|
+
const ctx = requestContext(name, (args ?? {}), startupRoot);
|
|
111
|
+
const root = ctx.root;
|
|
87
112
|
const started = Date.now();
|
|
88
113
|
try {
|
|
89
|
-
const result = await runIdempotentWrite(root, name, args, fn);
|
|
114
|
+
const result = await runIdempotentWrite(root, name, args, (value) => fn(value, ctx));
|
|
90
115
|
const prefix = result.replayed ? `↩️ 幂等复用 ${result.key}\n` : result.pending ? `⏳ 幂等占位 ${result.key}\n` : "";
|
|
91
116
|
const text = prefix + result.text;
|
|
92
117
|
recordUsage(root, name, "write-async", started, {
|
|
@@ -13,6 +13,18 @@ function scriptKind(rel) {
|
|
|
13
13
|
function stringValue(node) {
|
|
14
14
|
return node && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) ? node.text : null;
|
|
15
15
|
}
|
|
16
|
+
function insideEvidenceProperty(node) {
|
|
17
|
+
let current = node;
|
|
18
|
+
while (current.parent && (ts.isArrayLiteralExpression(current.parent) || ts.isSpreadElement(current.parent))) {
|
|
19
|
+
current = current.parent;
|
|
20
|
+
}
|
|
21
|
+
const parent = current.parent;
|
|
22
|
+
if (!parent || !ts.isPropertyAssignment(parent))
|
|
23
|
+
return false;
|
|
24
|
+
const name = parent.name;
|
|
25
|
+
const text = ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
|
|
26
|
+
return text !== undefined && /evidence/i.test(text);
|
|
27
|
+
}
|
|
16
28
|
function bindingNames(name) {
|
|
17
29
|
if (ts.isIdentifier(name))
|
|
18
30
|
return [name.text];
|
|
@@ -239,16 +251,22 @@ export function analyzeTypeScriptSource(rel, content) {
|
|
|
239
251
|
addReference(node, "grpc", firstArgument, 0.86);
|
|
240
252
|
}
|
|
241
253
|
else if (ts.isStringLiteralLike(node)) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
254
|
+
// Structured evidence fields (evidenceRef / lastEvidenceRef / evidence: [...]) carry
|
|
255
|
+
// data payloads pointing at evidence artifacts, not module specifiers; a path value
|
|
256
|
+
// like "apps/<pkg>/evidence/...-schema.json" must not become a contract link
|
|
257
|
+
// (upstream report T-095: phantom taiji-beidou-engine -> ai-gateway-service edge).
|
|
258
|
+
if (!insideEvidenceProperty(node)) {
|
|
259
|
+
const value = node.text;
|
|
260
|
+
const pathPart = value.split("#", 1)[0].replace(/\\/g, "/");
|
|
261
|
+
const basename = pathPart.split("/").at(-1) ?? "";
|
|
262
|
+
// Extension constants such as ".proto" are metadata, not contract links.
|
|
263
|
+
// Requiring a real basename keeps AST evidence precise while still
|
|
264
|
+
// accepting both nearby files (service.proto) and relative paths.
|
|
265
|
+
if (/^[^.\s][^/\s]*\.(?:proto|graphql|gql)$/i.test(basename))
|
|
266
|
+
addReference(node, "contract", value, 0.98, true);
|
|
267
|
+
else if (/^[^.\s][^/\s]*(?:openapi|swagger|asyncapi|contracts?|schemas?)[^/\s]*\.(?:ya?ml|json)$/i.test(basename))
|
|
268
|
+
addReference(node, "contract", value, 0.95, true);
|
|
269
|
+
}
|
|
252
270
|
}
|
|
253
271
|
else if (ts.isTaggedTemplateExpression(node) && /^(?:gql|graphql)$/.test(expressionPath(node.tag))) {
|
|
254
272
|
addReference(node, "contract", "inline GraphQL", 0.96);
|
package/dist/version.js
CHANGED
|
@@ -7,4 +7,4 @@ export const PACKAGE_SPEC = `${metadata.name}@${VERSION}`;
|
|
|
7
7
|
export const RUNTIME_CONTRACT = "pagination-evidence-v1";
|
|
8
8
|
// 服务器工具目录契约(doctor 与独立巡检 health-check 都以此核对)。
|
|
9
9
|
// 新增/移除 MCP 工具时必须同步更新这里,并用 `pm-mcp probe` 实测复核。
|
|
10
|
-
export const TOOL_CATALOG_SIZE =
|
|
10
|
+
export const TOOL_CATALOG_SIZE = 54;
|