@miphamai/cli 0.62.0 → 0.64.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.
@@ -9,6 +9,7 @@ import type { MiphamConfig } from '../shared/index.ts'
9
9
  import type { SkillsLoader } from '../skills/loader'
10
10
  import type { PluginManager } from '../plugin/plugin-manager'
11
11
  import type { Message } from '../shared/types.js'
12
+ import type { UpdateStatus } from '../shared/update'
12
13
  import { McpClient } from '../mcp/client'
13
14
  import { buildCapabilityReport } from '../core/capability-inventory'
14
15
  import { InstructionsLoader } from '../core/instructions'
@@ -18,6 +19,7 @@ import {
18
19
  produceCrsiProposal,
19
20
  produceRuleProposal,
20
21
  produceProseProposal,
22
+ produceCrossoverProposal,
21
23
  selectCrsiSignal,
22
24
  collectSkillFiles,
23
25
  proseProposalId,
@@ -29,6 +31,18 @@ import {
29
31
  } from '../core/crsi-producer'
30
32
  import { prefilterProposal } from '../core/proposal-guard'
31
33
  import { runEval, appendEvalScore } from '../core/eval-harness'
34
+ import { listRewardFns } from '../core/reward-fn'
35
+ import { runTaskPerformance, measureSkillDeltaRepeated } from '../core/task-performance'
36
+ import { randomUUID } from 'node:crypto'
37
+ import {
38
+ buildImprovementReport,
39
+ appendImprovement,
40
+ readImprovements,
41
+ improvementRate,
42
+ setPendingVerdict,
43
+ getPendingVerdict,
44
+ shouldBlockApproval,
45
+ } from '../core/improvement-track'
32
46
  import { NPM_UPDATE_COMMAND, PACKAGE_VERSION, COAUTHOR_TRAILER } from '../shared/index.ts'
33
47
  import { getPreference } from '../config/preferences'
34
48
  import { loadCrossSessionConfig } from '../config/loader'
@@ -81,6 +95,7 @@ export interface CommandContext {
81
95
  setFocusMode: (on: boolean) => void
82
96
  setGoal: (text: string) => void
83
97
  setUltracodeMode: (on: boolean) => void
98
+ setUpdateStatus: (s: UpdateStatus) => void
84
99
  skillsLoader?: SkillsLoader
85
100
  pluginManager?: PluginManager
86
101
  /** i18n translate function — populated from React tree via useI18n().
@@ -761,11 +776,16 @@ const crsiInventoryCmd: CommandHandler = async (ctx) => {
761
776
 
762
777
  const crsiModifyCmd: CommandHandler = async (ctx, args) => {
763
778
  if (args[0] === '--approve') {
779
+ if (shouldBlockApproval(getPendingVerdict() ?? 'inconclusive')) {
780
+ return { content: '❌ 任务表现倒退,禁止固化。请 /crsi modify --reject 丢弃,或改进后再试。' }
781
+ }
764
782
  const r = approvePending()
783
+ setPendingVerdict(null)
765
784
  return { content: r.success ? `✅ ${r.message}` : `⚠️ ${r.message}` }
766
785
  }
767
786
  if (args[0] === '--reject') {
768
787
  const r = rejectPending()
788
+ setPendingVerdict(null)
769
789
  return { content: r.success ? `✅ ${r.message}` : `⚠️ ${r.message}` }
770
790
  }
771
791
  if (args.length < 3) {
@@ -794,17 +814,50 @@ const crsiModifyCmd: CommandHandler = async (ctx, args) => {
794
814
  // 文件不存在 → 宽松模式(originalContent 为空)
795
815
  }
796
816
 
797
- const result = runCrsiModification({ description, filePath, newContent, originalContent })
817
+ const result = await runCrsiModification({
818
+ description,
819
+ filePath,
820
+ newContent,
821
+ originalContent,
822
+ blastRadius: [filePath],
823
+ })
798
824
  if (!result.applied || result.phase === 'failed') {
799
825
  return {
800
826
  content: `❌ 修改未通过(phase: ${result.phase})。\n${result.error ?? ''}`,
801
827
  }
802
828
  }
803
829
 
830
+ // 测量在 runCrsiModification 成功后进行(避免 failed proposal 白跑 6 次 LLM 调用)。
831
+ const llm = ctx.engine.getLlm() ?? ctx.engine.getRegistry()
832
+ let improvementLine = ''
833
+ try {
834
+ const sample = await measureSkillDeltaRepeated(llm, { filePath, originalContent, newContent })
835
+ if (sample) {
836
+ const report = buildImprovementReport(sample, [filePath])
837
+ setPendingVerdict(report.verdict)
838
+ appendImprovement({ ...report, id: randomUUID(), timestamp: new Date().toISOString() })
839
+ const rate = improvementRate(readImprovements())
840
+ const label =
841
+ report.verdict === 'improved'
842
+ ? 'improved ✅'
843
+ : report.verdict === 'regressed'
844
+ ? 'regressed ⚠️'
845
+ : 'inconclusive'
846
+ const sign = report.deltaMean >= 0 ? '+' : ''
847
+ improvementLine =
848
+ `\n📊 改进判定: ${label} (delta ${sign}${report.deltaMean.toFixed(1)}, 噪声 ${report.noise.toFixed(1)}, 阈值 ${report.minEffect.toFixed(1)})` +
849
+ `\n 改进率: ${rate.improved}/${rate.total} (${(rate.rate * 100).toFixed(0)}%, Wilson 95% [${(rate.lo * 100).toFixed(0)}%, ${(rate.hi * 100).toFixed(0)}%])` +
850
+ (report.verdict === 'regressed' ? '\n ⚠️ 任务表现倒退:--approve 将被拒绝。' : '')
851
+ }
852
+ } catch {
853
+ // 测量失败(LLM 不可用等)不阻断 modify 流程——改进信号是可选的。
854
+ }
855
+
804
856
  return {
805
857
  content:
806
- `✅ 测试通过。审阅下方 diff:\n\n${result.diff}\n\n` +
807
- '/crsi modify --approve 合并\n/crsi modify --reject 丢弃',
858
+ `✅ 测试通过。审阅下方 diff:\n\n${result.diff}\n` +
859
+ improvementLine +
860
+ '\n/crsi modify --approve 合并\n/crsi modify --reject 丢弃',
808
861
  }
809
862
  }
810
863
 
@@ -869,11 +922,12 @@ const crsiProposeCmd: CommandHandler = async (ctx, args) => {
869
922
  return { content: `❌ 提议未通过预筛:${verdict.reasons.join('; ')}` }
870
923
  }
871
924
 
872
- const result = runCrsiModification({
925
+ const result = await runCrsiModification({
873
926
  description: proposal.description,
874
927
  filePath: proposal.filePath,
875
928
  newContent: proposal.newContent,
876
929
  originalContent: proposal.originalContent,
930
+ blastRadius: [proposal.filePath],
877
931
  })
878
932
  if (!result.applied || result.phase === 'failed') {
879
933
  return { content: `❌ 生成失败(phase: ${result.phase})。\n${result.error ?? ''}` }
@@ -911,7 +965,7 @@ const crsiProposeCmd: CommandHandler = async (ctx, args) => {
911
965
  }
912
966
  }
913
967
 
914
- const result = runCrsiModification(proposal)
968
+ const result = await runCrsiModification(proposal)
915
969
  if (!result.applied || result.phase === 'failed') {
916
970
  return { content: `❌ 固化失败(phase: ${result.phase})。\n${result.error ?? ''}` }
917
971
  }
@@ -923,6 +977,38 @@ const crsiProposeCmd: CommandHandler = async (ctx, args) => {
923
977
  }
924
978
  }
925
979
 
980
+ // ── Crossover 路径:/crsi propose --crossover 合并两条重叠教训 ──
981
+ if (args[0] === '--crossover') {
982
+ const llm = ctx.engine.getLlm() ?? ctx.engine.getRegistry()
983
+ let current = ''
984
+ try {
985
+ const { readFileSync } = await import('node:fs')
986
+ const { join } = await import('node:path')
987
+ current = readFileSync(join(root, LESSONS_FILE), 'utf-8')
988
+ } catch {
989
+ current = ''
990
+ }
991
+ if (!current) {
992
+ return { content: '教训文件为空,无可合并。' }
993
+ }
994
+
995
+ const proposal = await produceCrossoverProposal(llm, current, new Date().toISOString())
996
+ if (!proposal) {
997
+ return { content: '没有找到可合并的重叠教训对。' }
998
+ }
999
+
1000
+ const result = await runCrsiModification(proposal)
1001
+ if (!result.applied || result.phase === 'failed') {
1002
+ return { content: `❌ 合并失败(phase: ${result.phase})。\n${result.error ?? ''}` }
1003
+ }
1004
+
1005
+ return {
1006
+ content:
1007
+ `✅ 已生成合并教训并跑过测试。审阅 diff:\n\n${result.diff}\n\n` +
1008
+ '/crsi modify --approve 合并 | /crsi modify --reject 丢弃',
1009
+ }
1010
+ }
1011
+
926
1012
  // ── 教训路径(默认):/crsi propose 追加教训 ──
927
1013
  let current = ''
928
1014
  try {
@@ -939,7 +1025,7 @@ const crsiProposeCmd: CommandHandler = async (ctx, args) => {
939
1025
  return { content: '没有足够的失败信号(autoApplicable insight 或高置信元规则)来生成教训。' }
940
1026
  }
941
1027
 
942
- const result = runCrsiModification(proposal)
1028
+ const result = await runCrsiModification(proposal)
943
1029
  if (!result.applied || result.phase === 'failed') {
944
1030
  return { content: `❌ 生成失败(phase: ${result.phase})。\n${result.error ?? ''}` }
945
1031
  }
@@ -951,9 +1037,28 @@ const crsiProposeCmd: CommandHandler = async (ctx, args) => {
951
1037
  }
952
1038
  }
953
1039
 
954
- const crsiEvalCmd: CommandHandler = async () => {
1040
+ const crsiEvalCmd: CommandHandler = async (ctx, args) => {
1041
+ const rewardIdx = args.indexOf('--reward')
1042
+ const rewardName = rewardIdx >= 0 ? args[rewardIdx + 1] : undefined
1043
+
1044
+ if (rewardName) {
1045
+ const llm = ctx.engine.getLlm() ?? ctx.engine.getRegistry()
1046
+ const fns = listRewardFns(llm)
1047
+ const fn = fns.find((f) => f.name === rewardName)
1048
+ if (!fn) {
1049
+ return {
1050
+ content: `❌ 未知 reward: ${rewardName}。可用: ${fns.map((f) => f.name).join(', ')}`,
1051
+ }
1052
+ }
1053
+ const report = await fn.evaluate()
1054
+ appendEvalScore(fn.name, report)
1055
+ return {
1056
+ content: `得分 **${report.score}/100** (${report.passed}/${report.total})\n失败: ${report.failures.join(', ') || '无'}`,
1057
+ }
1058
+ }
1059
+
955
1060
  const report = runEval()
956
- appendEvalScore(report)
1061
+ appendEvalScore('mechanism-sentinel', report)
957
1062
 
958
1063
  const lines: string[] = ['## 🧪 CRSI Eval Harness', '']
959
1064
  lines.push(`得分: **${report.score}/100** (${report.passed}/${report.total})`, '')
@@ -967,6 +1072,51 @@ const crsiEvalCmd: CommandHandler = async () => {
967
1072
  if (report.failures.length > 0) {
968
1073
  lines.push('', `❌ 失败任务: ${report.failures.join(', ')}`)
969
1074
  }
1075
+
1076
+ // 奖励函数注册表(reward function = policy→feedback 抽象可见)
1077
+ // 传 llm 列出完整注册表(task-performance 需 llm 才能跑,但构造它零 LLM 调用)。
1078
+ const llm = ctx.engine.getLlm() ?? ctx.engine.getRegistry()
1079
+ const fns = listRewardFns(llm)
1080
+ lines.push('', '## 🎁 奖励函数注册表', '')
1081
+ for (const f of fns) {
1082
+ lines.push(`- **${f.name}** — ${f.description}`)
1083
+ }
1084
+ lines.push('', '`/crsi eval --reward <name>` 跑指定奖励函数')
1085
+
1086
+ return { content: lines.join('\n') }
1087
+ }
1088
+
1089
+ const crsiBenchCmd: CommandHandler = async (ctx, args) => {
1090
+ const llm = ctx.engine.getLlm() ?? ctx.engine.getRegistry()
1091
+
1092
+ const skillIdx = args.indexOf('--skill')
1093
+ const skillName = skillIdx >= 0 ? args[skillIdx + 1] : undefined
1094
+ let skill: { name: string; text: string } | undefined
1095
+ if (skillName) {
1096
+ const body = ctx.skillsLoader?.get(skillName)?.body
1097
+ if (!body) {
1098
+ return { content: `❌ 未找到 skill: ${skillName}` }
1099
+ }
1100
+ skill = { name: skillName, text: body }
1101
+ }
1102
+
1103
+ const report = await runTaskPerformance(llm, skill ? { skill } : undefined)
1104
+
1105
+ const lines: string[] = [
1106
+ '## 🎯 CRSI 任务表现基准' + (skill ? `(skill: ${skill.name})` : ''),
1107
+ '',
1108
+ ]
1109
+ lines.push(`得分: **${report.score}/100** (${report.passed}/${report.total})`, '')
1110
+ lines.push('| 任务 | 结果 |')
1111
+ lines.push('|------|------|')
1112
+ for (const r of report.results) {
1113
+ lines.push(
1114
+ `| ${r.description.slice(0, 60)} | ${r.passed ? '✅' : '❌'}${r.detail ? ` — ${r.detail.slice(0, 80)}` : ''} |`,
1115
+ )
1116
+ }
1117
+ if (report.failures.length > 0) {
1118
+ lines.push('', `❌ 失败任务: ${report.failures.join(', ')}`)
1119
+ }
970
1120
  return { content: lines.join('\n') }
971
1121
  }
972
1122
 
@@ -3709,6 +3859,7 @@ const upgradeCmd: CommandHandler = async (ctx) => {
3709
3859
  if (ok) {
3710
3860
  const configPath = getConfigPath()
3711
3861
  const { existsSync } = await import('node:fs')
3862
+ ctx.setUpdateStatus({ state: 'installed', latest: update.latest })
3712
3863
  lines.push('')
3713
3864
  lines.push(t('commands.upgrade.updated', { version: update.latest }))
3714
3865
 
@@ -4695,6 +4846,7 @@ const commandsListCmd: CommandHandler = () => {
4695
4846
  '/crsi propose': 'Tools & Skills',
4696
4847
  '/crsi prose-clear': 'Tools & Skills',
4697
4848
  '/crsi eval': 'Tools & Skills',
4849
+ '/crsi bench': 'Tools & Skills',
4698
4850
  '/crsi meta': 'Tools & Skills',
4699
4851
  '/crsi interpret': 'Tools & Skills',
4700
4852
  '/crsi critique': 'Tools & Skills',
@@ -4853,6 +5005,7 @@ registry.set('/crsi modify', crsiModifyCmd)
4853
5005
  registry.set('/crsi propose', crsiProposeCmd)
4854
5006
  registry.set('/crsi prose-clear', crsiProseClearCmd)
4855
5007
  registry.set('/crsi eval', crsiEvalCmd)
5008
+ registry.set('/crsi bench', crsiBenchCmd)
4856
5009
  registry.set('/crsi meta', crsiMetaCmd)
4857
5010
  registry.set('/crsi interpret', crsiInterpretCmd)
4858
5011
  registry.set('/crsi critique', crsiCritiqueCmd)
@@ -5037,6 +5190,7 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
5037
5190
  '固化 CRSI 失败信号(默认教训 / --rule 受管理规则 / --prose 改 skill 散文),沙箱 + 人批准门控',
5038
5191
  '/crsi prose-clear': '清空散文提议去重 ledger(~/.mipham/crsi/prose-proposals.jsonl)',
5039
5192
  '/crsi eval': 'Run the ground-truth CRSI eval harness and record the score',
5193
+ '/crsi bench': 'Run the LLM task-performance benchmark and report the score',
5040
5194
  '/crsi meta': 'RSI Level 3 meta-rule analysis — rules that improve the rules',
5041
5195
  '/crsi interpret': 'Tool-call behavior dashboard — error patterns, usage, health',
5042
5196
  '/crsi critique': 'Enable/disable RLAIF self-critique on tool calls',
package/src/ui/input.tsx CHANGED
@@ -5,6 +5,8 @@ import { getCommandList } from './commands.js'
5
5
  import { CommandPicker } from './command-picker.js'
6
6
  import { useI18n } from '../i18n-context'
7
7
  import { discoverSessions } from '../agent/cross-session/discovery'
8
+ import { requestSuggestion, shouldAutocomplete, type RecentMessage } from '../core/autocomplete'
9
+ import type { Llm } from '../providers/llm'
8
10
 
9
11
  interface InputBarProps {
10
12
  onSubmit: (input: string) => void
@@ -23,6 +25,14 @@ interface InputBarProps {
23
25
  onCancel?: () => void
24
26
  /** When false, don't auto-open the slash-command picker when typing `/`. */
25
27
  showCommandPicker?: boolean
28
+ /** LLM 续写建议所需的模型(app.tsx 传;RemoteEngine 下 undefined → 补全禁用)。 */
29
+ llm?: Llm
30
+ /** 最近对话上下文(供续写贴合)。 */
31
+ recentMessages?: RecentMessage[]
32
+ /** 默认 true;app.tsx 传 config.autocomplete?.enabled ?? true。 */
33
+ autocompleteEnabled?: boolean
34
+ /** 默认 400ms;app.tsx 传 config.autocomplete?.debounceMs ?? 400。 */
35
+ autocompleteDebounceMs?: number
26
36
  }
27
37
 
28
38
  // ── Loading verb keys (i18n) ──
@@ -92,6 +102,10 @@ export function InputBar({
92
102
  onCyclePermission,
93
103
  onCancel,
94
104
  showCommandPicker = true,
105
+ llm,
106
+ recentMessages,
107
+ autocompleteEnabled = true,
108
+ autocompleteDebounceMs = 400,
95
109
  }: InputBarProps) {
96
110
  const { t } = useI18n()
97
111
  const [value, setValue] = useState('')
@@ -109,6 +123,22 @@ export function InputBar({
109
123
  const historyIndexRef = useRef(-1) // -1 = not browsing history
110
124
  const savedDraftRef = useRef('') // saved user draft before browsing history
111
125
 
126
+ // ── Ghost-text 自动补全 ──
127
+ const [suggestion, setSuggestion] = useState<string | null>(null)
128
+ const suggestionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
129
+ const suggestionReqIdRef = useRef(0)
130
+
131
+ // 统一清理:清 suggestion + 使在途/待发请求失效 + 取消防抖定时器。
132
+ // Escape / handleSubmit / 翻历史三处共用,避免各自手写漏掉某一环(rule of three)。
133
+ const clearSuggestion = () => {
134
+ setSuggestion(null)
135
+ suggestionReqIdRef.current++
136
+ if (suggestionTimerRef.current) {
137
+ clearTimeout(suggestionTimerRef.current)
138
+ suggestionTimerRef.current = null
139
+ }
140
+ }
141
+
112
142
  // Stabilize t ref — prevents stale closures in intervals and avoids
113
143
  // unnecessary effect re-runs when the i18n context value object changes.
114
144
  const tRef = useRef(t)
@@ -180,6 +210,7 @@ export function InputBar({
180
210
  // Idle → clear the draft (the intuitive "cancel")
181
211
  setValue('')
182
212
  valueRef.current = ''
213
+ clearSuggestion()
183
214
  return
184
215
  }
185
216
 
@@ -189,6 +220,14 @@ export function InputBar({
189
220
  onCyclePermission?.()
190
221
  return
191
222
  }
223
+ // Tab → 接受 ghost-text 建议(复用 Ctrl-key 的 revert 手法)
224
+ if (key.tab && !key.shift && suggestion) {
225
+ const next = valueBeforeShortcut.current + suggestion
226
+ setValue(next)
227
+ valueRef.current = next
228
+ setSuggestion(null)
229
+ return
230
+ }
192
231
  // Ctrl+P → toggle model picker
193
232
  // NOTE: Ink passes input=keypress.name (just 'p') when ctrl is true, not raw \x10.
194
233
  // ink-text-input inserts 'p' as literal text — revert it.
@@ -218,6 +257,9 @@ export function InputBar({
218
257
 
219
258
  // ── Arrow-key history navigation (Claude Code parity) ──
220
259
  if (key.upArrow || key.downArrow) {
260
+ // History navigation changes value via setValue (no onChange) — clear any
261
+ // ghost suggestion so a stale one isn't Tab-accepted onto a recalled entry.
262
+ clearSuggestion()
221
263
  // Ignore if picker is active (command picker handles its own arrows)
222
264
  if (value.startsWith('/')) return
223
265
 
@@ -299,6 +341,7 @@ export function InputBar({
299
341
  setValue('')
300
342
  valueRef.current = ''
301
343
  setPickerActive(false)
344
+ clearSuggestion()
302
345
  }
303
346
 
304
347
  // ── Picker mode: CommandPicker overlay ──
@@ -339,6 +382,33 @@ export function InputBar({
339
382
  // Normalize newlines → spaces. ink-text-input is single-line; multi-line
340
383
  // paste would trap arrow-key navigation on the first line.
341
384
  const normalized = val.replace(/\n/g, ' ')
385
+ // ── Ghost-text 自动补全:每次输入清 suggestion + 重排防抖 ──
386
+ setSuggestion(null)
387
+ const suggestionReqId = ++suggestionReqIdRef.current
388
+ if (suggestionTimerRef.current) {
389
+ clearTimeout(suggestionTimerRef.current)
390
+ suggestionTimerRef.current = null
391
+ }
392
+ if (
393
+ llm &&
394
+ autocompleteEnabled &&
395
+ shouldAutocomplete(normalized, isLoading, pickerActive)
396
+ ) {
397
+ suggestionTimerRef.current = setTimeout(() => {
398
+ requestSuggestion(
399
+ llm,
400
+ recentMessages ?? [],
401
+ normalized,
402
+ () => suggestionReqId !== suggestionReqIdRef.current,
403
+ )
404
+ .then((completion) => {
405
+ if (completion) setSuggestion(completion)
406
+ })
407
+ .catch(() => {
408
+ // 补全失败非关键——静默忽略
409
+ })
410
+ }, autocompleteDebounceMs)
411
+ }
342
412
  // 批量输入(paste/IME 替换)节流防渲染风暴;普通单字符输入立即显示,
343
413
  // 避免 33ms trailing 的「慢半拍」尾巴。
344
414
  const bulk = isBulkInput(valueRef.current, normalized)
@@ -363,6 +433,7 @@ export function InputBar({
363
433
  isLoading ? `${verb}...` : completionVerb ? completionVerb : t('ui.input.placeholder')
364
434
  }
365
435
  />
436
+ {suggestion && <Text dimColor>{suggestion}</Text>}
366
437
  </Box>
367
438
  {/* Slash command hints — shown when typing / (only when picker is NOT active) */}
368
439
  {slashHints.length > 0 && !pickerActive && (