@miphamai/cli 0.82.0 → 0.84.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 (84) hide show
  1. package/package.json +1 -1
  2. package/skills/standard/mipham-code-setup.SKILL.md +30 -8
  3. package/src/agent/agent-context.ts +5 -5
  4. package/src/agent/agent-experience.ts +2 -2
  5. package/src/agent/agent-registry.ts +4 -4
  6. package/src/agent/cross-session/discovery.ts +3 -2
  7. package/src/agent/cross-session/file-inbox.ts +2 -2
  8. package/src/agent/effectiveness-tracker.ts +2 -2
  9. package/src/agent/pattern-analyzer.ts +3 -4
  10. package/src/agent/sub-agent.ts +12 -2
  11. package/src/agent/types.ts +4 -1
  12. package/src/commands/autoloop-journal.ts +2 -2
  13. package/src/commands/environment.ts +2 -1
  14. package/src/commands/loop-scaffold.ts +2 -1
  15. package/src/commands/project.ts +86 -33
  16. package/src/config/keys-manager.ts +2 -2
  17. package/src/config/loader.ts +11 -10
  18. package/src/config/preferences.ts +3 -4
  19. package/src/core/auto-memory.ts +2 -3
  20. package/src/core/constitution-loader.ts +3 -4
  21. package/src/core/crsi-producer.ts +3 -3
  22. package/src/core/crsi-sandbox.ts +3 -2
  23. package/src/core/dream-engine.ts +2 -2
  24. package/src/core/engine.ts +38 -6
  25. package/src/core/error-signature-db.ts +2 -2
  26. package/src/core/eval-harness.ts +121 -4
  27. package/src/core/improvement-track.ts +35 -6
  28. package/src/core/instructions.ts +28 -4
  29. package/src/core/memory/memory-loader.ts +2 -3
  30. package/src/core/paths.ts +21 -1
  31. package/src/core/permission-audit.ts +120 -0
  32. package/src/core/permission-classifier.ts +449 -0
  33. package/src/core/permission-config.ts +106 -15
  34. package/src/core/permission.ts +369 -16
  35. package/src/core/rule-engine.ts +2 -2
  36. package/src/core/rules-loader.ts +3 -2
  37. package/src/core/session-log.ts +2 -3
  38. package/src/core/session-store.ts +2 -3
  39. package/src/core/task-performance.ts +22 -1
  40. package/src/core/workspace-trust.ts +4 -3
  41. package/src/daemon/database.ts +2 -2
  42. package/src/daemon/index.ts +2 -3
  43. package/src/daemon/launch.ts +3 -3
  44. package/src/daemon/server.ts +15 -0
  45. package/src/i18n-core/locales/en-US.json +3 -0
  46. package/src/i18n-core/locales/zh-CN.json +3 -0
  47. package/src/index.tsx +30 -6
  48. package/src/mcp/token-store.ts +2 -2
  49. package/src/plugin/plugin-manager.ts +2 -2
  50. package/src/shared/constants.ts +0 -1
  51. package/src/shared/package-info.ts +1 -1
  52. package/src/shared/types.ts +46 -6
  53. package/src/shared/update.ts +2 -3
  54. package/src/skills/bundled-skills.ts +1 -1
  55. package/src/skills/loader.ts +2 -3
  56. package/src/skills/marketplace.ts +2 -3
  57. package/src/skills/registry.ts +2 -2
  58. package/src/skills/skill-assets.ts +2 -2
  59. package/src/skills/usage.ts +2 -2
  60. package/src/telemetry/consent.ts +2 -3
  61. package/src/tools/agent/enter-plan.ts +3 -2
  62. package/src/tools/agent/exit-plan.ts +1 -1
  63. package/src/tools/agent/list-agents.ts +1 -1
  64. package/src/tools/agent/memory.ts +3 -3
  65. package/src/tools/agent/plan.ts +3 -2
  66. package/src/tools/agent/report-findings.ts +1 -1
  67. package/src/tools/agent/send-message.ts +1 -1
  68. package/src/tools/agent/skill.ts +1 -1
  69. package/src/tools/exec/git.ts +2 -2
  70. package/src/tools/exec/task.ts +1 -1
  71. package/src/tools/file/glob.ts +1 -1
  72. package/src/tools/file/grep.ts +1 -1
  73. package/src/tools/file/read.ts +1 -1
  74. package/src/tools/network/web-fetch.ts +1 -1
  75. package/src/tools/network/web-search.ts +1 -1
  76. package/src/tools/scheduling/cron.ts +5 -5
  77. package/src/tools/scheduling/schedule-wakeup.ts +1 -1
  78. package/src/tools/system/config.ts +2 -2
  79. package/src/tools/system/tool-search.ts +1 -1
  80. package/src/ui/app.tsx +51 -9
  81. package/src/ui/commands.ts +41 -19
  82. package/src/ui/config-wizard.tsx +2 -2
  83. package/src/ui/input.tsx +15 -3
  84. package/src/workflow/journal.ts +2 -2
