patchwarden 0.6.4 → 1.1.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.
Files changed (212) hide show
  1. package/README.en.md +18 -18
  2. package/README.md +20 -20
  3. package/dist/agents/agentRouter.d.ts +40 -0
  4. package/dist/agents/agentRouter.js +95 -0
  5. package/dist/config.d.ts +1 -1
  6. package/dist/config.js +6 -2
  7. package/dist/controlCenter.js +15 -2
  8. package/dist/doctor.js +32 -1
  9. package/dist/goal/acceptanceEngine.d.ts +62 -0
  10. package/dist/goal/acceptanceEngine.js +103 -0
  11. package/dist/goal/acceptanceTemplate.d.ts +10 -0
  12. package/dist/goal/acceptanceTemplate.js +104 -0
  13. package/dist/goal/goalGraph.d.ts +58 -0
  14. package/dist/goal/goalGraph.js +189 -0
  15. package/dist/goal/goalProgress.d.ts +81 -0
  16. package/dist/goal/goalProgress.js +216 -0
  17. package/dist/goal/goalStatus.d.ts +60 -0
  18. package/dist/goal/goalStatus.js +137 -0
  19. package/dist/goal/goalStore.d.ts +79 -0
  20. package/dist/goal/goalStore.js +211 -0
  21. package/dist/goal/handoffExport.d.ts +34 -0
  22. package/dist/goal/handoffExport.js +183 -0
  23. package/dist/goal/subgoalSync.d.ts +40 -0
  24. package/dist/goal/subgoalSync.js +72 -0
  25. package/dist/goal/worktreeManager.d.ts +96 -0
  26. package/dist/goal/worktreeManager.js +292 -0
  27. package/dist/logging.d.ts +44 -0
  28. package/dist/logging.js +68 -0
  29. package/dist/release/releaseGate.d.ts +99 -0
  30. package/dist/release/releaseGate.js +475 -0
  31. package/dist/runner/postTaskCleanup.d.ts +13 -0
  32. package/dist/runner/postTaskCleanup.js +147 -0
  33. package/dist/runner/runTask.js +50 -10
  34. package/dist/security/discoveryTokenStore.d.ts +62 -0
  35. package/dist/security/discoveryTokenStore.js +100 -0
  36. package/dist/security/toolInvocationGuard.d.ts +35 -0
  37. package/dist/security/toolInvocationGuard.js +153 -0
  38. package/dist/smoke-test.js +18 -10
  39. package/dist/taskRuntime.d.ts +17 -0
  40. package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
  41. package/dist/test/unit/acceptance-engine.test.js +228 -0
  42. package/dist/test/unit/agent-router.test.d.ts +1 -0
  43. package/dist/test/unit/agent-router.test.js +287 -0
  44. package/dist/test/unit/audit-checks.test.d.ts +1 -0
  45. package/dist/test/unit/audit-checks.test.js +350 -0
  46. package/dist/test/unit/diagnose-task.test.d.ts +1 -0
  47. package/dist/test/unit/diagnose-task.test.js +457 -0
  48. package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
  49. package/dist/test/unit/discovery-token-store.test.js +139 -0
  50. package/dist/test/unit/goal-graph.test.d.ts +1 -0
  51. package/dist/test/unit/goal-graph.test.js +298 -0
  52. package/dist/test/unit/goal-progress.test.d.ts +1 -0
  53. package/dist/test/unit/goal-progress.test.js +381 -0
  54. package/dist/test/unit/goal-status.test.d.ts +1 -0
  55. package/dist/test/unit/goal-status.test.js +215 -0
  56. package/dist/test/unit/goal-store.test.d.ts +1 -0
  57. package/dist/test/unit/goal-store.test.js +253 -0
  58. package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
  59. package/dist/test/unit/goal-subgoal-task.test.js +55 -0
  60. package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
  61. package/dist/test/unit/goal-tools-registry.test.js +190 -0
  62. package/dist/test/unit/handoff-export.test.d.ts +1 -0
  63. package/dist/test/unit/handoff-export.test.js +263 -0
  64. package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
  65. package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
  66. package/dist/test/unit/logging.test.js +127 -5
  67. package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
  68. package/dist/test/unit/post-task-cleanup.test.js +48 -0
  69. package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
  70. package/dist/test/unit/reconcile-tasks.test.js +456 -0
  71. package/dist/test/unit/release-gate.test.d.ts +1 -0
  72. package/dist/test/unit/release-gate.test.js +242 -0
  73. package/dist/test/unit/safe-views.test.d.ts +1 -0
  74. package/dist/test/unit/safe-views.test.js +171 -0
  75. package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
  76. package/dist/test/unit/schema-drift-check.test.js +175 -0
  77. package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
  78. package/dist/test/unit/subgoal-sync.test.js +183 -0
  79. package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
  80. package/dist/test/unit/tool-invocation-guard.test.js +432 -0
  81. package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
  82. package/dist/test/unit/tool-usage-stats.test.js +300 -0
  83. package/dist/test/unit/toolSearch.test.d.ts +1 -0
  84. package/dist/test/unit/toolSearch.test.js +571 -0
  85. package/dist/test/unit/worktree-manager.test.d.ts +1 -0
  86. package/dist/test/unit/worktree-manager.test.js +176 -0
  87. package/dist/tools/auditTask.d.ts +103 -1
  88. package/dist/tools/auditTask.js +461 -8
  89. package/dist/tools/cancelTask.js +1 -1
  90. package/dist/tools/checkReleaseGate.d.ts +21 -0
  91. package/dist/tools/checkReleaseGate.js +22 -0
  92. package/dist/tools/createTask.d.ts +18 -2
  93. package/dist/tools/createTask.js +29 -1
  94. package/dist/tools/diagnoseTask.d.ts +45 -0
  95. package/dist/tools/diagnoseTask.js +347 -0
  96. package/dist/tools/discardWorktree.d.ts +24 -0
  97. package/dist/tools/discardWorktree.js +27 -0
  98. package/dist/tools/discoverTools.d.ts +11 -0
  99. package/dist/tools/discoverTools.js +39 -0
  100. package/dist/tools/explainTool.d.ts +11 -0
  101. package/dist/tools/explainTool.js +21 -0
  102. package/dist/tools/getTaskSummary.js +2 -1
  103. package/dist/tools/goalSubgoalTask.d.ts +51 -0
  104. package/dist/tools/goalSubgoalTask.js +110 -0
  105. package/dist/tools/healthCheck.d.ts +1 -0
  106. package/dist/tools/healthCheck.js +5 -1
  107. package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
  108. package/dist/tools/invokeDiscoveredTool.js +112 -0
  109. package/dist/tools/listTasks.d.ts +3 -1
  110. package/dist/tools/listTasks.js +14 -1
  111. package/dist/tools/mergeWorktree.d.ts +24 -0
  112. package/dist/tools/mergeWorktree.js +27 -0
  113. package/dist/tools/reconcileTasks.d.ts +41 -0
  114. package/dist/tools/reconcileTasks.js +352 -0
  115. package/dist/tools/registry.js +550 -0
  116. package/dist/tools/safeStatus.d.ts +31 -1
  117. package/dist/tools/safeStatus.js +43 -1
  118. package/dist/tools/safeViews.d.ts +256 -0
  119. package/dist/tools/safeViews.js +250 -0
  120. package/dist/tools/schemaDriftCheck.d.ts +46 -0
  121. package/dist/tools/schemaDriftCheck.js +80 -0
  122. package/dist/tools/toolCatalog.d.ts +4 -3
  123. package/dist/tools/toolCatalog.js +33 -11
  124. package/dist/tools/toolRegistry.d.ts +61 -0
  125. package/dist/tools/toolRegistry.js +724 -0
  126. package/dist/tools/toolSearch.d.ts +110 -0
  127. package/dist/tools/toolSearch.js +331 -0
  128. package/dist/tools/toolUsageStats.d.ts +41 -0
  129. package/dist/tools/toolUsageStats.js +116 -0
  130. package/dist/tools/waitForTask.js +1 -0
  131. package/dist/version.d.ts +2 -2
  132. package/dist/version.js +2 -2
  133. package/dist/watcherStatus.d.ts +29 -0
  134. package/dist/watcherStatus.js +92 -1
  135. package/package.json +3 -2
  136. package/scripts/checks/control-smoke.js +2 -2
  137. package/scripts/checks/doctor-smoke.js +1 -0
  138. package/scripts/checks/lifecycle-smoke.js +13 -9
  139. package/scripts/checks/mcp-manifest-check.js +53 -6
  140. package/scripts/checks/mcp-smoke.js +36 -9
  141. package/scripts/checks/watcher-supervisor-smoke.js +2 -2
  142. package/src/agents/agentRouter.ts +149 -0
  143. package/src/config.ts +9 -3
  144. package/src/controlCenter.ts +17 -3
  145. package/src/doctor.ts +36 -1
  146. package/src/goal/acceptanceEngine.ts +160 -0
  147. package/src/goal/acceptanceTemplate.ts +121 -0
  148. package/src/goal/goalGraph.ts +225 -0
  149. package/src/goal/goalProgress.ts +301 -0
  150. package/src/goal/goalStatus.ts +234 -0
  151. package/src/goal/goalStore.ts +306 -0
  152. package/src/goal/handoffExport.ts +211 -0
  153. package/src/goal/subgoalSync.ts +82 -0
  154. package/src/goal/worktreeManager.ts +404 -0
  155. package/src/logging.ts +91 -0
  156. package/src/release/releaseGate.ts +567 -0
  157. package/src/runner/postTaskCleanup.ts +154 -0
  158. package/src/runner/runTask.ts +49 -10
  159. package/src/security/discoveryTokenStore.ts +157 -0
  160. package/src/security/toolInvocationGuard.ts +251 -0
  161. package/src/smoke-test.ts +15 -7
  162. package/src/taskRuntime.ts +17 -0
  163. package/src/test/unit/acceptance-engine.test.ts +261 -0
  164. package/src/test/unit/agent-router.test.ts +342 -0
  165. package/src/test/unit/audit-checks.test.ts +567 -0
  166. package/src/test/unit/diagnose-task.test.ts +544 -0
  167. package/src/test/unit/discovery-token-store.test.ts +181 -0
  168. package/src/test/unit/goal-graph.test.ts +347 -0
  169. package/src/test/unit/goal-progress.test.ts +538 -0
  170. package/src/test/unit/goal-status.test.ts +270 -0
  171. package/src/test/unit/goal-store.test.ts +318 -0
  172. package/src/test/unit/goal-subgoal-task.test.ts +72 -0
  173. package/src/test/unit/goal-tools-registry.test.ts +243 -0
  174. package/src/test/unit/handoff-export.test.ts +295 -0
  175. package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
  176. package/src/test/unit/logging.test.ts +177 -5
  177. package/src/test/unit/post-task-cleanup.test.ts +53 -0
  178. package/src/test/unit/reconcile-tasks.test.ts +551 -0
  179. package/src/test/unit/release-gate.test.ts +314 -0
  180. package/src/test/unit/safe-views.test.ts +184 -0
  181. package/src/test/unit/schema-drift-check.test.ts +258 -0
  182. package/src/test/unit/subgoal-sync.test.ts +236 -0
  183. package/src/test/unit/tool-invocation-guard.test.ts +542 -0
  184. package/src/test/unit/tool-usage-stats.test.ts +384 -0
  185. package/src/test/unit/toolSearch.test.ts +756 -0
  186. package/src/test/unit/worktree-manager.test.ts +247 -0
  187. package/src/tools/auditTask.ts +831 -402
  188. package/src/tools/cancelTask.ts +1 -1
  189. package/src/tools/checkReleaseGate.ts +45 -0
  190. package/src/tools/createTask.ts +64 -6
  191. package/src/tools/diagnoseTask.ts +460 -0
  192. package/src/tools/discardWorktree.ts +42 -0
  193. package/src/tools/discoverTools.ts +51 -0
  194. package/src/tools/explainTool.ts +26 -0
  195. package/src/tools/getTaskSummary.ts +2 -1
  196. package/src/tools/goalSubgoalTask.ts +170 -0
  197. package/src/tools/healthCheck.ts +4 -1
  198. package/src/tools/invokeDiscoveredTool.ts +163 -0
  199. package/src/tools/listTasks.ts +16 -2
  200. package/src/tools/mergeWorktree.ts +42 -0
  201. package/src/tools/reconcileTasks.ts +464 -0
  202. package/src/tools/registry.ts +618 -1
  203. package/src/tools/safeStatus.ts +73 -2
  204. package/src/tools/safeViews.ts +271 -0
  205. package/src/tools/schemaDriftCheck.ts +120 -0
  206. package/src/tools/toolCatalog.ts +34 -11
  207. package/src/tools/toolRegistry.ts +786 -0
  208. package/src/tools/toolSearch.ts +464 -0
  209. package/src/tools/toolUsageStats.ts +151 -0
  210. package/src/tools/waitForTask.ts +1 -0
  211. package/src/version.ts +2 -2
  212. package/src/watcherStatus.ts +97 -1
