@miphamai/cli 0.49.0 → 0.51.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 (58) hide show
  1. package/bin/mipham.ts +1 -9
  2. package/package.json +1 -1
  3. package/skills/standard/github-ops.SKILL.md +1 -1
  4. package/skills/standard/web-access/references/cdp-api.md +2 -1
  5. package/skills/standard/web-access/scripts/cdp-proxy.mjs +75 -1
  6. package/skills/standard/web-access/scripts/check-deps.mjs +17 -2
  7. package/skills/standard/web-access/scripts/find-url.mjs +0 -0
  8. package/skills/standard/web-access/scripts/match-site.mjs +0 -0
  9. package/skills/standard/web-access.SKILL.md +10 -3
  10. package/src/agent/cross-session/discovery.ts +26 -1
  11. package/src/commands/project.ts +1 -1
  12. package/src/config/defaults.ts +8 -0
  13. package/src/core/auto-memory.ts +3 -3
  14. package/src/core/behavior-tasks.json +101 -0
  15. package/src/core/behavior-tasks.ts +44 -0
  16. package/src/core/constitution-loader.ts +0 -2
  17. package/src/core/context.ts +0 -1
  18. package/src/core/credential-masker/index.ts +1 -0
  19. package/src/core/credential-masker/matcher.ts +18 -7
  20. package/src/core/credential-masker/search.ts +73 -0
  21. package/src/core/credential-masker.ts +1 -0
  22. package/src/core/crsi-producer.ts +214 -0
  23. package/src/core/crsi-sandbox.ts +5 -1
  24. package/src/core/dream-engine.ts +0 -2
  25. package/src/core/eval-harness.ts +7 -0
  26. package/src/core/instructions.ts +11 -0
  27. package/src/core/memory/memory-loader.ts +3 -1
  28. package/src/core/permission-rules.ts +23 -12
  29. package/src/core/proposal-guard.ts +100 -0
  30. package/src/core/rules-loader.ts +8 -10
  31. package/src/core/task-runner-tasks.json +14 -0
  32. package/src/core/task-runner.ts +163 -0
  33. package/src/daemon/database.ts +0 -1
  34. package/src/daemon/feishu/adapter.ts +2 -1
  35. package/src/daemon/heartbeat.ts +83 -0
  36. package/src/daemon/index.ts +18 -8
  37. package/src/daemon/server.ts +78 -3
  38. package/src/daemon/session-manager.ts +5 -4
  39. package/src/daemon/telegram/adapter.ts +70 -0
  40. package/src/daemon/telegram/api.ts +45 -0
  41. package/src/daemon/telegram/env.ts +14 -0
  42. package/src/daemon/telegram/poller.ts +64 -0
  43. package/src/daemon/telegram/types.ts +10 -0
  44. package/src/i18n-core/locales/en-US.json +123 -0
  45. package/src/i18n-core/locales/zh-CN.json +123 -0
  46. package/src/index.tsx +6 -0
  47. package/src/shared/package-info.ts +4 -1
  48. package/src/shared/types.ts +2 -0
  49. package/src/skills/bundled-skill-assets.ts +5 -5
  50. package/src/skills/bundled-skills.ts +2 -2
  51. package/src/tools/agent/exit-plan.ts +1 -3
  52. package/src/tools/agent/memory.ts +2 -1
  53. package/src/tools/file/glob.ts +38 -23
  54. package/src/tools/file/grep.ts +96 -64
  55. package/src/tools/index.ts +5 -5
  56. package/src/ui/app.tsx +21 -1
  57. package/src/ui/commands.ts +269 -125
  58. package/src/ui/config-wizard.tsx +1 -1
@@ -12,6 +12,10 @@
12
12
 
13
13
  import type { CrsiInsight } from './auto-memory'
14
14
  import type { MetaRule } from './meta-rule-engine'
15
+ import type { Llm } from '../providers/llm'
16
+ import { readdirSync, appendFileSync, readFileSync, existsSync, mkdirSync, rmSync } from 'node:fs'
17
+ import { join } from 'node:path'
18
+ import { homedir } from 'node:os'
15
19
 
