@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.
- 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 +121 -4
- package/src/core/improvement-track.ts +35 -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/task-performance.ts +22 -1
- 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 +3 -0
- package/src/i18n-core/locales/zh-CN.json +3 -0
- 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 +2 -3
- 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 +41 -19
- package/src/ui/config-wizard.tsx +2 -2
- package/src/ui/input.tsx +15 -3
- package/src/workflow/journal.ts +2 -2
|
@@ -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,15 +96,29 @@ 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')
|
|
100
|
+
|
|
101
|
+
/** 落盘的契约粒度投影 —— 只要 id/passed/role(EvalResult 的 description/detail 不落盘)。 */
|
|
102
|
+
export interface ContractResultRecord {
|
|
103
|
+
id: string
|
|
104
|
+
passed: boolean
|
|
105
|
+
role?: ContractRole
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 一次评估的契约粒度快照:契约 id → 是否通过。 */
|
|
109
|
+
export type ContractSnapshot = Record<string, boolean>
|
|
110
|
+
|
|
111
|
+
function toContractResultRecord(r: EvalResult): ContractResultRecord {
|
|
112
|
+
return { id: r.id, passed: r.passed, ...(r.role ? { role: r.role } : {}) }
|
|
113
|
+
}
|
|
99
114
|
|
|
100
115
|
/** 追加一次评估分数到 rewards 日志(按奖励函数名键控)。 */
|
|
101
116
|
export function appendEvalScore(
|
|
102
117
|
name: string,
|
|
103
|
-
report: { score: number; passed: number; total: number },
|
|
118
|
+
report: { score: number; passed: number; total: number; results?: EvalResult[] },
|
|
104
119
|
): void {
|
|
105
120
|
try {
|
|
106
|
-
mkdirSync(
|
|
121
|
+
mkdirSync(miphamHome('crsi'), { recursive: true })
|
|
107
122
|
appendFileSync(
|
|
108
123
|
SCORES_FILE,
|
|
109
124
|
JSON.stringify({
|
|
@@ -112,6 +127,9 @@ export function appendEvalScore(
|
|
|
112
127
|
score: report.score,
|
|
113
128
|
passed: report.passed,
|
|
114
129
|
total: report.total,
|
|
130
|
+
// 契约粒度(B1)。缺省不写该键:B1 之前落盘的旧记录没有它,
|
|
131
|
+
// 读取侧跳过 —— 这是向后兼容的承重判据,别改成 `results: []`。
|
|
132
|
+
...(report.results ? { results: report.results.map(toContractResultRecord) } : {}),
|
|
115
133
|
}) + '\n',
|
|
116
134
|
'utf-8',
|
|
117
135
|
)
|
|
@@ -135,6 +153,105 @@ export function getLastEvalScore(name: string): number | null {
|
|
|
135
153
|
}
|
|
136
154
|
}
|
|
137
155
|
|
|
156
|
+
/**
|
|
157
|
+
* 某奖励函数最近 n 次**按契约粒度**落盘的记录,新→旧。
|
|
158
|
+
*
|
|
159
|
+
* 只认带 `results` 的记录 —— B1 之前落盘的旧记录(只有聚合分数)被跳过,
|
|
160
|
+
* 于是调用方不必区分新旧形态。逐行容错:坏行跳过而不是让整条历史归零
|
|
161
|
+
* (`fixCache` 负责清理坏行)。
|
|
162
|
+
*/
|
|
163
|
+
export function getContractHistory(name: string, n = 3): ContractSnapshot[] {
|
|
164
|
+
try {
|
|
165
|
+
if (!existsSync(SCORES_FILE)) return []
|
|
166
|
+
const lines = readFileSync(SCORES_FILE, 'utf-8').trim().split('\n').filter(Boolean)
|
|
167
|
+
const out: ContractSnapshot[] = []
|
|
168
|
+
for (let i = lines.length - 1; i >= 0 && out.length < n; i--) {
|
|
169
|
+
let rec: { name?: string; results?: ContractResultRecord[] }
|
|
170
|
+
try {
|
|
171
|
+
rec = JSON.parse(lines[i]!) as { name?: string; results?: ContractResultRecord[] }
|
|
172
|
+
} catch {
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
if (rec.name !== name || !Array.isArray(rec.results)) continue
|
|
176
|
+
const snap: ContractSnapshot = {}
|
|
177
|
+
for (const r of rec.results) {
|
|
178
|
+
if (typeof r?.id === 'string') snap[r.id] = r.passed === true
|
|
179
|
+
}
|
|
180
|
+
out.push(snap)
|
|
181
|
+
}
|
|
182
|
+
return out
|
|
183
|
+
} catch {
|
|
184
|
+
return []
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export type ContractDelta = 'regressed' | 'fixed' | 'flaky' | 'new' | 'gone'
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 纯函数:当前 run vs 历史 → 每条契约的变化。**只报变化**,未变化的契约不出现。
|
|
192
|
+
*
|
|
193
|
+
* delta 判据(`history` 新→旧):
|
|
194
|
+
* - 历史上没出现过 → `new`
|
|
195
|
+
* - 历史上 true/false 都出现过 → `flaky`(压过 regressed/fixed:抖动的契约
|
|
196
|
+
* 不该被报成「已修复」或「回归」)
|
|
197
|
+
* - 上次与本次相反 → `regressed`(上次 PASS→本次 FAIL)/ `fixed`
|
|
198
|
+
* - 本次没有但历史有 → `gone`
|
|
199
|
+
*
|
|
200
|
+
* 为什么 `flaky` 要压过相邻两次的比较:只比相邻两次会把一个每次都在翻的契约
|
|
201
|
+
* 误报成「真回归」,而那正是这个账本要区分开的东西。
|
|
202
|
+
*/
|
|
203
|
+
export function diffContractHistory(
|
|
204
|
+
current: ContractResultRecord[],
|
|
205
|
+
history: ContractSnapshot[],
|
|
206
|
+
): { id: string; delta: ContractDelta; role?: ContractRole }[] {
|
|
207
|
+
const out: { id: string; delta: ContractDelta; role?: ContractRole }[] = []
|
|
208
|
+
const seen = new Set<string>()
|
|
209
|
+
for (const c of current) {
|
|
210
|
+
seen.add(c.id)
|
|
211
|
+
const past = history.filter((h) => c.id in h).map((h) => h[c.id] === true)
|
|
212
|
+
let delta: ContractDelta
|
|
213
|
+
if (past.length === 0) delta = 'new'
|
|
214
|
+
else if (past.includes(true) && past.includes(false)) delta = 'flaky'
|
|
215
|
+
else if (past[0] === !c.passed) delta = c.passed ? 'fixed' : 'regressed'
|
|
216
|
+
else continue // 未变化
|
|
217
|
+
out.push({ id: c.id, delta, ...(c.role ? { role: c.role } : {}) })
|
|
218
|
+
}
|
|
219
|
+
for (const h of history) {
|
|
220
|
+
for (const id of Object.keys(h)) {
|
|
221
|
+
if (seen.has(id)) continue
|
|
222
|
+
seen.add(id)
|
|
223
|
+
out.push({ id, delta: 'gone' })
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return out
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 把 diffContractHistory 的结果渲染成展示行。纯函数——不做 I/O、不读时钟。
|
|
231
|
+
* 返回空数组表示「无变化」,调用方据此决定是否打印标题。
|
|
232
|
+
*/
|
|
233
|
+
export function renderContractDiff(
|
|
234
|
+
deltas: { id: string; delta: ContractDelta; role?: ContractRole }[],
|
|
235
|
+
): string[] {
|
|
236
|
+
const text: Record<ContractDelta, string> = {
|
|
237
|
+
regressed: '上次 PASS,本次 FAIL(回归)',
|
|
238
|
+
fixed: '上次 FAIL,本次 PASS(已修复)',
|
|
239
|
+
flaky: '近几次结果不一致(抖动)',
|
|
240
|
+
new: '本次新增的契约',
|
|
241
|
+
gone: '本次未出现(已移出契约集)',
|
|
242
|
+
}
|
|
243
|
+
const icon: Record<ContractDelta, string> = {
|
|
244
|
+
regressed: '❌',
|
|
245
|
+
fixed: '✅',
|
|
246
|
+
flaky: '⚠️',
|
|
247
|
+
new: '🆕',
|
|
248
|
+
gone: '➖',
|
|
249
|
+
}
|
|
250
|
+
return deltas.map(
|
|
251
|
+
(d) => `${icon[d.delta]} ${d.id} ← ${text[d.delta]}${d.role ? ` \`${d.role}\`` : ''}`,
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
138
255
|
// ── Harness ──
|
|
139
256
|
|
|
140
257
|
/** 构建隔离组件,避免读用户 ~/.mipham 运行时状态。 */
|
|
@@ -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
|
|
|
@@ -26,6 +25,13 @@ export interface ImprovementReport {
|
|
|
26
25
|
predictedDelta?: number
|
|
27
26
|
/** 预测是否命中。与 predictedDelta 同时出现、同时缺席(JSON 序列化会丢掉 undefined 键)。 */
|
|
28
27
|
predictionHit?: boolean
|
|
28
|
+
/**
|
|
29
|
+
* B2 代价维:与分数数组逐项对齐的前/后耗时。**只记录,不进任何门禁** ——
|
|
30
|
+
* `verdict` / `deltaMean` / `minEffect` 一律不看这两个字段。
|
|
31
|
+
* 缺席(而非空数组)= 该记录早于代价维落地,或该样本未记代价。
|
|
32
|
+
*/
|
|
33
|
+
baselineDurations?: number[]
|
|
34
|
+
postDurations?: number[]
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
export interface ImprovementRecord extends ImprovementReport {
|
|
@@ -83,6 +89,11 @@ export function buildImprovementReport(
|
|
|
83
89
|
...(predicted !== undefined
|
|
84
90
|
? { predictedDelta: predicted, predictionHit: predictionHit(predicted, deltaMean) }
|
|
85
91
|
: {}),
|
|
92
|
+
// 代价维:**两条同生同灭**,与上面 ε 那条同理 —— 只写一半(有 baselineDurations
|
|
93
|
+
// 而无 postDurations)会让「代价」这件事在记录里既非有也非无,读侧无从判断。
|
|
94
|
+
...(sample.baselineDurations !== undefined && sample.postDurations !== undefined
|
|
95
|
+
? { baselineDurations: sample.baselineDurations, postDurations: sample.postDurations }
|
|
96
|
+
: {}),
|
|
86
97
|
}
|
|
87
98
|
}
|
|
88
99
|
|
|
@@ -148,15 +159,33 @@ export function predictionHitRate(records: ImprovementRecord[]): {
|
|
|
148
159
|
return { total, hits, rate: total === 0 ? 0 : hits / total, lo, hi }
|
|
149
160
|
}
|
|
150
161
|
|
|
162
|
+
/**
|
|
163
|
+
* 代价维的只读展示(B2):前/后均值一行。两个耗时数组缺席 → null,调用方据此整行不打印。
|
|
164
|
+
*
|
|
165
|
+
* **刻意不下结论**:倍数只是描述,不是判据。把「代价过高」变成 verdict 的一部分需要
|
|
166
|
+
* 样本量支撑,而当前 k 默认 3、`minEffect` 已经要 `max(20, 2×噪声)` —— 再塞一个维度
|
|
167
|
+
* 只会把统计问题变得更糟。所以这里只回答「花了多少」,不回答「值不值」。
|
|
168
|
+
*
|
|
169
|
+
* 基线均值为 0 时**不给倍数**:那个比值是 ∞(或 0/0 的 NaN),打出来是假读数。
|
|
170
|
+
*/
|
|
171
|
+
export function formatCostLine(report: ImprovementReport): string | null {
|
|
172
|
+
const { baselineDurations: before_, postDurations: after_ } = report
|
|
173
|
+
if (before_ === undefined || after_ === undefined) return null
|
|
174
|
+
const before = Math.round(mean(before_))
|
|
175
|
+
const after = Math.round(mean(after_))
|
|
176
|
+
const line = `⏱️ 代价: 均值 ${before}ms → ${after}ms`
|
|
177
|
+
return before > 0 ? `${line}(×${(after / before).toFixed(1)})` : line
|
|
178
|
+
}
|
|
179
|
+
|
|
151
180
|
// ── 台账 ──
|
|
152
181
|
|
|
153
182
|
export function improvementPath(): string {
|
|
154
|
-
return
|
|
183
|
+
return miphamHome('crsi', 'improvements.jsonl')
|
|
155
184
|
}
|
|
156
185
|
|
|
157
186
|
export function appendImprovement(record: ImprovementRecord): void {
|
|
158
187
|
const file = improvementPath()
|
|
159
|
-
mkdirSync(
|
|
188
|
+
mkdirSync(miphamHome('crsi'), { recursive: true })
|
|
160
189
|
// 原子激活(④):整账本读-改-写 + temp 文件 rename(见 shared/atomic-write),读者要么见旧要么见新。
|
|
161
190
|
// 非原子的 appendFileSync 写中途崩溃会留撕裂行,readImprovements 会 JSON.parse 抛错。
|
|
162
191
|
const existing = readImprovements()
|
|
@@ -184,7 +213,7 @@ export function readImprovements(): ImprovementRecord[] {
|
|
|
184
213
|
// (temp+rename,见 shared/atomic-write),替代易失内存变量(进程重启即丢、无 manifest)。
|
|
185
214
|
|
|
186
215
|
export function pendingVerdictPath(): string {
|
|
187
|
-
return
|
|
216
|
+
return miphamHome('crsi', 'pending-verdict.json')
|
|
188
217
|
}
|
|
189
218
|
|
|
190
219
|
export function setPendingVerdict(v: ImprovementVerdict | null): void {
|
|
@@ -193,7 +222,7 @@ export function setPendingVerdict(v: ImprovementVerdict | null): void {
|
|
|
193
222
|
rmSync(file, { force: true }) // 原子清除(unlink 原子)
|
|
194
223
|
return
|
|
195
224
|
}
|
|
196
|
-
mkdirSync(
|
|
225
|
+
mkdirSync(miphamHome('crsi'), { recursive: true })
|
|
197
226
|
atomicWriteFileSync(file, JSON.stringify({ verdict: v, timestamp: new Date().toISOString() }))
|
|
198
227
|
}
|
|
199
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
|
+
}
|