@miphamai/cli 0.83.0 → 0.85.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/bin/mipham.ts +20 -5
- package/package.json +1 -1
- package/skills/standard/mipham-code-setup.SKILL.md +30 -8
- package/src/agent/agent-context.ts +5 -5
- package/src/agent/agent-experience.ts +2 -2
- package/src/agent/agent-registry.ts +4 -4
- package/src/agent/cross-session/discovery.ts +3 -2
- package/src/agent/cross-session/file-inbox.ts +2 -2
- package/src/agent/effectiveness-tracker.ts +2 -2
- package/src/agent/pattern-analyzer.ts +3 -4
- package/src/agent/sub-agent.ts +12 -2
- package/src/agent/types.ts +4 -1
- package/src/commands/autoloop-journal.ts +2 -2
- package/src/commands/environment.ts +2 -1
- package/src/commands/loop-scaffold.ts +2 -1
- package/src/commands/project.ts +86 -33
- package/src/config/keys-manager.ts +2 -2
- package/src/config/loader.ts +11 -10
- package/src/config/preferences.ts +3 -4
- package/src/core/auto-memory.ts +2 -3
- package/src/core/constitution-loader.ts +3 -4
- package/src/core/crsi-producer.ts +3 -3
- package/src/core/crsi-sandbox.ts +3 -2
- package/src/core/dream-engine.ts +2 -2
- package/src/core/engine.ts +38 -6
- package/src/core/error-signature-db.ts +2 -2
- package/src/core/eval-harness.ts +4 -3
- package/src/core/improvement-track.ts +5 -6
- package/src/core/instructions.ts +28 -4
- package/src/core/memory/memory-loader.ts +2 -3
- package/src/core/paths.ts +21 -1
- package/src/core/permission-audit.ts +120 -0
- package/src/core/permission-classifier.ts +449 -0
- package/src/core/permission-config.ts +106 -15
- package/src/core/permission.ts +369 -16
- package/src/core/rule-engine.ts +2 -2
- package/src/core/rules-loader.ts +3 -2
- package/src/core/session-log.ts +2 -3
- package/src/core/session-store.ts +2 -3
- package/src/core/workspace-trust.ts +4 -3
- package/src/daemon/database.ts +2 -2
- package/src/daemon/index.ts +2 -3
- package/src/daemon/launch.ts +3 -3
- package/src/daemon/server.ts +15 -0
- package/src/i18n-core/locales/en-US.json +7 -1
- package/src/i18n-core/locales/zh-CN.json +7 -1
- package/src/index.tsx +30 -6
- package/src/mcp/token-store.ts +2 -2
- package/src/plugin/plugin-manager.ts +2 -2
- package/src/shared/constants.ts +0 -1
- package/src/shared/package-info.ts +1 -1
- package/src/shared/types.ts +46 -6
- package/src/shared/update.ts +255 -20
- package/src/skills/bundled-skills.ts +1 -1
- package/src/skills/loader.ts +2 -3
- package/src/skills/marketplace.ts +2 -3
- package/src/skills/registry.ts +2 -2
- package/src/skills/skill-assets.ts +2 -2
- package/src/skills/usage.ts +2 -2
- package/src/telemetry/consent.ts +2 -3
- package/src/tools/agent/enter-plan.ts +3 -2
- package/src/tools/agent/exit-plan.ts +1 -1
- package/src/tools/agent/list-agents.ts +1 -1
- package/src/tools/agent/memory.ts +3 -3
- package/src/tools/agent/plan.ts +3 -2
- package/src/tools/agent/report-findings.ts +1 -1
- package/src/tools/agent/send-message.ts +1 -1
- package/src/tools/agent/skill.ts +1 -1
- package/src/tools/exec/git.ts +2 -2
- package/src/tools/exec/task.ts +1 -1
- package/src/tools/file/glob.ts +1 -1
- package/src/tools/file/grep.ts +1 -1
- package/src/tools/file/read.ts +1 -1
- package/src/tools/network/web-fetch.ts +1 -1
- package/src/tools/network/web-search.ts +1 -1
- package/src/tools/scheduling/cron.ts +5 -5
- package/src/tools/scheduling/schedule-wakeup.ts +1 -1
- package/src/tools/system/config.ts +2 -2
- package/src/tools/system/tool-search.ts +1 -1
- package/src/ui/app.tsx +51 -9
- package/src/ui/commands.ts +26 -21
- package/src/ui/config-wizard.tsx +2 -2
- package/src/ui/input.tsx +15 -3
- package/src/workflow/journal.ts +2 -2
package/src/config/loader.ts
CHANGED
|
@@ -8,7 +8,6 @@ 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 { parse as parseYaml, stringify as stringifyYaml } from 'yaml'
|
|
13
12
|
import { atomicWriteFileSync } from '../shared/atomic-write'
|
|
14
13
|
import type {
|
|
@@ -29,8 +28,10 @@ import {
|
|
|
29
28
|
} from './defaults'
|
|
30
29
|
import { getCredentialKey, encryptApiKey, decryptApiKey, ENC_PREFIX } from './credential-crypto'
|
|
31
30
|
import type { SettingsHooks } from '../core/hooks-config'
|
|
31
|
+
import { miphamHome } from '../core/paths.ts'
|
|
32
|
+
import { MIPHAM_DIR } from '../shared/constants.ts'
|
|
32
33
|
|
|
33
|
-
const MIPHAM_HOME =
|
|
34
|
+
const MIPHAM_HOME = miphamHome()
|
|
34
35
|
const BACKUP_PREFIX = 'config.backup-'
|
|
35
36
|
|
|
36
37
|
/**
|
|
@@ -212,7 +213,7 @@ export function tryRestoreFromBackup(configPath: string): boolean {
|
|
|
212
213
|
function loadMcpJson(cwd: string): McpServerConfig[] {
|
|
213
214
|
const servers: McpServerConfig[] = []
|
|
214
215
|
const searchPaths = [
|
|
215
|
-
join(cwd,
|
|
216
|
+
join(cwd, MIPHAM_DIR, 'mcp.json'),
|
|
216
217
|
join(cwd, '.mcp.json'),
|
|
217
218
|
join(MIPHAM_HOME, 'mcp.json'),
|
|
218
219
|
]
|
|
@@ -294,7 +295,7 @@ export function loadSettingsJson(
|
|
|
294
295
|
|
|
295
296
|
const searchPaths: Array<{ path: string; readHooks: boolean; isProject?: boolean }> = [
|
|
296
297
|
{
|
|
297
|
-
path: join(cwd,
|
|
298
|
+
path: join(cwd, MIPHAM_DIR, 'settings.json'),
|
|
298
299
|
readHooks: options.includeProjectHooks ?? false,
|
|
299
300
|
isProject: true,
|
|
300
301
|
},
|
|
@@ -366,7 +367,7 @@ export type SettingsScope = 'project' | 'user'
|
|
|
366
367
|
export function settingsPathFor(scope: SettingsScope, cwd: string = process.cwd()): string {
|
|
367
368
|
return scope === 'user'
|
|
368
369
|
? join(MIPHAM_HOME, 'settings.json')
|
|
369
|
-
: join(cwd,
|
|
370
|
+
: join(cwd, MIPHAM_DIR, 'settings.json')
|
|
370
371
|
}
|
|
371
372
|
|
|
372
373
|
/**
|
|
@@ -442,7 +443,7 @@ export function removeSettingsRule(
|
|
|
442
443
|
}
|
|
443
444
|
|
|
444
445
|
export function loadConfig(cwd: string = process.cwd()): MiphamConfig {
|
|
445
|
-
const configPath = join(cwd,
|
|
446
|
+
const configPath = join(cwd, MIPHAM_DIR, 'config.yml')
|
|
446
447
|
const userConfigPath = join(MIPHAM_HOME, 'config.yml')
|
|
447
448
|
|
|
448
449
|
let config = { ...DEFAULT_CONFIG }
|
|
@@ -629,7 +630,7 @@ function mergeCredentialMaskingFile(
|
|
|
629
630
|
export function loadCredentialMaskingConfig(cwd: string = process.cwd()): CredentialMaskingConfig {
|
|
630
631
|
let merged = { ...DEFAULT_CREDENTIAL_MASKING_CONFIG }
|
|
631
632
|
merged = mergeCredentialMaskingFile(merged, join(MIPHAM_HOME, 'config.yml'), true)
|
|
632
|
-
merged = mergeCredentialMaskingFile(merged, join(cwd,
|
|
633
|
+
merged = mergeCredentialMaskingFile(merged, join(cwd, MIPHAM_DIR, 'config.yml'), false)
|
|
633
634
|
return merged
|
|
634
635
|
}
|
|
635
636
|
|
|
@@ -655,7 +656,7 @@ export function loadUserCredentialMaskingConfig(): CredentialMaskingConfig {
|
|
|
655
656
|
* Load background agent configuration from config sources.
|
|
656
657
|
*/
|
|
657
658
|
export function loadBackgroundAgentConfig(cwd: string = process.cwd()): BackgroundAgentConfig {
|
|
658
|
-
const configPath = join(cwd,
|
|
659
|
+
const configPath = join(cwd, MIPHAM_DIR, 'config.yml')
|
|
659
660
|
const userConfigPath = join(MIPHAM_HOME, 'config.yml')
|
|
660
661
|
|
|
661
662
|
let merged = { ...DEFAULT_BACKGROUND_AGENT_CONFIG }
|
|
@@ -688,7 +689,7 @@ export function loadBackgroundAgentConfig(cwd: string = process.cwd()): Backgrou
|
|
|
688
689
|
* Merges project-level over user-level. Returns defaults if no section present.
|
|
689
690
|
*/
|
|
690
691
|
export function loadCrossSessionConfig(cwd: string = process.cwd()): CrossSessionConfig {
|
|
691
|
-
const configPath = join(cwd,
|
|
692
|
+
const configPath = join(cwd, MIPHAM_DIR, 'config.yml')
|
|
692
693
|
const userConfigPath = join(MIPHAM_HOME, 'config.yml')
|
|
693
694
|
|
|
694
695
|
let merged = { ...DEFAULT_CROSS_SESSION_CONFIG }
|
|
@@ -751,7 +752,7 @@ function decryptProviderApiKeys(providers: ProviderConfig[] | undefined): void {
|
|
|
751
752
|
*/
|
|
752
753
|
export function getProviderApiKey(providerId: string, cwd: string = process.cwd()): string | null {
|
|
753
754
|
const userConfigPath = join(MIPHAM_HOME, 'config.yml')
|
|
754
|
-
const projectConfigPath = join(cwd,
|
|
755
|
+
const projectConfigPath = join(cwd, MIPHAM_DIR, 'config.yml')
|
|
755
756
|
const configPath = existsSync(userConfigPath) ? userConfigPath : projectConfigPath
|
|
756
757
|
|
|
757
758
|
try {
|
|
@@ -6,11 +6,10 @@
|
|
|
6
6
|
* NOT for secrets — this file is plain JSON, not encrypted.
|
|
7
7
|
*/
|
|
8
8
|
import { readFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
9
|
-
import { join } from 'node:path'
|
|
10
|
-
import { homedir } from 'node:os'
|
|
11
9
|
import { atomicWriteFileSync } from '../shared/atomic-write'
|
|
10
|
+
import { miphamHome } from '../core/paths.ts'
|
|
12
11
|
|
|
13
|
-
const PREFS_PATH =
|
|
12
|
+
const PREFS_PATH = miphamHome('preferences.json')
|
|
14
13
|
|
|
15
14
|
function readPrefs(): Record<string, string> {
|
|
16
15
|
try {
|
|
@@ -26,7 +25,7 @@ function readPrefs(): Record<string, string> {
|
|
|
26
25
|
|
|
27
26
|
function writePrefs(prefs: Record<string, string>): void {
|
|
28
27
|
try {
|
|
29
|
-
const dir =
|
|
28
|
+
const dir = miphamHome()
|
|
30
29
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
31
30
|
// 原子写:裸 writeFileSync 原地截断,崩在写中途就留下一份不可解析的文件,
|
|
32
31
|
// 而 readPrefs 把不可解析吞成「空」⇒ **全部**偏好静默消失(不是丢一项)。
|
package/src/core/auto-memory.ts
CHANGED
|
@@ -18,8 +18,7 @@ import type { EffectivenessTracker } from '../agent/effectiveness-tracker.js'
|
|
|
18
18
|
import type { ErrorSignatureDB } from './error-signature-db.js'
|
|
19
19
|
import type { CrsiProvenanceBridge } from '../agent/crsi-provenance-bridge.js'
|
|
20
20
|
import { getMetrics } from './metrics'
|
|
21
|
-
import {
|
|
22
|
-
import { homedir } from 'node:os'
|
|
21
|
+
import { miphamHome } from './paths.ts'
|
|
23
22
|
|
|
24
23
|
// ── Types ──
|
|
25
24
|
|
|
@@ -74,7 +73,7 @@ export interface TurnReflection {
|
|
|
74
73
|
|
|
75
74
|
// ── Constants ──
|
|
76
75
|
|
|
77
|
-
const DEFAULT_MEMORY_DIR =
|
|
76
|
+
const DEFAULT_MEMORY_DIR = miphamHome('memory')
|
|
78
77
|
|
|
79
78
|
/** Minimum number of similar failures before a CRSI rule is generated. */
|
|
80
79
|
const CRSI_RULE_THRESHOLD = 2
|
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
|
|
17
|
-
import {
|
|
18
|
-
import { homedir } from 'node:os'
|
|
17
|
+
import { miphamHome } from './paths.ts'
|
|
19
18
|
import alignmentVocabulary from './alignment-vocabulary.json' with { type: 'json' }
|
|
20
19
|
|
|
21
20
|
// ── Types ──
|
|
@@ -76,7 +75,7 @@ export class ConstitutionLoader {
|
|
|
76
75
|
private cached: MiphamConstitution | null = null
|
|
77
76
|
|
|
78
77
|
constructor(customPath?: string) {
|
|
79
|
-
this.path = customPath ||
|
|
78
|
+
this.path = customPath || miphamHome('ai-guardrails.yml')
|
|
80
79
|
}
|
|
81
80
|
|
|
82
81
|
/**
|
|
@@ -102,7 +101,7 @@ export class ConstitutionLoader {
|
|
|
102
101
|
// Write the default constitution to disk for visibility
|
|
103
102
|
this.cached = DEFAULT_CONSTITUTION
|
|
104
103
|
try {
|
|
105
|
-
const dir =
|
|
104
|
+
const dir = miphamHome()
|
|
106
105
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
107
106
|
writeFileSync(this.path, this.serializeToYaml(DEFAULT_CONSTITUTION), 'utf-8')
|
|
108
107
|
} catch {
|
|
@@ -15,7 +15,7 @@ import type { MetaRule } from './meta-rule-engine'
|
|
|
15
15
|
import type { Llm } from '../providers/llm'
|
|
16
16
|
import { readdirSync, appendFileSync, readFileSync, existsSync, mkdirSync, rmSync } from 'node:fs'
|
|
17
17
|
import { join } from 'node:path'
|
|
18
|
-
import {
|
|
18
|
+
import { miphamHome } from './paths.ts'
|
|
19
19
|
|
|
20
20
|
/** 教训文件(相对仓库根)。预建,沙箱只能改已存在文件。 */
|
|
21
21
|
export const LESSONS_FILE = 'apps/cli/crsi-lessons.md'
|
|
@@ -514,7 +514,7 @@ export interface ProseProposalRecord {
|
|
|
514
514
|
}
|
|
515
515
|
|
|
516
516
|
function proseLedgerFile(): string {
|
|
517
|
-
return
|
|
517
|
+
return miphamHome('crsi', 'prose-proposals.jsonl')
|
|
518
518
|
}
|
|
519
519
|
|
|
520
520
|
/** 该信号是否已生成过散文提议。 */
|
|
@@ -537,7 +537,7 @@ export function hasProposedProse(id: string): boolean {
|
|
|
537
537
|
/** 追加一条散文提议记录(append-only,非关键——失败不影响提议本身)。 */
|
|
538
538
|
export function appendProseProposal(record: ProseProposalRecord): void {
|
|
539
539
|
try {
|
|
540
|
-
mkdirSync(
|
|
540
|
+
mkdirSync(miphamHome('crsi'), { recursive: true })
|
|
541
541
|
appendFileSync(proseLedgerFile(), JSON.stringify(record) + '\n', 'utf-8')
|
|
542
542
|
} catch {
|
|
543
543
|
// ledger 非关键,失败不影响提议本身
|
package/src/core/crsi-sandbox.ts
CHANGED
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
import { execSync } from 'node:child_process'
|
|
18
18
|
import { mkdirSync, rmSync, existsSync, writeFileSync, readFileSync, readdirSync } from 'node:fs'
|
|
19
19
|
import { join, resolve, sep, posix } from 'node:path'
|
|
20
|
-
import { tmpdir
|
|
20
|
+
import { tmpdir } from 'node:os'
|
|
21
21
|
import { randomUUID } from 'node:crypto'
|
|
22
22
|
import { LESSONS_FILE, MANAGED_RULES_FILE } from './crsi-producer'
|
|
23
|
+
import { miphamHome } from './paths.ts'
|
|
23
24
|
|
|
24
25
|
// ── Types ──
|
|
25
26
|
|
|
@@ -86,7 +87,7 @@ export interface CrsiSessionReport {
|
|
|
86
87
|
|
|
87
88
|
const WORKTREE_PREFIX = 'crsi-sandbox-'
|
|
88
89
|
const TEST_TIMEOUT_MS = 120_000 // 2 minutes
|
|
89
|
-
const REPORT_DIR =
|
|
90
|
+
const REPORT_DIR = miphamHome('crsi-sandbox')
|
|
90
91
|
|
|
91
92
|
/**
|
|
92
93
|
* 自改进的「不可变基础」(immutable base)——按语义角色三类。
|
package/src/core/dream-engine.ts
CHANGED
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
statSync,
|
|
36
36
|
} from 'node:fs'
|
|
37
37
|
import { join } from 'node:path'
|
|
38
|
-
import {
|
|
38
|
+
import { miphamHome } from './paths.ts'
|
|
39
39
|
|
|
40
40
|
// ── Types ──
|
|
41
41
|
|
|
@@ -78,7 +78,7 @@ interface MemoryFile {
|
|
|
78
78
|
|
|
79
79
|
// ── Constants ──
|
|
80
80
|
|
|
81
|
-
const DEFAULT_MEMORY_DIR =
|
|
81
|
+
const DEFAULT_MEMORY_DIR = miphamHome('memory')
|
|
82
82
|
const STALE_DAYS = 30
|
|
83
83
|
const SIMILARITY_THRESHOLD = 0.65
|
|
84
84
|
|
package/src/core/engine.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { ChatRequest } from '../providers/registry'
|
|
|
11
11
|
import type { Llm } from '../providers/llm'
|
|
12
12
|
import { ContextManager } from './context'
|
|
13
13
|
import { PermissionSystem } from './permission'
|
|
14
|
+
import type { ApprovalDecision } from './permission'
|
|
14
15
|
import type { HookEngine } from './hooks'
|
|
15
16
|
import type { ArtifactServer } from '../artifacts/server'
|
|
16
17
|
import type { AgentRegistry } from '../agent/agent-registry'
|
|
@@ -701,7 +702,7 @@ export class QueryEngine {
|
|
|
701
702
|
const toolCallRecords: ToolCallRecord[] = []
|
|
702
703
|
for (const toolUse of toolUses) {
|
|
703
704
|
const toolStart = Date.now()
|
|
704
|
-
const result = await this.executeTool(toolUse.name, toolUse.input)
|
|
705
|
+
const result = await this.executeTool(toolUse.name, toolUse.input, signal)
|
|
705
706
|
yield {
|
|
706
707
|
type: 'tool_result',
|
|
707
708
|
tool_use_id: toolUse.id,
|
|
@@ -1041,7 +1042,7 @@ export class QueryEngine {
|
|
|
1041
1042
|
|
|
1042
1043
|
// Execute tools and feed results back to the model for the next turn
|
|
1043
1044
|
for (const toolUse of toolUses) {
|
|
1044
|
-
const result = await this.executeTool(toolUse.name, toolUse.input)
|
|
1045
|
+
const result = await this.executeTool(toolUse.name, toolUse.input, signal)
|
|
1045
1046
|
lastActivity = Date.now()
|
|
1046
1047
|
yield {
|
|
1047
1048
|
type: 'tool_result',
|
|
@@ -1068,7 +1069,11 @@ export class QueryEngine {
|
|
|
1068
1069
|
// Max turns reached — safety limit, stop gracefully
|
|
1069
1070
|
}
|
|
1070
1071
|
|
|
1071
|
-
private async executeTool(
|
|
1072
|
+
private async executeTool(
|
|
1073
|
+
name: string,
|
|
1074
|
+
params: Record<string, unknown>,
|
|
1075
|
+
signal?: AbortSignal,
|
|
1076
|
+
): Promise<ToolResult> {
|
|
1072
1077
|
getMetrics().toolCalls.inc({ tool_name: name })
|
|
1073
1078
|
const tool = this.tools.get(name)
|
|
1074
1079
|
if (!tool) {
|
|
@@ -1080,12 +1085,20 @@ export class QueryEngine {
|
|
|
1080
1085
|
return { success: false, content: '', error: `Unknown tool: ${name}${hint}` }
|
|
1081
1086
|
}
|
|
1082
1087
|
|
|
1083
|
-
// Security: check permission before executing
|
|
1084
|
-
|
|
1088
|
+
// Security: check permission before executing.
|
|
1089
|
+
//
|
|
1090
|
+
// `resolveApproval` answers the same question `needsApproval` did for every mode
|
|
1091
|
+
// except `auto`, where it additionally lets the classifier rule on a call the
|
|
1092
|
+
// static chain could only refuse. It is deliberately a superset — the earlier
|
|
1093
|
+
// `needsApproval(...)` check would have been a *second* gate, and a second gate
|
|
1094
|
+
// is how a call refused here gets allowed there. The signal is the caller's, so
|
|
1095
|
+
// an interrupt cancels a ruling in flight (the classifier denies on abort).
|
|
1096
|
+
const decision = await this.permission.resolveApproval(tool, params, { signal })
|
|
1097
|
+
if (decision.level === 'ask') {
|
|
1085
1098
|
// P1-4: Increment consecutive block counter; if limit exceeded,
|
|
1086
1099
|
// tell the model to move on instead of retrying.
|
|
1087
1100
|
const limitExceeded = this.permission.incrementBlockCounter()
|
|
1088
|
-
const baseError = this.buildDenialError(name, tool, params)
|
|
1101
|
+
const baseError = this.buildDenialError(name, tool, params, decision)
|
|
1089
1102
|
const moveOnHint = limitExceeded
|
|
1090
1103
|
? '\n(Consecutive block limit reached. Please try a different approach or ask the user for guidance.)'
|
|
1091
1104
|
: ''
|
|
@@ -1451,12 +1464,31 @@ export class QueryEngine {
|
|
|
1451
1464
|
/**
|
|
1452
1465
|
* Build a rich permission-denial error naming the mode (level), the setting
|
|
1453
1466
|
* (rule/level) that caused the denial, and the correct fix (#52).
|
|
1467
|
+
*
|
|
1468
|
+
* The `decision` comes from `resolveApproval` and is used rather than re-derived:
|
|
1469
|
+
* `explainDenial` can only describe the *static* chain, so for a classifier
|
|
1470
|
+
* refusal it would report the underlying `mode-baseline`/`tool-default` and tell
|
|
1471
|
+
* the model to switch modes — advice that changes nothing, because the refusal is
|
|
1472
|
+
* the classifier's, not the mode's. The parameter is optional so the static path
|
|
1473
|
+
* keeps working unchanged where no decision is at hand.
|
|
1454
1474
|
*/
|
|
1455
1475
|
private buildDenialError(
|
|
1456
1476
|
name: string,
|
|
1457
1477
|
tool: ToolDefinition,
|
|
1458
1478
|
params: Record<string, unknown>,
|
|
1479
|
+
decision?: ApprovalDecision,
|
|
1459
1480
|
): string {
|
|
1481
|
+
if (decision?.source === 'classifier' && decision.denialReason === 'classifier-deny') {
|
|
1482
|
+
// Two different facts, and the model acts differently on each: a policy
|
|
1483
|
+
// refusal is final, while an unreachable/unreadable classifier means the call
|
|
1484
|
+
// was *held back* and a retry is appropriate. Telling it "denied" for the
|
|
1485
|
+
// second makes it abandon work that was never actually judged.
|
|
1486
|
+
const reason = decision.classifierReason ?? ''
|
|
1487
|
+
return decision.retryable
|
|
1488
|
+
? t('errors.tool_denied_classifier_unavailable', { name, reason })
|
|
1489
|
+
: t('errors.tool_denied_classifier', { name, reason })
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1460
1492
|
const { reason, rulePattern } = this.permission.explainDenial(tool, params)
|
|
1461
1493
|
const mode = this.permission.getMode()
|
|
1462
1494
|
switch (reason) {
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
18
18
|
import { join } from 'node:path'
|
|
19
|
-
import { homedir } from 'node:os'
|
|
20
19
|
import { randomUUID } from 'node:crypto'
|
|
20
|
+
import { miphamHome } from './paths.ts'
|
|
21
21
|
|
|
22
22
|
// ── Types ──
|
|
23
23
|
|
|
@@ -61,7 +61,7 @@ export interface ErrorSignatureStats {
|
|
|
61
61
|
|
|
62
62
|
// ── Constants ──
|
|
63
63
|
|
|
64
|
-
const DEFAULT_STORE_DIR =
|
|
64
|
+
const DEFAULT_STORE_DIR = miphamHome('sis')
|
|
65
65
|
const STORE_FILE = 'error-signatures.json'
|
|
66
66
|
const MIN_SUCCESS_RATE = 0.5 // below this → degraded
|
|
67
67
|
const RETIREMENT_RATE = 0.2 // below this and > 90 days old → retired
|
package/src/core/eval-harness.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { join } from 'node:path'
|
|
15
|
-
import { tmpdir
|
|
15
|
+
import { tmpdir } from 'node:os'
|
|
16
16
|
import { mkdirSync, appendFileSync, readFileSync, existsSync } from 'node:fs'
|
|
17
17
|
import { ExperienceRuleEngine } from './rule-engine'
|
|
18
18
|
import { ConstitutionLoader, DEFAULT_CONSTITUTION } from './constitution-loader'
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
import type { CrsiSignal } from './crsi-producer'
|
|
38
38
|
import { predictionHit } from './improvement-track'
|
|
39
39
|
import { loadBehaviorTasks, judgeBehaviorTask } from './behavior-tasks'
|
|
40
|
+
import { miphamHome } from './paths.ts'
|
|
40
41
|
|
|
41
42
|
// ── Types ──
|
|
42
43
|
|
|
@@ -95,7 +96,7 @@ export function regressedAnchors(results: EvalResult[]): string[] {
|
|
|
95
96
|
|
|
96
97
|
// ── Rewards log (path A Phase 1: 奖励信号持久化) ──
|
|
97
98
|
|
|
98
|
-
const SCORES_FILE =
|
|
99
|
+
const SCORES_FILE = miphamHome('crsi', 'eval-scores.jsonl')
|
|
99
100
|
|
|
100
101
|
/** 落盘的契约粒度投影 —— 只要 id/passed/role(EvalResult 的 description/detail 不落盘)。 */
|
|
101
102
|
export interface ContractResultRecord {
|
|
@@ -117,7 +118,7 @@ export function appendEvalScore(
|
|
|
117
118
|
report: { score: number; passed: number; total: number; results?: EvalResult[] },
|
|
118
119
|
): void {
|
|
119
120
|
try {
|
|
120
|
-
mkdirSync(
|
|
121
|
+
mkdirSync(miphamHome('crsi'), { recursive: true })
|
|
121
122
|
appendFileSync(
|
|
122
123
|
SCORES_FILE,
|
|
123
124
|
JSON.stringify({
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// CRSI 改进轨:噪声自适应改进判定 + 台账 + pending verdict 闸。
|
|
2
2
|
// A1 不破:verdict / minEffect / 改进率全是确定性算术(均值/标准差/阈值/Wilson),无 LLM 裁判。
|
|
3
3
|
import { readFileSync, existsSync, mkdirSync, rmSync } from 'node:fs'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { homedir } from 'node:os'
|
|
6
4
|
import { atomicWriteFileSync } from '../shared/atomic-write'
|
|
7
5
|
import type { SkillDeltaSample } from './task-performance'
|
|
6
|
+
import { miphamHome } from './paths.ts'
|
|
8
7
|
|
|
9
8
|
export type ImprovementVerdict = 'improved' | 'regressed' | 'inconclusive'
|
|
10
9
|
|
|
@@ -181,12 +180,12 @@ export function formatCostLine(report: ImprovementReport): string | null {
|
|
|
181
180
|
// ── 台账 ──
|
|
182
181
|
|
|
183
182
|
export function improvementPath(): string {
|
|
184
|
-
return
|
|
183
|
+
return miphamHome('crsi', 'improvements.jsonl')
|
|
185
184
|
}
|
|
186
185
|
|
|
187
186
|
export function appendImprovement(record: ImprovementRecord): void {
|
|
188
187
|
const file = improvementPath()
|
|
189
|
-
mkdirSync(
|
|
188
|
+
mkdirSync(miphamHome('crsi'), { recursive: true })
|
|
190
189
|
// 原子激活(④):整账本读-改-写 + temp 文件 rename(见 shared/atomic-write),读者要么见旧要么见新。
|
|
191
190
|
// 非原子的 appendFileSync 写中途崩溃会留撕裂行,readImprovements 会 JSON.parse 抛错。
|
|
192
191
|
const existing = readImprovements()
|
|
@@ -214,7 +213,7 @@ export function readImprovements(): ImprovementRecord[] {
|
|
|
214
213
|
// (temp+rename,见 shared/atomic-write),替代易失内存变量(进程重启即丢、无 manifest)。
|
|
215
214
|
|
|
216
215
|
export function pendingVerdictPath(): string {
|
|
217
|
-
return
|
|
216
|
+
return miphamHome('crsi', 'pending-verdict.json')
|
|
218
217
|
}
|
|
219
218
|
|
|
220
219
|
export function setPendingVerdict(v: ImprovementVerdict | null): void {
|
|
@@ -223,7 +222,7 @@ export function setPendingVerdict(v: ImprovementVerdict | null): void {
|
|
|
223
222
|
rmSync(file, { force: true }) // 原子清除(unlink 原子)
|
|
224
223
|
return
|
|
225
224
|
}
|
|
226
|
-
mkdirSync(
|
|
225
|
+
mkdirSync(miphamHome('crsi'), { recursive: true })
|
|
227
226
|
atomicWriteFileSync(file, JSON.stringify({ verdict: v, timestamp: new Date().toISOString() }))
|
|
228
227
|
}
|
|
229
228
|
|
package/src/core/instructions.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'node:fs'
|
|
2
2
|
import { join, resolve, relative, sep, isAbsolute } from 'node:path'
|
|
3
|
-
import { homedir } from 'node:os'
|
|
4
3
|
import { execSync } from 'node:child_process'
|
|
5
4
|
import { parse as parseYaml } from 'yaml'
|
|
6
5
|
import type { InstructionFile } from '../shared/index.ts'
|
|
@@ -11,6 +10,7 @@ import {
|
|
|
11
10
|
buildCrsiLessonsBlock,
|
|
12
11
|
type CrsiLessonSummary,
|
|
13
12
|
} from './crsi-producer'
|
|
13
|
+
import { miphamHome } from './paths.ts'
|
|
14
14
|
|
|
15
15
|
export interface FrontmatterResult {
|
|
16
16
|
data: Record<string, unknown>
|
|
@@ -127,8 +127,7 @@ export class InstructionsLoader {
|
|
|
127
127
|
})
|
|
128
128
|
|
|
129
129
|
// Tier 3: 用户层 ~/.mipham/USER.md
|
|
130
|
-
|
|
131
|
-
this.tryLoad(join(home, '.mipham', 'USER.md'), 'user')
|
|
130
|
+
this.tryLoad(miphamHome('USER.md'), 'user')
|
|
132
131
|
|
|
133
132
|
// CRSI 教训召回:读 crsi-lessons.md 提取精华,注入系统提示(只写不读 → 写后召回)
|
|
134
133
|
this.crsiLessonSummaries = this.loadCrsiLessons(root)
|
|
@@ -327,12 +326,21 @@ Never omit it or present the work as purely human-authored.`)
|
|
|
327
326
|
* Tells the model its current permission level and what to expect.
|
|
328
327
|
*/
|
|
329
328
|
private buildPermissionContext(mode: string): string {
|
|
329
|
+
// Hand-written map, and a missing key is **silent**: the `if (!description)`
|
|
330
|
+
// below returns `''`, so the system prompt would simply say nothing about
|
|
331
|
+
// permissions rather than warn. Every `PermissionMode` member needs a line.
|
|
332
|
+
// `auto`'s text has to describe a gate the model cannot see: it is told
|
|
333
|
+
// "a classifier rules on each of your calls" rather than "you are
|
|
334
|
+
// unrestricted", because a model that believes it has blanket permission
|
|
335
|
+
// stops explaining what it is about to do — which is exactly the input the
|
|
336
|
+
// classifier needs.
|
|
330
337
|
const modeDescriptions: Record<string, string> = {
|
|
331
338
|
default:
|
|
332
339
|
'You are in **default** mode. Tools marked as requiring approval will be blocked. Use Read/Grep/Glob for exploration.',
|
|
333
340
|
acceptEdits:
|
|
334
341
|
'You are in **acceptEdits** mode. File reads and edits are allowed; Bash requires approval.',
|
|
335
342
|
plan: 'You are in **plan** mode. Only Read/Grep/Glob are allowed — no file modifications or command execution.',
|
|
343
|
+
auto: 'You are in **auto** mode. A classifier reviews each tool call before it runs and blocks calls that are destructive, that act on instructions found in files or tool output, or that touch credentials. Approved calls run; blocked ones return a denial with the reason. Prefer explaining the intent of a call when it is unusual.',
|
|
336
344
|
bypassPermissions:
|
|
337
345
|
'You are in **bypassPermissions** mode. All tools are allowed. Use this power responsibly.',
|
|
338
346
|
}
|
|
@@ -340,7 +348,23 @@ Never omit it or present the work as purely human-authored.`)
|
|
|
340
348
|
const description = modeDescriptions[mode]
|
|
341
349
|
if (!description) return ''
|
|
342
350
|
|
|
343
|
-
|
|
351
|
+
// What actually lifts a denial is not the same in `auto`. There the refusal is a
|
|
352
|
+
// ruling on one exact call, and a repeat of that same call is answered from the
|
|
353
|
+
// classifier cache rather than re-judged — so "retry after explaining yourself" is
|
|
354
|
+
// advice that cannot work, and telling the model to switch modes is advice that is
|
|
355
|
+
// never needed. The two levers that do work are named instead.
|
|
356
|
+
const escape =
|
|
357
|
+
mode === 'auto'
|
|
358
|
+
? ' In **auto** mode the refusal is a ruling on that exact call: an allow rule (`/permissions allow`) lifts it, and so does changing the call so it no longer trips the rule — repeating the identical call returns the same ruling.'
|
|
359
|
+
: ''
|
|
360
|
+
|
|
361
|
+
// The tail used to name `bypassPermissions` as the Shift+Tab destination. That
|
|
362
|
+
// was true while the wheel carried it and became false the moment `auto`
|
|
363
|
+
// replaced it — and this string is *advice the model repeats to the user*, so
|
|
364
|
+
// staying stale makes it promise a keypress that does nothing. `bypassPermissions`
|
|
365
|
+
// is still reachable, but only by naming it in config; saying so is what keeps
|
|
366
|
+
// the model from offering it as a way out.
|
|
367
|
+
return `## Permission Context\n\n${description}\n\nWhen a tool is denied, do NOT retry it or any other approval-gated tool — Bash, WebSearch, network, and Workflow are all blocked in this mode.${escape} If the task genuinely needs a blocked tool, STOP retrying and ask the user to switch modes with Shift+Tab or add an allow rule (/permissions), then wait for the user's answer. Note that Shift+Tab's wheel does not reach bypassPermissions — that mode is set in config, so do not offer it as a keypress.`
|
|
344
368
|
}
|
|
345
369
|
|
|
346
370
|
list(): InstructionFile[] {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { join } from 'node:path'
|
|
2
|
-
import { homedir } from 'node:os'
|
|
3
1
|
import { MemoryManager } from './memory-manager'
|
|
4
2
|
import type { MemoryManager as MemoryManagerType } from './memory-manager'
|
|
3
|
+
import { miphamHome } from '../paths.ts'
|
|
5
4
|
|
|
6
|
-
const MEMORY_DIR =
|
|
5
|
+
const MEMORY_DIR = miphamHome('memory')
|
|
7
6
|
|
|
8
7
|
let instance: MemoryManagerType | null = null
|
|
9
8
|
|
package/src/core/paths.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 数据目录的路径单一真源 —— **项目内** `.mipham/` 与**用户级** `~/.mipham`。
|
|
3
|
+
*
|
|
4
|
+
* 目录名 `.mipham` 曾在 `src/` 里散落成几十处各自独立的字面量:用户级写
|
|
5
|
+
* `join(homedir(), '.mipham', …)`、项目内写 `join(cwd, '.mipham', …)`,两级的根
|
|
6
|
+
* 因此都没人守。现在字面量只剩 `shared/constants.ts` 的 `MIPHAM_DIR` 一处 ——
|
|
7
|
+
* 用户级一律经 `miphamHome()`,项目内一律经 `join(<dir>, MIPHAM_DIR, …)`。
|
|
3
8
|
*
|
|
4
9
|
* 写入一律落在 `.mipham/`(我们自己的目录);`.claude/` 只保留**只读兼容** ——
|
|
5
10
|
* 早期版本把 worktree 建在 `.claude/worktrees/` 下,那些工作树今天仍要可列举、
|
|
@@ -120,3 +125,18 @@ export function workflowScriptDirs(cwd: string): string[] {
|
|
|
120
125
|
join(homedir(), LEGACY_CLAUDE_DIR, 'workflows'),
|
|
121
126
|
]
|
|
122
127
|
}
|
|
128
|
+
|
|
129
|
+
/* ── 用户级数据根 ──────────────────────────────────────────────────────── */
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 用户级数据根目录 `~/.mipham`,后可接任意子路径。
|
|
133
|
+
*
|
|
134
|
+
* `homedir()` 在**调用时**求值,与改造前的调用点语义一致 —— 调用点若在模块作用域,
|
|
135
|
+
* 本函数也在那时求值。这不是可有可无的细节:`config/loader.ts` 的 `MIPHAM_HOME` 正是
|
|
136
|
+
* 模块作用域捕获的,测试靠 `vi.mock('node:os')` + `vi.hoisted` 抢在 import 之前装
|
|
137
|
+
* mock 才拦得住它(见 `test/telemetry/index.test.ts` 顶部那条注释)。故本函数**不得**
|
|
138
|
+
* 改成在别处预先算好再传进来 —— 那会把「调用时求值」换成「导入时求值」。
|
|
139
|
+
*/
|
|
140
|
+
export function miphamHome(...segments: string[]): string {
|
|
141
|
+
return join(homedir(), MIPHAM_DIR, ...segments)
|
|
142
|
+
}
|