16
20
  /** 教训文件(相对仓库根)。预建,沙箱只能改已存在文件。 */
17
21
  export const LESSONS_FILE = 'apps/cli/crsi-lessons.md'
@@ -86,6 +90,9 @@ export function produceCrsiProposal(
86
90
  const signal = selectCrsiSignal(insights, metaRules)
87
91
  if (!signal) return null
88
92
 
93
+ // 幂等:同一信号的教训标题已在文件中,不再重复产出。
94
+ if (currentLessons.includes(`## ${signal.category}: ${signal.title}`)) return null
95
+
89
96
  const lesson = buildLessonContent(signal, timestamp)
90
97
  const newContent = currentLessons ? `${currentLessons.trimEnd()}\n\n${lesson}\n` : `${lesson}\n`
91
98
 
@@ -186,3 +193,210 @@ export function produceRuleProposal(
186
193
  originalContent: currentManagedRules,
187
194
  }
188
195
  }
196
+
197
+ // ── Producer 散文提议(块 1):从失败信号生成「改 skill 散文」提议 ──
198
+ // A1 边界首次实演:LLM 只作「生成」(候选),判定仍走确定性(guard 预筛 / 行为效果 / 人审)。
199
+
200
+ const PROSE_SELECT_PROMPT_VERSION = '1.0.0'
201
+
202
+ function buildSelectSkillPrompt(signal: CrsiSignal, skillFiles: string[]): string {
203
+ return [
204
+ `你是 CRSI producer(producer-prose-select v${PROSE_SELECT_PROMPT_VERSION})。给定失败信号,从候选 skill 文件列表中选出最相关的一个,返回其文件路径(只返回路径,一行,不要其他文字)。`,
205
+ '',
206
+ '失败信号:',
207
+ `- category: ${signal.category}`,
208
+ `- title: ${signal.title}`,
209
+ signal.severity ? `- severity: ${signal.severity}` : '',
210
+ `- suggestion: ${signal.suggestion}`,
211
+ `- evidence: ${signal.evidence.join(' | ')}`,
212
+ '',
213
+ '候选 skill 文件:',
214
+ ...skillFiles.map((f) => `- ${f}`),
215
+ ]
216
+ .filter(Boolean)
217
+ .join('\n')
218
+ }
219
+
220
+ async function collectLlmText(llm: Llm, prompt: string): Promise<string> {
221
+ let text = ''
222
+ const req = {
223
+ model: 'prose',
224
+ messages: [{ role: 'user' as const, content: prompt }],
225
+ systemPrompt: '',
226
+ }
227
+ for await (const chunk of llm.chat(req)) {
228
+ if (chunk.type === 'text' && chunk.content) text += chunk.content
229
+ }
230
+ return text.trim()
231
+ }
232
+
233
+ function extractFilePath(response: string, skillFiles: string[]): string | null {
234
+ for (const f of skillFiles) {
235
+ if (response.includes(f)) return f
236
+ }
237
+ return null
238
+ }
239
+
240
+ export async function selectTargetSkill(
241
+ signal: CrsiSignal,
242
+ llm: Llm,
243
+ skillFiles: string[],
244
+ ): Promise<string | null> {
245
+ if (skillFiles.length === 0) return null
246
+ const prompt = buildSelectSkillPrompt(signal, skillFiles)
247
+ const response = await collectLlmText(llm, prompt)
248
+ if (!response) return null
249
+ return extractFilePath(response, skillFiles)
250
+ }
251
+
252
+ const PROSE_GENERATE_PROMPT_VERSION = '1.0.0'
253
+
254
+ function buildGenerateProsePrompt(
255
+ signal: CrsiSignal,
256
+ filePath: string,
257
+ originalContent: string,
258
+ ): string {
259
+ return [
260
+ `你是 CRSI producer(producer-prose-generate v${PROSE_GENERATE_PROMPT_VERSION})。基于失败信号,改进目标 skill 的内容。`,
261
+ '',
262
+ '失败信号:',
263
+ `- category: ${signal.category}`,
264
+ `- title: ${signal.title}`,
265
+ `- suggestion: ${signal.suggestion}`,
266
+ `- evidence: ${signal.evidence.join(' | ')}`,
267
+ '',
268
+ `目标文件:${filePath}`,
269
+ '',
270
+ '当前内容:',
271
+ originalContent,
272
+ '',
273
+ '请返回改进后的完整 markdown(保持 YAML frontmatter 的 name/description 字段,正文针对失败信号做针对性改进)。只返回 markdown,不要额外说明。',
274
+ ].join('\n')
275
+ }
276
+
277
+ function stripMarkdownFence(text: string): string {
278
+ const match = text.match(/^```(?:markdown|md)?\s*\n([\s\S]*?)\n```\s*$/)
279
+ return match ? match[1]! : text
280
+ }
281
+
282
+ export async function generateProseContent(
283
+ signal: CrsiSignal,
284
+ llm: Llm,
285
+ filePath: string,
286
+ originalContent: string,
287
+ ): Promise<string | null> {
288
+ const prompt = buildGenerateProsePrompt(signal, filePath, originalContent)
289
+ const response = await collectLlmText(llm, prompt)
290
+ if (!response) return null
291
+ return stripMarkdownFence(response)
292
+ }
293
+
294
+ export interface ProseProposalResult {
295
+ filePath: string
296
+ newContent: string
297
+ originalContent: string
298
+ description: string
299
+ }
300
+
301
+ export async function produceProseProposal(
302
+ signal: CrsiSignal,
303
+ llm: Llm,
304
+ skillFiles: string[],
305
+ readSkill: (filePath: string) => string,
306
+ ): Promise<ProseProposalResult | null> {
307
+ const filePath = await selectTargetSkill(signal, llm, skillFiles)
308
+ if (!filePath) return null
309
+
310
+ let originalContent: string
311
+ try {
312
+ originalContent = readSkill(filePath)
313
+ } catch {
314
+ return null
315
+ }
316
+
317
+ const newContent = await generateProseContent(signal, llm, filePath, originalContent)
318
+ if (!newContent) return null
319
+
320
+ return { filePath, newContent, originalContent, description: signal.title }
321
+ }
322
+
323
+ const SKILL_DIRS: Array<[string, string]> = [
324
+ ['standard', '.SKILL.md'],
325
+ ['mipham', '.mipham-skill.md'],
326
+ ]
327
+
328
+ /** 收集仓库内所有 skill 文件(相对仓库根的路径),供 produceProseProposal 选目标。 */
329
+ export function collectSkillFiles(root: string): string[] {
330
+ const files: string[] = []
331
+ for (const [dir, ext] of SKILL_DIRS) {
332
+ let entries: string[] = []
333
+ try {
334
+ entries = readdirSync(join(root, 'apps', 'cli', 'skills', dir))
335
+ } catch {
336
+ continue
337
+ }
338
+ for (const entry of entries) {
339
+ if (entry.endsWith(ext)) files.push(`apps/cli/skills/${dir}/${entry}`)
340
+ }
341
+ }
342
+ return files
343
+ }
344
+
345
+ // ── 幂等去重(prose ledger) ──
346
+ // 散文提议(块 1)的幂等:同一失败信号只生成一次提议。与 --rule 路径「目标文件内 id marker」去重不同,
347
+ // 散文改的是 skill 内容(非追加 marker),故用 ~/.mipham 下的 append-only ledger 记录「已提议的信号」。
348
+
349
+ /** 散文提议的稳定 id(同 category + 同 title → 同 id,同 managedRuleId 的 hash 语义)。 */
350
+ export function proseProposalId(signal: CrsiSignal): string {
351
+ return `prose-${signal.category}-${stableHash(signal.title)}`
352
+ }
353
+
354
+ /** ledger 里的一条散文提议记录。 */
355
+ export interface ProseProposalRecord {
356
+ id: string
357
+ filePath: string
358
+ timestamp: string
359
+ }
360
+
361
+ function proseLedgerFile(): string {
362
+ return join(homedir(), '.mipham', 'crsi', 'prose-proposals.jsonl')
363
+ }
364
+
365
+ /** 该信号是否已生成过散文提议。 */
366
+ export function hasProposedProse(id: string): boolean {
367
+ try {
368
+ if (!existsSync(proseLedgerFile())) return false
369
+ const lines = readFileSync(proseLedgerFile(), 'utf-8').trim().split('\n').filter(Boolean)
370
+ return lines.some((line) => {
371
+ try {
372
+ return (JSON.parse(line) as { id?: string }).id === id
373
+ } catch {
374
+ return false
375
+ }
376
+ })
377
+ } catch {
378
+ return false
379
+ }
380
+ }
381
+
382
+ /** 追加一条散文提议记录(append-only,非关键——失败不影响提议本身)。 */
383
+ export function appendProseProposal(record: ProseProposalRecord): void {
384
+ try {
385
+ mkdirSync(join(homedir(), '.mipham', 'crsi'), { recursive: true })
386
+ appendFileSync(proseLedgerFile(), JSON.stringify(record) + '\n', 'utf-8')
387
+ } catch {
388
+ // ledger 非关键,失败不影响提议本身
389
+ }
390
+ }
391
+
392
+ /** 清空散文提议 ledger,返回清除的记录数(无文件时返回 0)。 */
393
+ export function clearProseProposals(): number {
394
+ try {
395
+ if (!existsSync(proseLedgerFile())) return 0
396
+ const lines = readFileSync(proseLedgerFile(), 'utf-8').trim().split('\n').filter(Boolean)
397
+ rmSync(proseLedgerFile(), { force: true })
398
+ return lines.length
399
+ } catch {
400
+ return 0
401
+ }
402
+ }
@@ -84,7 +84,6 @@ export interface CrsiSessionReport {
84
84
  // ── Constants ──
85
85
 
86
86
  const WORKTREE_PREFIX = 'crsi-sandbox-'
87
- const MAX_WORKTREE_AGE_MS = 30 * 60 * 1000 // 30 minutes
88
87
  const TEST_TIMEOUT_MS = 120_000 // 2 minutes
89
88
  const REPORT_DIR = join(homedir(), '.mipham', 'crsi-sandbox')
90
89
 
@@ -107,10 +106,15 @@ const PROTECTED_PATHS = [
107
106
  'apps/cli/src/vajra/constitution.ts',
108
107
  // eval harness
109
108
  'apps/cli/test/',
109
+ 'apps/cli/src/core/eval-harness.ts',
110
+ 'apps/cli/src/core/behavior-tasks.ts',
111
+ 'apps/cli/src/core/behavior-tasks.json',
110
112
  // 改进机制自身
111
113
  'apps/cli/src/agent/effectiveness-tracker.ts',
112
114
  'apps/cli/src/core/meta-rule-engine.ts',
113
115
  'apps/cli/src/core/crsi-sandbox.ts',
116
+ 'apps/cli/src/core/crsi-producer.ts',
117
+ 'apps/cli/src/core/proposal-guard.ts',
114
118
  ]
115
119
 
116
120
  /** 是否命中只读边界。前缀匹配,目录条目以 `/` 结尾。 */
@@ -469,7 +469,6 @@ export class DreamEngine {
469
469
  const lines = match[1]!.split('\n')
470
470
  const fm: Record<string, unknown> = {}
471
471
  let currentNested: Record<string, unknown> | null = null
472
- let nestedKey = ''
473
472
  for (const line of lines) {
474
473
  // Handle nested YAML (e.g. "metadata:\n type: feedback")
475
474
  const nested = line.match(/^ (\w[\w\s]*?):\s*(.+)$/)
@@ -485,7 +484,6 @@ export class DreamEngine {
485
484
  if (val === '') {
486
485
  // Empty value → start nested block
487
486
  currentNested = {}
488
- nestedKey = key
489
487
  fm[key] = currentNested
490
488
  } else {
491
489
  currentNested = null
@@ -22,6 +22,7 @@ import { RedTeam } from './red-team'
22
22
  import { isProtectedPath } from './crsi-sandbox'
23
23
  import { produceRuleProposal, MANAGED_RULES_FILE } from './crsi-producer'
24
24
  import type { CrsiSignal } from './crsi-producer'
25
+ import { loadBehaviorTasks, judgeBehaviorTask } from './behavior-tasks'
25
26
 
26
27
  // ── Types ──
27
28
 
@@ -223,6 +224,12 @@ export function runEval(): EvalReport {
223
224
  })
224
225
  }
225
226
 
227
+ // ── 行为任务集(ground truth:约束行为效果,确定性无 LLM) ──
228
+ const behaviorTasks = loadBehaviorTasks()
229
+ for (const task of behaviorTasks) {
230
+ results.push(judgeBehaviorTask(task, ruleEngine))
231
+ }
232
+
226
233
  const passed = results.filter((r) => r.passed).length
227
234
  return {
228
235
  total: results.length,
@@ -2,6 +2,7 @@ import { readFileSync, existsSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
  import { parse as parseYaml } from 'yaml'
4
4
  import type { InstructionFile } from '../shared/index.ts'
5
+ import { COAUTHOR_TRAILER } from '../shared/index.ts'
5
6
 
6
7
  interface FrontmatterResult {
7
8
  data: Record<string, unknown>
@@ -163,6 +164,16 @@ its live CRSI / SIS / constitution state. Report the numbers you read
163
164
  from it as live counts; if it shows a subsystem as 未初始化 (uninitialized),
164
165
  say so explicitly instead of claiming it exists.`)
165
166
 
167
+ // AI 署名披露:提交时附带 Co-Authored-By 署名(与 Undercover 式隐瞒相反)
168
+ parts.push(`## Commit Attribution
169
+
170
+ When you create a git commit, always append this trailer on its own line
171
+ at the end of the commit message, disclosing AI involvement:
172
+
173
+ ${COAUTHOR_TRAILER}
174
+
175
+ Never omit it or present the work as purely human-authored.`)
176
+
166
177
  return parts.join('\n\n---\n\n')
167
178
  }
168
179
 
@@ -1,7 +1,9 @@
1
+ import { join } from 'node:path'
2
+ import { homedir } from 'node:os'
1
3
  import { MemoryManager } from './memory-manager'
2
4
  import type { MemoryManager as MemoryManagerType } from './memory-manager'
3
5
 
4
- const MEMORY_DIR = `${process.env.HOME || '~'}/.mipham/memory`
6
+ const MEMORY_DIR = join(homedir(), '.mipham', 'memory')
5
7
 
6
8
  let instance: MemoryManagerType | null = null
7
9
 
@@ -1,14 +1,16 @@
1
1
  import type { PermissionRuleEntry } from '../shared/index.ts'
2
+ import { matchPath } from './credential-masker/matcher'
2
3
 
3
- /**
4
- * Match a Bash(command_pattern) rule against an actual tool call.
5
- *
6
- * Pattern formats:
7
- * "Bash" → matches any Bash call
8
- * "Bash(git:*)" → matches "git status", "git diff --cached", etc.
9
- * "Bash(npm test:*)" → matches "npm test -- --coverage"
10
- * "Write(/etc/*)" → matches Write to /etc/passwd, /etc/hosts, etc.
11
- */
4
+ // Match a tool(parameter) rule against an actual tool call.
5
+ //
6
+ // Pattern formats:
7
+ // "Bash" → matches any Bash call
8
+ // "Bash(git:*)" → matches "git status", "git diff --cached", etc.
9
+ // "Bash(npm test:*)" → matches "npm test -- --coverage"
10
+ // "Write(/etc/*)" → matches Write to /etc/passwd, /etc/hosts, etc.
11
+ // "Read(**/.ssh/*)" → matches Read of any path under .ssh
12
+ // "Grep(**/vendor)" → matches Grep rooted under a vendor directory
13
+ // "Glob(**/.ssh)" → matches Glob rooted under a .ssh directory
12
14
  export function matchBashRule(
13
15
  pattern: string,
14
16
  toolName: string,
@@ -31,10 +33,19 @@ export function matchBashRule(
31
33
  return wildcardMatch(subPattern!, cmd.trim())
32
34
  }
33
35
 
34
- // For Write/Edit: match against the file_path
35
- if (baseTool === 'Write' || baseTool === 'Edit') {
36
+ // For Write/Edit/Read: match against the file_path with path-glob semantics.
37
+ // Use matchPath (NOT wildcardMatch): wildcardMatch is tuned for Bash commands
38
+ // (`:` → colon-or-whitespace, `*` → `.*`), which is wrong for filesystem
39
+ // paths — `*` would cross `/` and Windows drive letters like `C:\` get mangled.
40
+ if (baseTool === 'Write' || baseTool === 'Edit' || baseTool === 'Read') {
36
41
  const path = String(toolInput.file_path || '')
37
- return wildcardMatch(subPattern!, path)
42
+ return matchPath(path, subPattern!)
43
+ }
44
+
45
+ // For Grep/Glob: match against the base search path (a directory)
46
+ if (baseTool === 'Grep' || baseTool === 'Glob') {
47
+ const path = String(toolInput.path || '')
48
+ return matchPath(path, subPattern!)
38
49
  }
39
50
 
40
51
  return false
@@ -0,0 +1,100 @@
1
+ // apps/cli/src/core/proposal-guard.ts
2
+ // CRSI 提案预筛器(块 2 最小版):结构确定性预筛,零 LLM。
3
+ //
4
+ // 这是 CRSI 自改进「producer 改散文」闭环的第一道闸(三层验证的第①层):
5
+ // ① 结构确定性(本文件):受保护路径 + 自引用封闭 + 目标范围 + 结构不变量
6
+ // ② 行为效果(M3/A:LLM 生成行为 → 测试判定)—— 留作 seam,未实现
7
+ // ③ 人类审批 —— CrsiSandbox 之后
8
+ //
9
+ // 诚实边界:本预筛只能证「结构合法」,不能证「散文更好」——
10
+ // 后者是第②层(行为效果)的职责,需要 LLM 生成行为,属另一 A1 边界决策。
11
+
12
+ import { parse as parseYaml } from 'yaml'
13
+ import { isProtectedPath } from './crsi-sandbox'
14
+ import { MANAGED_RULES_FILE, MANAGED_RULE_MARKER } from './crsi-producer'
15
+
16
+ /** producer 产出的一条「改散文」提议(最小字段集,guard 阶段只消费这些)。 */
17
+ export interface ProducerProposal {
18
+ id: string
19
+ /** 仓库根相对路径 */
20
+ filePath: string
21
+ /** 最小版只放行两类目标,收窄散文风险面 */
22
+ kind: 'skill' | 'managed-rule'
23
+ /** 改后的完整文件内容 */
24
+ newContent: string
25
+ }
26
+
27
+ export interface PrefilterVerdict {
28
+ pass: boolean
29
+ /** 拒绝理由;pass=true 时为空 */
30
+ reasons: string[]
31
+ }
32
+
33
+ const SKILL_DIR = 'apps/cli/skills/'
34
+
35
+ function isSkillPath(filePath: string): boolean {
36
+ return (
37
+ filePath.startsWith(SKILL_DIR) &&
38
+ (filePath.endsWith('.SKILL.md') || filePath.endsWith('.mipham-skill.md'))
39
+ )
40
+ }
41
+
42
+ /**
43
+ * 更严、独立于 skills loader 的最小 frontmatter 解析。
44
+ *
45
+ * 与 loader 的宽松回退不同:frontmatter 缺失 / YAML 非法 / name 或 description
46
+ * 为空,都判失败(返回 null)——proposal 必须显式携带合法头部,不得依赖
47
+ * loader 的「文件名兜底 name」「空 description」等宽进逻辑。
48
+ */
49
+ function parseStrictSkillFrontmatter(raw: string): { name: string; description: string } | null {
50
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/)
51
+ if (!match) return null
52
+
53
+ let data: unknown
54
+ try {
55
+ data = parseYaml(match[1] || '')
56
+ } catch {
57
+ return null
58
+ }
59
+
60
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) return null
61
+ const obj = data as Record<string, unknown>
62
+
63
+ const name = typeof obj.name === 'string' ? obj.name.trim() : ''
64
+ const description = typeof obj.description === 'string' ? obj.description.trim() : ''
65
+ if (!name || !description) return null
66
+ return { name, description }
67
+ }
68
+
69
+ export function prefilterProposal(p: ProducerProposal): PrefilterVerdict {
70
+ const reasons: string[] = []
71
+
72
+ // ① 受保护路径 + 自引用封闭(isProtectedPath 已含评估机制自身的 5 个补洞)
73
+ if (isProtectedPath(p.filePath)) {
74
+ reasons.push(`protected path: ${p.filePath} is read-only to the self-improvement loop`)
75
+ }
76
+
77
+ // ② 目标范围白名单
78
+ const inScope =
79
+ (p.kind === 'skill' && isSkillPath(p.filePath)) ||
80
+ (p.kind === 'managed-rule' && p.filePath === MANAGED_RULES_FILE)
81
+ if (!inScope) {
82
+ reasons.push(`out of scope: ${p.filePath} is not an allowed ${p.kind} target`)
83
+ }
84
+
85
+ // ③ 结构不变量
86
+ if (p.kind === 'skill') {
87
+ if (!parseStrictSkillFrontmatter(p.newContent)) {
88
+ reasons.push(
89
+ 'skill frontmatter invalid: missing/illegal frontmatter or empty name/description',
90
+ )
91
+ }
92
+ } else if (!p.newContent.includes(MANAGED_RULE_MARKER)) {
93
+ reasons.push('managed rule missing the producer append marker')
94
+ }
95
+
96
+ // ②(行为效果 seam):此处接入 M3/A 任务表现度量——用 LLM 生成行为 + 确定性
97
+ // 测试判定,过滤「行为效果退化」的提议。未实现,属 producer LLM 生成能力(块 1)。
98
+
99
+ return { pass: reasons.length === 0, reasons }
100
+ }
@@ -19,6 +19,7 @@
19
19
 
20
20
  import { readdirSync, readFileSync, existsSync } from 'node:fs'
21
21
  import { join } from 'node:path'
22
+ import { globToRegexSource } from './credential-masker/matcher'
22
23
 
23
24
  interface RuleFile {
24
25
  name: string
@@ -117,18 +118,15 @@ export class RulesLoader {
117
118
  }
118
119
 
119
120
  /**
120
- * Simple glob matching. Supports **, *, and exact file matching.
121
- * Returns true if file matches pattern.
121
+ * Glob matching for path-scoped rules. Reuses the shared path-glob core
122
+ * (globToRegexSource) so `*`/`**`/`?` semantics match credential-file
123
+ * matching. Anchoring differs: rules match by *suffix* (a rule `*.ts`
124
+ * applies to any file ending in `.ts`), not full path.
122
125
  */
123
126
  private matchPath(file: string, pattern: string): boolean {
124
- // Convert glob pattern to regex
125
- let regexStr = pattern
126
- .replace(/\./g, '\\.')
127
- .replace(/\*\*/g, '<<GLOBSTAR>>')
128
- .replace(/\*/g, '[^/]*')
129
- .replace(/<<GLOBSTAR>>/g, '.*')
130
-
131
- // If pattern doesn't start with ** or *, anchor to be a suffix match
127
+ let regexStr = globToRegexSource(pattern)
128
+
129
+ // If pattern doesn't start with ** or *, anchor to a suffix match
132
130
  if (!pattern.startsWith('**') && !pattern.startsWith('*')) {
133
131
  regexStr = regexStr + '$'
134
132
  }
@@ -0,0 +1,14 @@
1
+ {
2
+ "version": 1,
3
+ "tasks": [
4
+ {
5
+ "id": "task-answer-fn",
6
+ "instruction": "用 Write 工具在 <taskDir>/solution.ts 写入一个 TypeScript 文件,导出 `export function answer(): number { return 42 }`。",
7
+ "groundTruth": {
8
+ "kind": "file-contains",
9
+ "file": "solution.ts",
10
+ "contains": ["export function answer", "42"]
11
+ }
12
+ }
13
+ ]
14
+ }