@miphamai/cli 0.60.1 → 0.61.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.60.1",
3
+ "version": "0.61.0",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  import { randomUUID } from 'node:crypto'
16
- import { CrsiSandbox } from './crsi-sandbox'
16
+ import { CrsiSandbox, validateBlastRadius } from './crsi-sandbox'
17
17
  import type { CrsiModificationResult } from './crsi-sandbox'
18
18
  import { runEval, appendEvalScore, getLastEvalScore } from './eval-harness'
19
19
 
@@ -28,6 +28,13 @@ export interface CrsiProposal {
28
28
  originalContent?: string
29
29
  /** 触发此改动的 CRSI insight id */
30
30
  crsiInsightId?: string
31
+ /**
32
+ * 完整覆盖(blast radius):此改动触及的**所有**代码路径(文件列表)。
33
+ *
34
+ * 源于 2026-08-26 教训:修「思考转储」只接实时指示器、漏历史行冲刷路径——
35
+ * 局部正确、全局遗漏。自修改前必须摸清并声明全部受影响路径,否则 fail-closed 拒绝。
36
+ */
37
+ blastRadius?: string[]
31
38
  }
32
39
 
33
40
  // ── Pending proposal registry (两阶段闸门) ──
@@ -42,6 +49,25 @@ export function runCrsiModification(
42
49
  proposal: CrsiProposal,
43
50
  sandbox: CrsiSandbox = new CrsiSandbox(),
44
51
  ): CrsiModificationResult {
52
+ // 完整覆盖闸:自修改必须声明 blastRadius,否则 fail-closed(在 worktree 之前,零副作用)。
53
+ const blastRadiusError = validateBlastRadius(proposal)
54
+ if (blastRadiusError) {
55
+ return {
56
+ modification: {
57
+ id: 'crsi-mod-rejected-blast-radius',
58
+ description: proposal.description,
59
+ filePath: proposal.filePath,
60
+ newContent: proposal.newContent,
61
+ originalContent: proposal.originalContent ?? '',
62
+ crsiInsightId: proposal.crsiInsightId,
63
+ timestamp: new Date().toISOString(),
64
+ },
65
+ applied: false,
66
+ phase: 'failed',
67
+ error: blastRadiusError,
68
+ }
69
+ }
70
+
45
71
  sandbox.createWorktree()
46
72
 
47
73
  const applied = sandbox.applyModification({
@@ -127,7 +127,13 @@ export function produceCrsiProposal(
127
127
  metaRules: MetaRule[],
128
128
  currentLessons: string,
129
129
  timestamp: string,
130
- ): { description: string; filePath: string; newContent: string; originalContent: string } | null {
130
+ ): {
131
+ description: string
132
+ filePath: string
133
+ newContent: string
134
+ originalContent: string
135
+ blastRadius: string[]
136
+ } | null {
131
137
  const signal = selectCrsiSignal(insights, metaRules)
132
138
  if (!signal) return null
133
139
 
@@ -142,6 +148,7 @@ export function produceCrsiProposal(
142
148
  filePath: LESSONS_FILE,
143
149
  newContent,
144
150
  originalContent: currentLessons,
151
+ blastRadius: [LESSONS_FILE],
145
152
  }
146
153
  }
147
154
 
@@ -215,7 +222,13 @@ export function renderManagedRuleSource(signal: CrsiSignal): string | null {
215
222
  export function produceRuleProposal(
216
223
  signal: CrsiSignal,
217
224
  currentManagedRules: string,
218
- ): { description: string; filePath: string; newContent: string; originalContent: string } | null {
225
+ ): {
226
+ description: string
227
+ filePath: string
228
+ newContent: string
229
+ originalContent: string
230
+ blastRadius: string[]
231
+ } | null {
219
232
  const ruleSource = renderManagedRuleSource(signal)
220
233
  if (!ruleSource) return null
221
234
 
@@ -232,6 +245,7 @@ export function produceRuleProposal(
232
245
  filePath: MANAGED_RULES_FILE,
233
246
  newContent,
234
247
  originalContent: currentManagedRules,
248
+ blastRadius: [MANAGED_RULES_FILE],
235
249
  }
236
250
  }
237
251
 
@@ -122,6 +122,24 @@ export function isProtectedPath(filePath: string): boolean {
122
122
  return PROTECTED_PATHS.some((p) => filePath === p || filePath.startsWith(p))
123
123
  }
124
124
 
125
+ /**
126
+ * 完整覆盖闸(blast radius):自修改 proposal 必须声明非空 blastRadius,否则拒绝。
127
+ *
128
+ * 源于 2026-08-26 教训:修「思考转储」只接实时指示器、漏历史行冲刷路径——
129
+ * 局部正确、全局遗漏。自修改前必须摸清并声明全部受影响路径,fail-closed。
130
+ *
131
+ * 返回错误字符串(拒绝理由),合法时返回 null。
132
+ */
133
+ export function validateBlastRadius(proposal: { blastRadius?: string[] }): string | null {
134
+ if (!proposal.blastRadius || proposal.blastRadius.length === 0) {
135
+ return (
136
+ 'blast radius 未声明:自修改必须枚举触及的全部代码路径。' +
137
+ '教训:两条渲染路径只接一条 = 局部正确全局遗漏。'
138
+ )
139
+ }
140
+ return null
141
+ }
142
+
125
143
  // ── Sandbox ──
126
144
 
127
145
  export class CrsiSandbox {
@@ -19,7 +19,7 @@ import { ConstitutionLoader, DEFAULT_CONSTITUTION } from './constitution-loader'
19
19
  import { ErrorSignatureDB } from './error-signature-db'
20
20
  import { PreFlightChecker } from './preflight-checker'
21
21
  import { RedTeam } from './red-team'
22
- import { isProtectedPath } from './crsi-sandbox'
22
+ import { isProtectedPath, validateBlastRadius } from './crsi-sandbox'
23
23
  import { produceRuleProposal, MANAGED_RULES_FILE } from './crsi-producer'
24
24
  import type { CrsiSignal } from './crsi-producer'
25
25
  import { loadBehaviorTasks, judgeBehaviorTask } from './behavior-tasks'
@@ -167,6 +167,16 @@ export function runEval(): EvalReport {
167
167
  results.push({ id, description: `受保护路径被拒: ${path}`, passed: isProtectedPath(path) })
168
168
  }
169
169
 
170
+ // ── 完整覆盖闸(ground truth:未声明 blast radius 的 proposal 被 fail-closed 拒绝) ──
171
+ results.push({
172
+ id: 'blast-radius-gate',
173
+ description: '自修改 proposal 未声明 blast radius 被拒,声明后放行',
174
+ passed:
175
+ validateBlastRadius({ blastRadius: undefined }) !== null &&
176
+ validateBlastRadius({ blastRadius: [] }) !== null &&
177
+ validateBlastRadius({ blastRadius: ['apps/cli/src/foo.ts'] }) === null,
178
+ })
179
+
170
180
  // ── 安全(ground truth:16 攻击零漏过) ──
171
181
  const redTeam = new RedTeam().run(constitution, preflight, errorDB)
172
182
  results.push({
@@ -1,5 +1,5 @@
1
1
  import { readFileSync, existsSync } from 'node:fs'
2
- import { join } from 'node:path'
2
+ import { join, resolve, relative, sep, isAbsolute } from 'node:path'
3
3
  import { execSync } from 'node:child_process'
4
4
  import { parse as parseYaml } from 'yaml'
5
5
  import type { InstructionFile } from '../shared/index.ts'
@@ -59,32 +59,78 @@ export function parsePromptExclude(value: unknown): string[] {
59
59
  return []
60
60
  }
61
61
 
62
+ /** 定位仓库根(git rev-parse --show-toplevel),非 git 目录回退 cwd。 */
63
+ export function gitRoot(cwd: string): string {
64
+ try {
65
+ return execSync('git rev-parse --show-toplevel', {
66
+ cwd,
67
+ timeout: 5000,
68
+ encoding: 'utf-8',
69
+ }).trim()
70
+ } catch {
71
+ return cwd
72
+ }
73
+ }
74
+
75
+ /** 从仓库根到 cwd 的目录链(含两端),就近(cwd)在最后。cwd 不在 root 下时退化为 [cwd]。 */
76
+ export function discoverDirectories(root: string, cwd: string): string[] {
77
+ const absRoot = resolve(root)
78
+ const absCwd = resolve(cwd)
79
+ if (absCwd === absRoot) return [absRoot]
80
+
81
+ const rel = relative(absRoot, absCwd)
82
+ if (rel.startsWith('..') || isAbsolute(rel)) return [absCwd]
83
+
84
+ const dirs = [absRoot]
85
+ let cur = absRoot
86
+ for (const seg of rel.split(sep)) {
87
+ cur = join(cur, seg)
88
+ dirs.push(cur)
89
+ }
90
+ return dirs
91
+ }
92
+
93
+ /**
94
+ * 每目录内指令文件的读取顺序(后加载 = 更高优先级)。
95
+ * AGENTS.md(行业标准基线)→ AGENTS.override.md(Codex 覆盖层)→
96
+ * MIPHAM.md(Mipham 品牌)→ CLAUDE.md(Claude Code 兼容)。
97
+ * MIPHAM→CLAUDE 相对顺序保持现状不变,仅前置 AGENTS 两条,避免行为回归。
98
+ */
99
+ export const INSTRUCTION_FILENAMES = [
100
+ 'AGENTS.md',
101
+ 'AGENTS.override.md',
102
+ 'MIPHAM.md',
103
+ 'CLAUDE.md',
104
+ ] as const
105
+
62
106
  export class InstructionsLoader {
63
107
  private instructions: InstructionFile[] = []
64
108
  private crsiLessonSummaries: CrsiLessonSummary[] = []
65
109
 
66
110
  loadAll(cwd: string): void {
67
111
  this.instructions = []
112
+ const root = gitRoot(cwd)
113
+
114
+ // Tier 1: 集团/公司策略(锚定仓库根,从任意子目录启动都正确;不读 AGENTS.md)
115
+ this.tryLoad(join(root, '..', '..', 'CLAUDE.md'), 'group') // Rismed_Ronxin_Capital
116
+ this.tryLoad(join(root, '..', 'CLAUDE.md'), 'company') // One_Mipham_Corporation
117
+ this.tryLoad(join(root, '..', 'MIPHAM.md'), 'group')
118
+
119
+ // Tier 2: 递归项目层 — git 根 → cwd,逐目录读,就近(cwd)最后 = 优先级最高
120
+ const dirs = discoverDirectories(root, cwd)
121
+ dirs.forEach((dir, i) => {
122
+ const level: InstructionFile['level'] = i === dirs.length - 1 ? 'project' : 'directory'
123
+ for (const name of INSTRUCTION_FILENAMES) {
124
+ this.tryLoad(join(dir, name), level)
125
+ }
126
+ })
68
127
 
69
- // Tier 1: Ancestor-level CLAUDE.md (for Claude Code compatibility)
70
- // ../../ = Rismed_Ronxin_Capital
71
- this.tryLoad(join(cwd, '..', '..', 'CLAUDE.md'), 'group')
72
- // ../ = One_Mipham_Corporation
73
- this.tryLoad(join(cwd, '..', 'CLAUDE.md'), 'company')
74
-
75
- // Tier 1b: Group-level MIPHAM.md — One_Mipham_Corporation is the root
76
- this.tryLoad(join(cwd, '..', 'MIPHAM.md'), 'group')
77
-
78
- // Tier 2: Project-level (CLAUDE.md + MIPHAM.md at cwd)
79
- this.tryLoad(join(cwd, 'MIPHAM.md'), 'project')
80
- this.tryLoad(join(cwd, 'CLAUDE.md'), 'project')
81
-
82
- // Tier 3: User-level ~/.mipham/USER.md
128
+ // Tier 3: 用户层 ~/.mipham/USER.md
83
129
  const home = process.env.HOME || '~'
84
130
  this.tryLoad(join(home, '.mipham', 'USER.md'), 'user')
85
131
 
86
132
  // CRSI 教训召回:读 crsi-lessons.md 提取精华,注入系统提示(只写不读 → 写后召回)
87
- this.crsiLessonSummaries = this.loadCrsiLessons(cwd)
133
+ this.crsiLessonSummaries = this.loadCrsiLessons(root)
88
134
  }
89
135
 
90
136
  buildSystemPrompt(permissionMode?: string): string {
@@ -235,17 +281,7 @@ Never omit it or present the work as purely human-authored.`)
235
281
  }
236
282
 
237
283
  /** 读 crsi-lessons.md(按仓库根定位)提取教训精华。读不到则返回空。 */
238
- private loadCrsiLessons(cwd: string): CrsiLessonSummary[] {
239
- let root = cwd
240
- try {
241
- root = execSync('git rev-parse --show-toplevel', {
242
- cwd,
243
- timeout: 5000,
244
- encoding: 'utf-8',
245
- }).trim()
246
- } catch {
247
- // 非 git 目录 → 回退 cwd
248
- }
284
+ private loadCrsiLessons(root: string): CrsiLessonSummary[] {
249
285
  try {
250
286
  const content = readFileSync(join(root, LESSONS_FILE), 'utf-8')
251
287
  return extractCrsiLessonSummaries(content)
@@ -9,7 +9,7 @@
9
9
  export const PACKAGE_NAME = '@miphamai/cli' as const
10
10
 
11
11
  /** 当前发布版本 */
12
- export const PACKAGE_VERSION = '0.60.1' as const
12
+ export const PACKAGE_VERSION = '0.61.0' as const
13
13
 
14
14
  /** npm install 全局安装命令 */
15
15
  export const NPM_INSTALL_COMMAND = `npm install -g ${PACKAGE_NAME}` as const