@@ -0,0 +1,464 @@
1
+ /**
2
+ * v0.7.1: SafeToolSearch 搜索引擎 — 只读发现层。
3
+ *
4
+ * 实现 discoverTools(搜索候选工具)和 explainTool(展开工具详情)。
5
+ * 搜索算法:
6
+ * 1. 读取所有 ToolMeta
7
+ * 2. 按 profile / mode / riskCeiling 过滤
8
+ * 3. query 中英文归一化 + INTENT_TERMS 叠加
9
+ * 4. 按 name / title / tags / aliases / summary / description 加权打分
10
+ * 5. 排序返回 topK
11
+ * 6. 对被隐藏的高风险工具返回 hidden_results
12
+ */
13
+
14
+ import {
15
+ TOOL_RISK_RANK,
16
+ INTENT_TERMS,
17
+ type PatchWardenToolMeta,
18
+ type ToolRisk,
19
+ type ToolMode,
20
+ } from "./toolRegistry.js";
21
+ import type { ToolProfile } from "./toolCatalog.js";
22
+ import type { ToolUsageStats } from "./toolUsageStats.js";
23
+
24
+ // ── 类型定义 ──────────────────────────────────────────────────────
25
+
26
+ export interface DiscoverToolsInput {
27
+ query: string;
28
+ profile?: ToolProfile;
29
+ mode?: ToolMode;
30
+ maxResults?: number; // 默认 8
31
+ riskCeiling?: ToolRisk; // 最高允许风险等级
32
+ includeHighRisk?: boolean; // 默认 false(隐藏 command/release/credential_sensitive)
33
+ }
34
+
35
+ export interface DiscoverToolResult {
36
+ name: string;
37
+ title: string;
38
+ summary: string;
39
+ risk: ToolRisk;
40
+ schema_digest: string;
41
+ discoveryToken: string; // v0.8.1: server-side token id,由 tokenIssuer 生成
42
+ why: string;
43
+ }
44
+
45
+ export interface HiddenResultGroup {
46
+ risk: ToolRisk;
47
+ count: number;
48
+ reason: string;
49
+ }
50
+
51
+ // ── v0.9.0: 查询意图分类 ──────────────────────────────────────────
52
+
53
+ export type QueryIntent = "read" | "write" | "verify" | "release" | "diagnose" | "unknown";
54
+
55
+ /**
56
+ * 意图优先风险等级映射(roadmap 8.3)。
57
+ * verify 和 diagnose 意图在 ToolRisk 中没有 audit/diagnostic,
58
+ * 用 readonly/workspace_read_sensitive 替代(审计/诊断工具通常是只读的)。
59
+ */
60
+ export const INTENT_PRIORITY_RISK: Record<QueryIntent, ToolRisk[]> = {
61
+ read: ["readonly", "workspace_read_sensitive"],
62
+ write: ["workspace_write"],
63
+ verify: ["readonly", "workspace_read_sensitive"],
64
+ release: ["release"],
65
+ diagnose: ["readonly", "workspace_read_sensitive"],
66
+ unknown: [],
67
+ };
68
+
69
+ export interface DiscoverToolsOutput {
70
+ query: string;
71
+ intent: QueryIntent; // v0.9.0: 查询意图分类
72
+ results: DiscoverToolResult[];
73
+ hidden_results: HiddenResultGroup[];
74
+ total_matched: number;
75
+ total_hidden: number;
76
+ }
77
+
78
+ export interface ExplainToolInput {
79
+ name: string;
80
+ includeSchema?: boolean;
81
+ }
82
+
83
+ export interface ExplainToolOutput {
84
+ name: string;
85
+ title: string;
86
+ summary: string;
87
+ description: string;
88
+ risk: ToolRisk;
89
+ risk_rank: number;
90
+ profiles: ToolProfile[];
91
+ modes: ToolMode[];
92
+ tags: string[];
93
+ aliases: string[];
94
+ requires_confirmation: boolean;
95
+ schema_digest: string;
96
+ related_tools: string[];
97
+ input_schema?: unknown;
98
+ schema_drift_warning?: string; // v0.8.1: schema digest 不一致时警告
99
+ }
100
+
101
+ // ── 常量 ──────────────────────────────────────────────────────────
102
+
103
+ /** 高风险等级:默认隐藏(除非 includeHighRisk=true 或 riskCeiling 明确覆盖) */
104
+ const HIGH_RISK_LEVELS: ToolRisk[] = ["command", "release", "credential_sensitive"];
105
+
106
+ /** 规则匹配权重(ruleScore 分量) */
107
+ const RULE_WEIGHTS = {
108
+ name: 10,
109
+ title: 8,
110
+ summary: 4,
111
+ description: 2,
112
+ } as const;
113
+
114
+ /** 标签匹配权重(tagScore 分量) */
115
+ const TAG_WEIGHTS = {
116
+ tag: 6,
117
+ alias: 6,
118
+ } as const;
119
+
120
+ const DEFAULT_MAX_RESULTS = 8;
121
+
122
+ // ── 搜索引擎 ──────────────────────────────────────────────────────
123
+
124
+ /**
125
+ * 将 query 拆分为搜索 token(小写、去空格)。
126
+ * 同时展开中文意图词(INTENT_TERMS)。
127
+ */
128
+ function tokenizeQuery(query: string): string[] {
129
+ const normalized = query.toLowerCase().trim();
130
+ if (!normalized) return [];
131
+
132
+ const tokens = new Set<string>();
133
+
134
+ // 按空格和标点拆分英文/数字 token
135
+ const rawTokens = normalized.split(/[\s,,;;、|]+/).filter(Boolean);
136
+ for (const token of rawTokens) {
137
+ tokens.add(token);
138
+ }
139
+
140
+ // 整体 query 也作为一个 token(用于精确匹配)
141
+ if (normalized.length > 0) {
142
+ tokens.add(normalized);
143
+ }
144
+
145
+ // 展开中文意图词
146
+ for (const [intent, terms] of Object.entries(INTENT_TERMS)) {
147
+ if (normalized.includes(intent.toLowerCase())) {
148
+ for (const term of terms) {
149
+ tokens.add(term.toLowerCase());
150
+ }
151
+ }
152
+ }
153
+
154
+ return Array.from(tokens);
155
+ }
156
+
157
+ /**
158
+ * 计算单个工具对 token 的匹配得分,分为两个分量:
159
+ * - ruleScore:name/title/summary/description 的匹配得分
160
+ * - tagScore:tags/aliases 的匹配得分
161
+ * matchedFields 保持不变。
162
+ */
163
+ function scoreTool(
164
+ tool: PatchWardenToolMeta,
165
+ tokens: string[]
166
+ ): { ruleScore: number; tagScore: number; matchedFields: string[] } {
167
+ let ruleScore = 0;
168
+ let tagScore = 0;
169
+ const matchedFields: string[] = [];
170
+
171
+ if (tokens.length === 0) {
172
+ return { ruleScore: 0, tagScore: 0, matchedFields };
173
+ }
174
+
175
+ const nameLower = tool.name.toLowerCase();
176
+ const titleLower = tool.title.toLowerCase();
177
+ const summaryLower = tool.summary.toLowerCase();
178
+ const descLower = tool.description.toLowerCase();
179
+ const tagsLower = tool.tags.map((t) => t.toLowerCase());
180
+ const aliasesLower = tool.aliases.map((a) => a.toLowerCase());
181
+
182
+ for (const token of tokens) {
183
+ if (nameLower.includes(token)) {
184
+ ruleScore += RULE_WEIGHTS.name;
185
+ if (!matchedFields.includes("name")) matchedFields.push("name");
186
+ }
187
+ if (titleLower.includes(token)) {
188
+ ruleScore += RULE_WEIGHTS.title;
189
+ if (!matchedFields.includes("title")) matchedFields.push("title");
190
+ }
191
+ if (tagsLower.some((t) => t.includes(token) || token.includes(t))) {
192
+ tagScore += TAG_WEIGHTS.tag;
193
+ if (!matchedFields.includes("tags")) matchedFields.push("tags");
194
+ }
195
+ if (aliasesLower.some((a) => a.includes(token) || token.includes(a))) {
196
+ tagScore += TAG_WEIGHTS.alias;
197
+ if (!matchedFields.includes("aliases")) matchedFields.push("aliases");
198
+ }
199
+ if (summaryLower.includes(token)) {
200
+ ruleScore += RULE_WEIGHTS.summary;
201
+ if (!matchedFields.includes("summary")) matchedFields.push("summary");
202
+ }
203
+ if (descLower.includes(token)) {
204
+ ruleScore += RULE_WEIGHTS.description;
205
+ if (!matchedFields.includes("description")) matchedFields.push("description");
206
+ }
207
+ }
208
+
209
+ return { ruleScore, tagScore, matchedFields };
210
+ }
211
+
212
+ /**
213
+ * 判断工具是否应被隐藏(高风险 + includeHighRisk=false)。
214
+ */
215
+ function isHiddenByRisk(
216
+ tool: PatchWardenToolMeta,
217
+ riskCeiling: ToolRisk | undefined,
218
+ includeHighRisk: boolean
219
+ ): boolean {
220
+ // riskCeiling 优先:如果设了 riskCeiling,超过 ceiling 的一律隐藏
221
+ if (riskCeiling !== undefined) {
222
+ return TOOL_RISK_RANK[tool.risk] > TOOL_RISK_RANK[riskCeiling];
223
+ }
224
+ // 没设 riskCeiling 但 includeHighRisk=false:隐藏高风险
225
+ if (!includeHighRisk && HIGH_RISK_LEVELS.includes(tool.risk)) {
226
+ return true;
227
+ }
228
+ return false;
229
+ }
230
+
231
+ // ── v0.9.0: 混合排序公式分量 ──────────────────────────────────────
232
+
233
+ /**
234
+ * 风险等级对应的 bonus/penalty(roadmap 8.2)。
235
+ * 风险越低加分越高,风险越高扣分越多。
236
+ */
237
+ export function riskBonusOrPenalty(risk: ToolRisk): number {
238
+ switch (risk) {
239
+ case "readonly":
240
+ return 1;
241
+ case "workspace_read_sensitive":
242
+ return 0.5;
243
+ case "workspace_write":
244
+ return 0;
245
+ case "command":
246
+ return -0.5;
247
+ case "release":
248
+ return -1;
249
+ case "credential_sensitive":
250
+ return -1;
251
+ }
252
+ }
253
+
254
+ /**
255
+ * 混合排序公式(roadmap 8.2):
256
+ * final_score = rule_score * 0.45 + tag_score * 0.25 + profile_match * 0.15
257
+ * + history_score * 0.10 + risk_bonus_or_penalty * 0.05
258
+ *
259
+ * - ruleScore: name/title/summary/description 匹配得分
260
+ * - tagScore: tags/aliases 匹配得分
261
+ * - profileMatch: 0 或 1,工具是否属于当前 profile
262
+ * - historyScore: successRate * 5,范围 [0, 5];无历史数据时为 0
263
+ * - riskBonus: 来自 riskBonusOrPenalty
264
+ */
265
+ export function computeFinalScore(
266
+ ruleScore: number,
267
+ tagScore: number,
268
+ profileMatch: number,
269
+ historyScore: number,
270
+ riskBonus: number
271
+ ): number {
272
+ return (
273
+ ruleScore * 0.45 +
274
+ tagScore * 0.25 +
275
+ profileMatch * 0.15 +
276
+ historyScore * 0.10 +
277
+ riskBonus * 0.05
278
+ );
279
+ }
280
+
281
+ /**
282
+ * 查询意图分类(roadmap 8.3)。
283
+ * 不区分大小写,query 包含触发词即命中。
284
+ * 按 read → write → verify → release → diagnose 顺序匹配,第一个命中的意图即返回。
285
+ * 都不命中返回 "unknown"。
286
+ */
287
+ export function classifyQueryIntent(query: string): QueryIntent {
288
+ const lower = query.toLowerCase();
289
+ const readTriggers = ["read", "查看", "读取", "看看"];
290
+ const writeTriggers = ["fix", "patch", "修改", "修复"];
291
+ const verifyTriggers = ["test", "check", "验收", "检查"];
292
+ const releaseTriggers = ["release", "publish", "deploy", "发布"];
293
+ const diagnoseTriggers = ["卡住", "running", "stale", "旧任务"];
294
+
295
+ if (readTriggers.some((t) => lower.includes(t.toLowerCase()))) return "read";
296
+ if (writeTriggers.some((t) => lower.includes(t.toLowerCase()))) return "write";
297
+ if (verifyTriggers.some((t) => lower.includes(t.toLowerCase()))) return "verify";
298
+ if (releaseTriggers.some((t) => lower.includes(t.toLowerCase()))) return "release";
299
+ if (diagnoseTriggers.some((t) => lower.includes(t.toLowerCase()))) return "diagnose";
300
+ return "unknown";
301
+ }
302
+
303
+ // ── 公开 API ──────────────────────────────────────────────────────
304
+
305
+ /**
306
+ * discoverTools: 搜索候选工具,返回压缩摘要。
307
+ * v0.9.0: 升级为混合排序公式(5 维加权)+ 查询意图分类。
308
+ */
309
+ export function discoverTools(
310
+ input: DiscoverToolsInput,
311
+ registry: PatchWardenToolMeta[],
312
+ tokenIssuer?: (tool: PatchWardenToolMeta) => string,
313
+ usageStatsProvider?: (toolName: string) => ToolUsageStats | null
314
+ ): DiscoverToolsOutput {
315
+ const {
316
+ query,
317
+ profile,
318
+ mode,
319
+ maxResults = DEFAULT_MAX_RESULTS,
320
+ riskCeiling,
321
+ includeHighRisk = false,
322
+ } = input;
323
+
324
+ // v0.9.0: 查询意图分类
325
+ const intent = classifyQueryIntent(query);
326
+ const tokens = tokenizeQuery(query);
327
+
328
+ // 1. 按 profile / mode 过滤
329
+ let candidates = registry.filter((tool) => {
330
+ if (profile && !tool.profiles.includes(profile)) return false;
331
+ if (mode && !tool.modes.includes(mode)) return false;
332
+ return true;
333
+ });
334
+
335
+ // 2. 分离可见/隐藏工具(风险过滤在打分前执行,historyScore 不影响过滤)
336
+ const visible: Array<{ tool: PatchWardenToolMeta; finalScore: number; matchedFields: string[] }> = [];
337
+ const hidden: PatchWardenToolMeta[] = [];
338
+
339
+ for (const tool of candidates) {
340
+ if (isHiddenByRisk(tool, riskCeiling, includeHighRisk)) {
341
+ hidden.push(tool);
342
+ } else {
343
+ const { ruleScore, tagScore, matchedFields } = scoreTool(tool, tokens);
344
+
345
+ // profileMatch:工具是否属于当前 profile(input.profile 存在且 tool.profiles 包含)
346
+ const profileMatch = profile && tool.profiles.includes(profile) ? 1 : 0;
347
+
348
+ // historyScore:来自 usageStatsProvider,无 provider 或无数据时为 0
349
+ let historyScore = 0;
350
+ if (usageStatsProvider) {
351
+ const stats = usageStatsProvider(tool.name);
352
+ if (stats) {
353
+ historyScore = stats.successRate * 5;
354
+ }
355
+ }
356
+
357
+ // riskBonus:来自风险等级;无 usageStatsProvider 时不应用
358
+ // (退化为规则打分+意图微调,向后兼容 v0.7.1,spec SubTask 2.6)
359
+ const riskBonus = usageStatsProvider ? riskBonusOrPenalty(tool.risk) : 0;
360
+
361
+ // 混合排序公式
362
+ let finalScore = computeFinalScore(ruleScore, tagScore, profileMatch, historyScore, riskBonus);
363
+
364
+ // 意图微调:仅打破平局(+0.01)
365
+ if (intent !== "unknown" && INTENT_PRIORITY_RISK[intent].includes(tool.risk)) {
366
+ finalScore += 0.01;
367
+ }
368
+
369
+ visible.push({ tool, finalScore, matchedFields });
370
+ }
371
+ }
372
+
373
+ // 3. 排序:finalScore 降序,同分按 name 字母序
374
+ visible.sort((a, b) => {
375
+ if (b.finalScore !== a.finalScore) return b.finalScore - a.finalScore;
376
+ return a.tool.name.localeCompare(b.tool.name);
377
+ });
378
+
379
+ // 4. 取 topK
380
+ const topK = visible.slice(0, maxResults);
381
+
382
+ // 5. 构造结果
383
+ const results: DiscoverToolResult[] = topK.map(({ tool, matchedFields }) => ({
384
+ name: tool.name,
385
+ title: tool.title,
386
+ summary: tool.summary,
387
+ risk: tool.risk,
388
+ schema_digest: tool.inputSchemaDigest,
389
+ discoveryToken: tokenIssuer ? tokenIssuer(tool) : "",
390
+ why: matchedFields.length > 0
391
+ ? `matched: ${matchedFields.join(", ")}`
392
+ : "no direct match (included for browsing)",
393
+ }));
394
+
395
+ // 6. hidden_results 按风险等级分组统计
396
+ const hiddenByRisk = new Map<ToolRisk, number>();
397
+ for (const tool of hidden) {
398
+ hiddenByRisk.set(tool.risk, (hiddenByRisk.get(tool.risk) ?? 0) + 1);
399
+ }
400
+ const hidden_results: HiddenResultGroup[] = Array.from(hiddenByRisk.entries())
401
+ .sort(([a], [b]) => TOOL_RISK_RANK[a] - TOOL_RISK_RANK[b])
402
+ .map(([risk, count]) => ({
403
+ risk,
404
+ count,
405
+ reason: riskCeiling
406
+ ? `exceeds riskCeiling=${riskCeiling}`
407
+ : `includeHighRisk=false`,
408
+ }));
409
+
410
+ return {
411
+ query,
412
+ intent,
413
+ results,
414
+ hidden_results,
415
+ total_matched: results.length,
416
+ total_hidden: hidden.length,
417
+ };
418
+ }
419
+
420
+ /**
421
+ * explainTool: 展开单个工具详情。
422
+ */
423
+ export function explainTool(
424
+ input: ExplainToolInput,
425
+ registry: PatchWardenToolMeta[],
426
+ toolDefs?: Map<string, { inputSchema: unknown }>,
427
+ tokenPeeker?: (toolName: string) => { schemaDigest: string } | null
428
+ ): ExplainToolOutput | null {
429
+ const tool = registry.find((t) => t.name === input.name || t.aliases.includes(input.name));
430
+ if (!tool) return null;
431
+
432
+ const output: ExplainToolOutput = {
433
+ name: tool.name,
434
+ title: tool.title,
435
+ summary: tool.summary,
436
+ description: tool.description,
437
+ risk: tool.risk,
438
+ risk_rank: TOOL_RISK_RANK[tool.risk],
439
+ profiles: tool.profiles,
440
+ modes: tool.modes,
441
+ tags: tool.tags,
442
+ aliases: tool.aliases,
443
+ requires_confirmation: tool.requiresConfirmation,
444
+ schema_digest: tool.inputSchemaDigest,
445
+ related_tools: tool.relatedTools ?? [],
446
+ };
447
+
448
+ if (input.includeSchema && toolDefs) {
449
+ const def = toolDefs.get(tool.name);
450
+ if (def) {
451
+ output.input_schema = def.inputSchema;
452
+ }
453
+ }
454
+
455
+ // v0.8.1: schema drift 检测——比较 tokenPeeker 返回的 digest 与 registry 记录的 digest
456
+ if (input.includeSchema && tokenPeeker) {
457
+ const tokenRecord = tokenPeeker(tool.name);
458
+ if (tokenRecord && tokenRecord.schemaDigest !== tool.inputSchemaDigest) {
459
+ output.schema_drift_warning = `Schema digest mismatch: token recorded ${tokenRecord.schemaDigest}, current tool has ${tool.inputSchemaDigest}. Tool schema may have changed since discovery.`;
460
+ }
461
+ }
462
+
463
+ return output;
464
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * v0.9.0: 工具历史成功率聚合模块。
3
+ *
4
+ * 从 v0.8.1 引入的 invocation.log(JSON Lines 格式)中聚合每个工具的
5
+ * 调用统计,用于搜索排序。聚合维度包括:总调用数、成功率、平均耗时、
6
+ * 最近一次调用时间。
7
+ *
8
+ * 设计原则:
9
+ * - 只读,绝不修改 invocation.log。
10
+ * - 容错:损坏的 JSON 行或缺少必要字段的行被跳过,不抛错、不阻断聚合。
11
+ * - 不引入第三方依赖,仅使用 node:fs / node:path。
12
+ */
13
+
14
+ import { readFileSync, existsSync } from "node:fs";
15
+ import { join } from "node:path";
16
+
17
+ // ── 类型定义 ──────────────────────────────────────────────────────
18
+
19
+ /**
20
+ * 单个工具的历史调用统计。
21
+ */
22
+ export interface ToolUsageStats {
23
+ /** 总调用次数 */
24
+ totalCalls: number;
25
+ /** 成功率(0~1):result==="ok" 的调用数 / totalCalls */
26
+ successRate: number;
27
+ /** 平均耗时(毫秒):所有调用 duration_ms 的平均值 */
28
+ avgDurationMs: number;
29
+ /** 最近一次调用的 timestamp(ISO 字符串);无调用时为 null */
30
+ lastUsedAt: string | null;
31
+ }
32
+
33
+ // ── 内部聚合累加器 ────────────────────────────────────────────────
34
+
35
+ interface StatsAccumulator {
36
+ totalCalls: number;
37
+ successCount: number;
38
+ totalDurationMs: number;
39
+ lastUsedAt: string | null;
40
+ }
41
+
42
+ // ── 行级校验 ──────────────────────────────────────────────────────
43
+
44
+ /**
45
+ * 校验单行解析结果是否包含聚合所需必要字段。
46
+ * 必要字段:toolName(string)、result(string)、duration_ms(number)、timestamp(string)。
47
+ * 缺少任一字段或类型不符的行将被跳过。
48
+ */
49
+ function isValidEntry(entry: unknown): entry is {
50
+ toolName: string;
51
+ result: string;
52
+ duration_ms: number;
53
+ timestamp: string;
54
+ [key: string]: unknown;
55
+ } {
56
+ if (entry === null || typeof entry !== "object") return false;
57
+ const e = entry as Record<string, unknown>;
58
+ if (typeof e.toolName !== "string" || e.toolName.length === 0) return false;
59
+ if (typeof e.result !== "string") return false;
60
+ if (typeof e.duration_ms !== "number" || !Number.isFinite(e.duration_ms)) return false;
61
+ if (typeof e.timestamp !== "string" || e.timestamp.length === 0) return false;
62
+ return true;
63
+ }
64
+
65
+ // ── 聚合主函数 ────────────────────────────────────────────────────
66
+
67
+ /**
68
+ * 从 `<logsDir>/invocation.log`(JSON Lines 格式)聚合每个工具的历史调用统计。
69
+ *
70
+ * 行为约定:
71
+ * - `logsDir` 不存在或 invocation.log 不存在时返回空 Map(不抛错)。
72
+ * - invocation.log 为空文件时返回空 Map(不抛错)。
73
+ * - 损坏的 JSON 行被跳过(不阻断聚合、不抛错)。
74
+ * - 缺少必要字段(toolName/result/duration_ms/timestamp)的行被跳过。
75
+ * - successRate = result==="ok" 的数量 / totalCalls。
76
+ * - avgDurationMs = 所有调用 duration_ms 的算术平均。
77
+ * - lastUsedAt 取所有调用中最大的 timestamp(ISO 字符串按字典序即可比较)。
78
+ *
79
+ * @param logsDir 日志目录,函数会读取其中的 `invocation.log`
80
+ * @returns 按工具名(toolName)索引的统计 Map
81
+ */
82
+ export function aggregateUsageStats(logsDir: string): Map<string, ToolUsageStats> {
83
+ const stats = new Map<string, ToolUsageStats>();
84
+
85
+ // logsDir 不存在或 invocation.log 不存在时返回空 Map
86
+ const logFilePath = join(logsDir, "invocation.log");
87
+ let raw: string;
88
+ try {
89
+ if (!existsSync(logFilePath)) return stats;
90
+ raw = readFileSync(logFilePath, "utf-8");
91
+ } catch {
92
+ // 读取失败(权限、IO 错误等)按空结果返回,不抛错
93
+ return stats;
94
+ }
95
+
96
+ // 空文件直接返回空 Map
97
+ if (raw.length === 0) return stats;
98
+
99
+ const lines = raw.split("\n");
100
+ const accumulators = new Map<string, StatsAccumulator>();
101
+
102
+ for (const line of lines) {
103
+ const trimmed = line.trim();
104
+ // 跳过空行(包括文件末尾的尾随换行产生的空串)
105
+ if (trimmed.length === 0) continue;
106
+
107
+ // 损坏的 JSON 行跳过,不阻断聚合
108
+ let parsed: unknown;
109
+ try {
110
+ parsed = JSON.parse(trimmed);
111
+ } catch {
112
+ continue;
113
+ }
114
+
115
+ // 缺少必要字段的行跳过
116
+ if (!isValidEntry(parsed)) continue;
117
+
118
+ const { toolName, result, duration_ms, timestamp } = parsed;
119
+
120
+ let acc = accumulators.get(toolName);
121
+ if (!acc) {
122
+ acc = {
123
+ totalCalls: 0,
124
+ successCount: 0,
125
+ totalDurationMs: 0,
126
+ lastUsedAt: null,
127
+ };
128
+ accumulators.set(toolName, acc);
129
+ }
130
+
131
+ acc.totalCalls += 1;
132
+ if (result === "ok") acc.successCount += 1;
133
+ acc.totalDurationMs += duration_ms;
134
+ // ISO 8601 字符串按字典序即可正确比较时间先后
135
+ if (acc.lastUsedAt === null || timestamp > acc.lastUsedAt) {
136
+ acc.lastUsedAt = timestamp;
137
+ }
138
+ }
139
+
140
+ // 将累加器转换为最终统计结果
141
+ for (const [toolName, acc] of accumulators) {
142
+ stats.set(toolName, {
143
+ totalCalls: acc.totalCalls,
144
+ successRate: acc.totalCalls > 0 ? acc.successCount / acc.totalCalls : 0,
145
+ avgDurationMs: acc.totalCalls > 0 ? acc.totalDurationMs / acc.totalCalls : 0,
146
+ lastUsedAt: acc.lastUsedAt,
147
+ });
148
+ }
149
+
150
+ return stats;
151
+ }
@@ -4,6 +4,7 @@ import { getTaskSummary, type TaskSummaryResult } from "./getTaskSummary.js";
4
4
 
5
5
  const TERMINAL_STATUSES = new Set([
6
6
  "done",
7
+ "done_by_agent",
7
8
  "failed",
8
9
  "failed_verification",
9
10
  "failed_scope_violation",
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
- export const PATCHWARDEN_VERSION = "0.6.4";
2
- export const TOOL_SCHEMA_EPOCH = "2026-06-24-v7";
1
+ export const PATCHWARDEN_VERSION = "1.1.0";
2
+ export const TOOL_SCHEMA_EPOCH = "2026-06-27-v9";