@@ -1,8 +1,8 @@
1
1
  import type { ExperienceRule } from '../agent/experience-rules.js'
2
2
  import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs'
3
3
  import { join, dirname } from 'node:path'
4
- import { homedir } from 'node:os'
5
4
  import { MANAGED_RULES } from './crsi-managed-rules'
5
+ import { miphamHome } from './paths.ts'
6
6
 
7
7
  export interface ToolRule {
8
8
  id: string
@@ -62,7 +62,7 @@ export class ExperienceRuleEngine {
62
62
  private rules: ToolRule[]
63
63
  private storePath: string
64
64
 
65
- constructor(storeDir: string = join(homedir(), '.mipham', 'rule-engine')) {
65
+ constructor(storeDir: string = miphamHome('rule-engine')) {
66
66
  this.rules = [...BUILTIN_RULES, ...MANAGED_RULES].map((r) => ({ ...r }))
67
67
  this.storePath = join(storeDir, 'rules.json')
68
68
  this.load()
@@ -21,6 +21,7 @@ import { readdirSync, readFileSync, existsSync } from 'node:fs'
21
21
  import { join } from 'node:path'
22
22
  import { globToRegexSource } from './credential-masker/matcher'
23
23
  import { findWorktreeMarker } from './paths.ts'
24
+ import { MIPHAM_DIR } from '../shared/constants.ts'
24
25
 
25
26
  interface RuleFile {
26
27
  name: string
@@ -49,9 +50,9 @@ export class RulesLoader {
49
50
  private projectRulesDir: string | null
50
51
 
51
52
  constructor(cwd: string) {
52
- this.rulesDir = join(cwd, '.mipham', 'rules')
53
+ this.rulesDir = join(cwd, MIPHAM_DIR, 'rules')
53
54
  const marker = findWorktreeMarker(cwd)
54
- const projectRulesDir = marker ? join(marker.root, '.mipham', 'rules') : null
55
+ const projectRulesDir = marker ? join(marker.root, MIPHAM_DIR, 'rules') : null
55
56
  this.projectRulesDir = projectRulesDir === this.rulesDir ? null : projectRulesDir
56
57
  }
57
58
 
@@ -1,9 +1,9 @@
1
1
  import { appendFileSync, readFileSync, mkdirSync, existsSync } from 'node:fs'
2
- import { homedir } from 'node:os'
3
2
  import { join } from 'node:path'
4
3
  import { createHash } from 'node:crypto'
5
4
  import type { Message, ToolUseContent, ToolResultContent, ToolResult } from '../shared/types'
6
5
  import type { CheckerDecision } from './post-flight-checker'
6
+ import { miphamHome } from './paths.ts'
7
7
 
8
8
  export type SessionEvent =
9
9
  | {
@@ -115,8 +115,7 @@ export function deriveMessages(events: SessionEvent[]): Message[] {
115
115
  return out
116
116
  }
117
117
 
118
- const HOME = homedir()
119
- const LOG_DIR = join(HOME, '.mipham', 'sessions')
118
+ const LOG_DIR = miphamHome('sessions')
120
119
 
121
120
  /** 将会话名消毒为安全文件名(与 SessionStore 共用;防路径穿越)。 */
122
121
  export function sanitizeSessionName(name: string): string {
@@ -8,11 +8,11 @@ import {
8
8
  existsSync,
9
9
  statSync,
10
10
  } from 'node:fs'
11
- import { homedir } from 'node:os'
12
11
  import { join } from 'node:path'
13
12
  import { createHash } from 'node:crypto'
14
13
  import type { Message } from '../shared/types'
15
14
  import { sanitizeSessionName, SessionLog, deriveMessages, messageToEvents } from './session-log'
15
+ import { miphamHome } from './paths.ts'
16
16
 
17
17
  export interface SessionMetadata {
18
18
  name: string
@@ -29,8 +29,7 @@ export interface StoredSession {
29
29
  messages: Message[]
30
30
  }
31
31
 
32
- const HOME = homedir()
33
- const SESSIONS_DIR = join(HOME, '.mipham', 'sessions')
32
+ const SESSIONS_DIR = miphamHome('sessions')
34
33
  const INDEX_FILE = join(SESSIONS_DIR, '.index.json')
35
34
  const SUMMARIES_DIR = join(SESSIONS_DIR, '.summaries')
36
35
 
@@ -68,6 +68,8 @@ export interface TaskPerformanceReport {
68
68
  score: number
69
69
  results: TaskPerformanceResult[]
70
70
  failures: string[]
71
+ /** B2 代价维:整轮(LLM 生成 + 冻结测试判定)的墙钟耗时。只记录,不参与任何判定。 */
72
+ durationMs: number
71
73
  }
72
74
 
73
75
  /** 剥掉 LLM 可能包裹的 markdown 代码块(```ts ... ```),拿到裸代码。 */
@@ -101,6 +103,7 @@ export async function runTaskPerformance(
101
103
  const wanted = opts?.skill?.name
102
104
  const tasks = loadPerformanceTasks().filter((t) => (t.skill ?? undefined) === wanted)
103
105
  const results: TaskPerformanceResult[] = []
106
+ const startedAt = Date.now()
104
107
  for (const task of tasks) {
105
108
  const code = await collectGeneratedCode(llm, task.prompt, opts?.skill?.text)
106
109
  if (!code) {
@@ -127,6 +130,7 @@ export async function runTaskPerformance(
127
130
  score: results.length > 0 ? Math.round((passed / results.length) * 100) : 100,
128
131
  results,
129
132
  failures: results.filter((r) => !r.passed).map((r) => r.id),
133
+ durationMs: Date.now() - startedAt,
130
134
  }
131
135
  }
132
136
 
@@ -210,6 +214,13 @@ export interface SkillDeltaSample {
210
214
  skillName: string
211
215
  baselineScores: number[]
212
216
  postScores: number[]
217
+ /**
218
+ * B2 代价维:与分数数组**逐项对齐**的耗时(第 i 项就是产出第 i 个分数的那次采样)。
219
+ * 缺席 = 该样本未记代价(旧记录 / 旧调用点),读侧须按「缺席」处理、不得当成 0。
220
+ * 只记录,不参与改进判定 —— 样本量 k=3 时再加一个维度只会让统计问题更糟。
221
+ */
222
+ baselineDurations?: number[]
223
+ postDurations?: number[]
213
224
  }
214
225
 
215
226
  /**
@@ -227,18 +238,28 @@ export async function measureSkillDeltaRepeated(
227
238
  const k = opts?.k ?? 3
228
239
  const baselineScores: number[] = []
229
240
  const postScores: number[] = []
241
+ const baselineDurations: number[] = []
242
+ const postDurations: number[] = []
230
243
  for (let i = 0; i < k; i++) {
231
244
  const r = await runTaskPerformance(llm, {
232
245
  skill: { name: resolved.skillName, text: resolved.baselineText },
233
246
  })
234
247
  baselineScores.push(r.score)
248
+ baselineDurations.push(r.durationMs)
235
249
  }
236
250
  for (let i = 0; i < k; i++) {
237
251
  const r = await runTaskPerformance(llm, {
238
252
  skill: { name: resolved.skillName, text: resolved.postText },
239
253
  })
240
254
  postScores.push(r.score)
255
+ postDurations.push(r.durationMs)
241
256
  }
242
257
 
243
- return { skillName: resolved.skillName, baselineScores, postScores }
258
+ return {
259
+ skillName: resolved.skillName,
260
+ baselineScores,
261
+ postScores,
262
+ baselineDurations,
263
+ postDurations,
264
+ }
244
265
  }
@@ -1,9 +1,10 @@
1
1
  import { readFileSync, existsSync, mkdirSync } from 'node:fs'
2
2
  import { join, dirname, resolve } from 'node:path'
3
- import { homedir } from 'node:os'
4
3
  import { atomicWriteFileSync } from '../shared/atomic-write'
4
+ import { miphamHome } from './paths.ts'
5
+ import { MIPHAM_DIR } from '../shared/constants.ts'
5
6
 
6
- const MIPHAM_HOME = join(homedir(), '.mipham')
7
+ const MIPHAM_HOME = miphamHome()
7
8
  const TRUST_STORE_PATH = join(MIPHAM_HOME, 'trusted-workspaces.json')
8
9
 
9
10
  export interface TrustedWorkspaces {
@@ -198,7 +199,7 @@ export function resetWorkspaceTrust(): void {
198
199
  */
199
200
  export function warnProjectHooksSkipped(cwd: string): void {
200
201
  process.stderr.write(
201
- `⚠️ Workspace not trusted: skipped hooks from ${join(cwd, '.mipham', 'settings.json')}\n` +
202
+ `⚠️ Workspace not trusted: skipped hooks from ${join(cwd, MIPHAM_DIR, 'settings.json')}\n` +
202
203
  ` (hooks run commands — repository-controlled). Trust this directory in an interactive\n` +
203
204
  ` session to enable them.\n`,
204
205
  )
@@ -1,7 +1,6 @@
1
1
  // apps/cli/src/daemon/database.ts
2
2
  import { Database } from 'bun:sqlite'
3
3
  import { readdirSync, readFileSync, existsSync } from 'node:fs'
4
- import { homedir } from 'node:os'
5
4
  import { join } from 'node:path'
6
5
  import type {
7
6
  DaemonSession,
@@ -12,6 +11,7 @@ import type {
12
11
  CreateSessionInput,
13
12
  SessionStatus,
14
13
  } from './types'
14
+ import { miphamHome } from '../core/paths.ts'
15
15
 
16
16
  export class DaemonDatabase {
17
17
  private db: Database
@@ -340,7 +340,7 @@ export class DaemonDatabase {
340
340
  // ── Migration ──────────────────────────────────────────────
341
341
 
342
342
  migrateFromJsonl(): number {
343
- const sessionsDir = join(homedir(), '.mipham', 'sessions')
343
+ const sessionsDir = miphamHome('sessions')
344
344
 
345
345
  if (!existsSync(sessionsDir)) return 0
346
346
 
@@ -8,7 +8,6 @@
8
8
  import { join } from 'node:path'
9
9
  import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs'
10
10
  import { createServer as createNetServer } from 'node:net'
11
- import { homedir } from 'node:os'
12
11
  import type { Server } from 'bun'
13
12
  import { DaemonDatabase } from './database'
14
13
  import { SessionManager } from './session-manager'
@@ -32,9 +31,9 @@ import type { WecomConfig } from './wecom/types.js'
32
31
  import { parseDingtalkEnv } from './dingtalk/env.js'
33
32
  import type { DingtalkConfig } from './dingtalk/types.js'
34
33
  import { loadConfig } from '../config/loader'
34
+ import { miphamHome } from '../core/paths.ts'
35
35
 
36
- const HOME = homedir()
37
- const MIPHAM_HOME = join(HOME, '.mipham')
36
+ const MIPHAM_HOME = miphamHome()
38
37
  const DB_PATH = join(MIPHAM_HOME, 'daemon.db')
39
38
  const TOKEN_PATH = join(MIPHAM_HOME, 'daemon.token')
40
39
  const PID_FILE = join(MIPHAM_HOME, 'daemon.pid')
@@ -13,13 +13,13 @@
13
13
 
14
14
  import { spawn, type SpawnOptions } from 'node:child_process'
15
15
  import { closeSync, mkdirSync, openSync, readFileSync, statSync } from 'node:fs'
16
- import { homedir } from 'node:os'
17
- import { dirname, join, resolve } from 'node:path'
16
+ import { dirname, resolve } from 'node:path'
17
+ import { miphamHome } from '../core/paths.ts'
18
18
 
19
19
  /** argv sentinel that re-enters this program as a daemon. Not user-facing. */
20
20
  export const DAEMON_ENTRY = '__daemon'
21
21
 
22
- const DEFAULT_LOG_FILE = join(homedir(), '.mipham', 'daemon.log')
22
+ const DEFAULT_LOG_FILE = miphamHome('daemon.log')
23
23
 
24
24
  /**
25
25
  * argv prefix that re-runs *this* program.
@@ -63,10 +63,25 @@ interface WsData {
63
63
  sessionId: string
64
64
  }
65
65
 
66
+ /**
67
+ * Values `MIPHAM_DAEMON_PERMISSION` accepts. The **default stays `'default'`**;
68
+ * every widening is an explicit operator choice.
69
+ *
70
+ * `'auto'` is accepted, and today it means **every tool call is refused**: the
71
+ * mode's static baseline answers `'ask'` for all of them, and the daemon has no
72
+ * classifier to rule on the `'ask'` (the classifier is wired in the CLI's
73
+ * `index.tsx`, not here). Refusing with the mode named is the honest reading —
74
+ * `auto` means "there is no human to ask, let a classifier decide", and with no
75
+ * classifier there is nothing to decide with. The alternative (leaving it out of
76
+ * this set) would silently fall back to `'default'`, which is *more* permissive
77
+ * than what was asked for; a silent widening is the worse of the two ways to be
78
+ * wrong. This entry becomes useful the day the daemon builds a classifier.
79
+ */
66
80
  const DAEMON_PERMISSION_MODES: ReadonlySet<PermissionMode> = new Set<PermissionMode>([
67
81
  'default',
68
82
  'acceptEdits',
69
83
  'plan',
84
+ 'auto',
70
85
  'bypassPermissions',
71
86
  ])
72
87
 
@@ -804,6 +804,7 @@
804
804
  "manual": "manual mode",
805
805
  "accept_edits": "accept edits on",
806
806
  "plan_mode": "plan mode / read-only",
807
+ "auto_mode": "auto mode on",
807
808
  "bypass": "bypass"
808
809
  },
809
810
  "loading": {
@@ -992,6 +993,8 @@
992
993
  "tool_denied_deny_rule": "Tool \"{name}\" blocked by a deny rule (\"{pattern}\"). Deny rules override permission mode — try a different approach, or drop the rule with: /permissions remove \"{pattern}\"",
993
994
  "tool_denied_ask_rule": "Tool \"{name}\" requires approval (ask rule: \"{pattern}\"). Approve when prompted, or stop being asked with: /permissions allow \"{pattern}\"",
994
995
  "tool_denied_mode": "Tool \"{name}\" requires approval under \"{mode}\" mode. Press Shift+Tab to switch permission mode, or stop being asked with: /permissions allow \"{name}\" (or a narrower \"{name}(arg)\")",
996
+ "tool_denied_classifier": "Tool \"{name}\" was blocked in auto mode by the permission classifier ({reason}). This is a policy decision, not a failure — switching modes will not change it, and repeating the identical call returns the same ruling. Change the call so it no longer trips the rule, or allow it outright with: /permissions allow \"{name}\"",
997
+ "tool_denied_classifier_unavailable": "Tool \"{name}\" was held back because the auto-mode classifier could not be reached or its answer could not be read ({reason}). This is NOT a policy decision — the call was never judged, and retrying is appropriate. If this keeps happening, switch modes with Shift+Tab.",
995
998
  "tool_blocked": "Tool \"{name}\" blocked by hook",
996
999
  "user_input_blocked": "User input blocked by hook.",
997
1000
  "dlp_blocked": "Request blocked by DLP policy.",
@@ -804,6 +804,7 @@
804
804
  "manual": "手动模式",
805
805
  "accept_edits": "接受编辑",
806
806
  "plan_mode": "计划模式 / 只读",
807
+ "auto_mode": "自动模式",
807
808
  "bypass": "绕过权限"
808
809
  },
809
810
  "loading": {
@@ -992,6 +993,8 @@
992
993
  "tool_denied_deny_rule": "工具 \"{name}\" 被拒绝规则(\"{pattern}\")阻止。拒绝规则优先于权限模式 — 请改用其他方式,或移除该规则:/permissions remove \"{pattern}\"",
993
994
  "tool_denied_ask_rule": "工具 \"{name}\" 需要批准(ask 规则:\"{pattern}\")。请在提示时批准,或不再询问:/permissions allow \"{pattern}\"",
994
995
  "tool_denied_mode": "工具 \"{name}\" 在 \"{mode}\" 模式下需要批准。按 Shift+Tab 切换权限模式,或不再询问:/permissions allow \"{name}\"(更窄的 \"{name}(arg)\" 亦可)",
996
+ "tool_denied_classifier": "工具 \"{name}\" 在 auto 模式下被权限分类器拦下({reason})。这是策略裁决、不是故障 —— 切换模式不会改变它,把同一次调用原样重发也只会得到同一个裁决。请改变这次调用使其不再触犯该规则,或直接放行:/permissions allow \"{name}\"",
997
+ "tool_denied_classifier_unavailable": "工具 \"{name}\" 被暂缓:auto 模式的分类器不可达,或其回复无法解析({reason})。这**不是**策略裁决 —— 该调用从未被裁决,重试是恰当的。若持续发生,按 Shift+Tab 切换模式。",
995
998
  "tool_blocked": "工具 \"{name}\" 被钩子拦截",
996
999
  "user_input_blocked": "用户输入被钩子拦截。",
997
1000
  "dlp_blocked": "请求被 DLP 策略阻止。",
package/src/index.tsx CHANGED
@@ -1,6 +1,5 @@
1
1
  import React from 'react'
2
2
  import { join, basename } from 'node:path'
3
- import { homedir } from 'node:os'
4
3
  import { existsSync } from 'node:fs'
5
4
  import { render } from 'ink'
6
5
  import * as readline from 'node:readline'
@@ -33,6 +32,7 @@ import { SessionLog } from './core/session-log'
33
32
  import { SessionStore } from './core/session-store'
34
33
  import type { PermissionLevel, MiphamConfig, McpServerConfig } from './shared/types'
35
34
  import { PermissionSystem } from './core/permission'
35
+ import { LlmPermissionClassifier } from './core/permission-classifier'
36
36
  import { SkillsLoader } from './skills/loader'
37
37
  import { PluginManager } from './plugin/plugin-manager'
38
38
  import { loadPlugins } from './plugin/plugin-loader'
@@ -54,7 +54,7 @@ import { initTelemetry, enableTelemetryNow } from './telemetry/index'
54
54
  import { wasPrompted, markPrompted, isInteractive, setTelemetryEnabled } from './telemetry/consent'
55
55
  import { officialEndpointHost } from './telemetry/endpoint'
56
56
  import { getWorkspaceTrust, warnProjectHooksSkipped } from './core/workspace-trust'
57
- import { ARTIFACT_PORT } from './shared/constants'
57
+ import { ARTIFACT_PORT, MIPHAM_DIR } from './shared/constants'
58
58
  import { artifactsRoot } from './artifacts/paths'
59
59
  import { AgentViewManager } from './agent-view/agent-view-manager'
60
60
  import { AgentViewDashboard } from './agent-view/dashboard'
@@ -65,6 +65,7 @@ import { ConfigWizard } from './ui/config-wizard'
65
65
  import enUS from './i18n-core/locales/en-US.json' with { type: 'json' }
66
66
  import zhCN from './i18n-core/locales/zh-CN.json' with { type: 'json' }
67
67
  import type { TranslationMap } from './i18n-core/types'
68
+ import { miphamHome } from './core/paths.ts'
68
69
 
69
70
  // Locale bundles — defined at module level so the remote-attach branch can
70
71
  // reference them before the full bootstrap path runs.
@@ -401,6 +402,21 @@ export async function runApp(options: RunOptions): Promise<void> {
401
402
  const defaultModel = options.model || config.defaultModel
402
403
  const registry = bootstrapProviders(config.providers, defaultProvider, defaultModel)
403
404
 
405
+ // Attach `auto` mode's classifier here, where the provider registry exists —
406
+ // `PermissionSystem` itself imports no provider module, so the classifier is handed
407
+ // in rather than built inside. Doing it on the permission system instead of the
408
+ // engine is deliberate: an engine-side setter would be a new engine capability the
409
+ // daemon would then have to match or be exempted from
410
+ // (`test/integrity/daemon-capability-parity.test.ts`). Because that guard therefore
411
+ // cannot see this line, the wiring is asserted separately at the source level.
412
+ //
413
+ // The model is resolved per ruling through the thunk: capturing the active model
414
+ // here would freeze it at whatever was active at startup, so a user who switches to
415
+ // a cheaper model would keep paying for the old one with nothing on screen to say so.
416
+ permission.setClassifier(
417
+ new LlmPermissionClassifier(registry, { resolveModel: () => registry.getActiveModel() }),
418
+ )
419
+
404
420
  // Load instructions
405
421
  const instructions = new InstructionsLoader()
406
422
  instructions.loadAll(process.cwd())
@@ -529,8 +545,8 @@ export async function runApp(options: RunOptions): Promise<void> {
529
545
  // First-run setup detection: inject mipham-code-setup guidance when
530
546
  // no project or user config exists. This mirrors Claude Code's first-run
531
547
  // onboarding flow — the AI proactively offers to help configure the tool.
532
- const hasProjectConfig = existsSync(join(process.cwd(), '.mipham', 'config.yml'))
533
- const hasUserConfig = existsSync(join(homedir(), '.mipham', 'config.yml'))
548
+ const hasProjectConfig = existsSync(join(process.cwd(), MIPHAM_DIR, 'config.yml'))
549
+ const hasUserConfig = existsSync(miphamHome('config.yml'))
534
550
  const hasMiphamMd = existsSync(join(process.cwd(), 'MIPHAM.md'))
535
551
  if (!hasProjectConfig && !hasUserConfig) {
536
552
  const setupReminder = [
@@ -672,6 +688,14 @@ export async function runApp(options: RunOptions): Promise<void> {
672
688
  process.stderr.write(`⚠ Mipham Code: ${msg}\n`)
673
689
  }
674
690
 
691
+ // Third channel: an unrecognized `permission:` value in config.yml. Until
692
+ // `setDefaultLevel` honoured real mode names, *no* value except `bypass` could
693
+ // report anything — they all just became `default`. Warnings are collected on the
694
+ // instance, so this has to run after the config was applied above.
695
+ for (const msg of engine.getPermission().getInvalidPermissionMode()) {
696
+ process.stderr.write(`⚠ Mipham Code: ${msg}\n`)
697
+ }
698
+
675
699
  // Initialize agent registry and load plugin agents/skills/MCP/hooks
676
700
  const agentRegistry = new AgentRegistry()
677
701
  agentRegistry.loadUserAgents()
@@ -815,8 +839,8 @@ export async function runApp(options: RunOptions): Promise<void> {
815
839
  })
816
840
 
817
841
  // ── First-run detection: show interactive ConfigWizard if no config exists ──
818
- const hasUserConfig = existsSync(join(homedir(), '.mipham', 'config.yml'))
819
- const hasProjectConfig = existsSync(join(process.cwd(), '.mipham', 'config.yml'))
842
+ const hasUserConfig = existsSync(miphamHome('config.yml'))
843
+ const hasProjectConfig = existsSync(join(process.cwd(), MIPHAM_DIR, 'config.yml'))
820
844
  const needsSetup = !hasUserConfig && !hasProjectConfig
821
845
 
822
846
  // Only ask when the first-run wizard is *not* about to take over the
@@ -8,8 +8,8 @@ import {
8
8
  chmodSync,
9
9
  } from 'node:fs'
10
10
  import { join, dirname } from 'node:path'
11
- import { homedir } from 'node:os'
12
11
  import { encrypt, decrypt, getCredentialKey } from '../config/credential-crypto'
12
+ import { miphamHome } from '../core/paths.ts'
13
13
 
14
14
  interface TokenData {
15
15
  accessToken: string
@@ -24,7 +24,7 @@ export class TokenStore {
24
24
  private storeDir: string
25
25
 
26
26
  constructor(storeDir?: string) {
27
- this.storeDir = storeDir || join(homedir(), '.mipham', 'mcp-tokens')
27
+ this.storeDir = storeDir || miphamHome('mcp-tokens')
28
28
  this.key = getCredentialKey(dirname(this.storeDir))
29
29
  }
30
30
 
@@ -8,12 +8,12 @@ import {
8
8
  renameSync,
9
9
  } from 'node:fs'
10
10
  import { join } from 'node:path'
11
- import { homedir } from 'node:os'
12
11
  import { execFileSync } from 'node:child_process'
13
12
  import { validatePlugin } from './plugin-validator'
14
13
  import { atomicWriteFileSync } from '../shared/atomic-write'
14
+ import { miphamHome } from '../core/paths.ts'
15
15
 
16
- const PLUGIN_DIR = join(homedir(), '.mipham', 'plugins')
16
+ const PLUGIN_DIR = miphamHome('plugins')
17
17
 
18
18
  export interface InstalledPlugin {
19
19
  name: string
@@ -539,7 +539,6 @@ export const TOOL_CATEGORIES = [
539
539
  ] as const
540
540
  export const CONFIG_FILE_NAME = 'config.yml'
541
541
  export const MIPHAM_DIR = '.mipham'
542
- export const USER_CONFIG_DIR = '.mipham'
543
542
  export const MEMORY_DIR = 'memory'
544
543
 
545
544
  /** 预置 Ollama 模型 — 与 ollama list 结果合并去重后展示 */
@@ -9,7 +9,7 @@
9
9
  export const PACKAGE_NAME = '@miphamai/cli' as const
10
10
 
11
11
  /** 当前发布版本 */
12
- export const PACKAGE_VERSION = '0.82.0' as const
12
+ export const PACKAGE_VERSION = '0.84.0' as const
13
13
 
14
14
  /** npm install 全局安装命令 */
15
15
  export const NPM_INSTALL_COMMAND = `npm install -g ${PACKAGE_NAME}` as const
@@ -67,7 +67,14 @@ export interface Message {
67
67
  }
68
68
 
69
69
  // ── Tool Types ──
70
- export type ToolPermission = 'auto' | 'ask' | 'bypass'
70
+ /**
71
+ * Legacy 3-level tool permission (predates `PermissionMode`).
72
+ * `'self'` means "let the tool self-decide" — i.e. allowed, but not flagged as `'bypass'`.
73
+ * Renamed from `'auto'` (2026-09-22) so that `auto` can name the *classifier mode*
74
+ * without a second meaning living in `PermissionLevel` — see
75
+ * `docs/superpowers/specs/2026-09-22-permission-classifier-design.md` §3.1.
76
+ */
77
+ export type ToolPermission = 'self' | 'ask' | 'bypass'
71
78
  export type ToolCategory =
72
79
  'file' | 'exec' | 'agent' | 'network' | 'system' | 'artifact' | 'scheduling'
73
80
 
@@ -326,11 +333,44 @@ export interface InstructionFile {
326
333
  }
327
334
 
328
335
  // ── Permission Types ──
329
- /** Four explicit permission modes matching Claude Code's permission architecture */
330
- export type PermissionMode = 'default' | 'acceptEdits' | 'plan' | 'bypassPermissions'
331
-
332
- /** Backward-compatible alias: PermissionMode plus legacy 'auto'/'ask'/'bypass' */
333
- export type PermissionLevel = PermissionMode | 'auto' | 'ask' | 'bypass'
336
+ /**
337
+ * Permission modes, matching Claude Code's permission architecture.
338
+ *
339
+ * `bypassPermissions` is legal but **off the Shift+Tab cycle** — the cycle is the
340
+ * four slots Claude Code's own cycle array lists (`default`, `acceptEdits`,
341
+ * `plan`, `auto`), while this union is the full set the config may request. See
342
+ * `ALL_MODES` / `MODE_CYCLE` in `core/permission-config.ts` for why those two
343
+ * arrays are separate, and why merging them silently demotes
344
+ * `bypassPermissions`.
345
+ *
346
+ * `auto` behaves unlike the other four: it does not decide anything statically.
347
+ * Its baseline answers `'ask'` for **every** call, and the ruling is delegated to
348
+ * the LLM permission classifier (`core/permission-classifier.ts`). Two
349
+ * consequences worth knowing before reading code that switches on this union:
350
+ *
351
+ * - `auto` has **no static width**, so a "which mode is narrower" measurement
352
+ * taken from the static chain reads it as narrower than `plan` — a
353
+ * measurement of the wrong object, not a fact about `auto`.
354
+ * - A mode whose static baseline is `'ask'` must not be *reached* by a caller
355
+ * that never consults the classifier, or it degrades into "refuse everything".
356
+ *
357
+ * This copy is kept in step with `packages/shared/src/types.ts` mechanically:
358
+ * `test/integrity/shared-types-parity.test.ts` asserts that every declaration
359
+ * the two files share has the same member set (and, for union aliases, the same
360
+ * literal set). Member *lists* are all it covers — a stale prose default is
361
+ * still a human's to catch.
362
+ */
363
+ export type PermissionMode = 'default' | 'acceptEdits' | 'plan' | 'auto' | 'bypassPermissions'
364
+
365
+ /**
366
+ * Backward-compatible alias: `PermissionMode` plus the legacy 3-level
367
+ * `'self'`/`'ask'`/`'bypass'`. The legacy level was named `'auto'` until 2026-09-22;
368
+ * that name now belongs to the classifier mode, so the two can no longer be
369
+ * confused for one another (the `fc5afd3a` incident was exactly that confusion:
370
+ * a config `permission: auto` meaning "tool self-decides" silently becoming
371
+ * "run everything").
372
+ */
373
+ export type PermissionLevel = PermissionMode | 'self' | 'ask' | 'bypass'
334
374
 
335
375
  /** Org-level restrictions that cap or forbid specific permission modes. */
336
376
  export interface PermissionRestrictions {
@@ -9,12 +9,11 @@
9
9
  import { readFileSync, existsSync, copyFileSync, mkdirSync, chmodSync } from 'node:fs'
10
10
  import { join } from 'node:path'
11
11
  import { execSync } from 'node:child_process'
12
- import { homedir } from 'node:os'
13
12
  import { PACKAGE_VERSION } from './package-info'
13
+ import { miphamHome } from '../core/paths.ts'
14
14
 
15
15
  const PACKAGE = '@miphamai/cli'
16
- const HOME = homedir()
17
- const MIPHAM_HOME = join(HOME, '.mipham')
16
+ const MIPHAM_HOME = miphamHome()
18
17
  const CONFIG_PATH = join(MIPHAM_HOME, 'config.yml')
19
18
 
20
19
  // Registry fallback chain: npm default → npmmirror (China mirror)