@miphamai/cli 0.22.0 → 0.24.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 (38) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/skills/workflows/audit.js +66 -18
  4. package/src/agent/agent-context.ts +20 -57
  5. package/src/agent/agent-experience.ts +8 -0
  6. package/src/agent/cross-session/discovery.ts +99 -0
  7. package/src/agent/cross-session/file-inbox.ts +118 -0
  8. package/src/agent/cross-session/transport.ts +17 -0
  9. package/src/agent/effectiveness-tracker.ts +149 -0
  10. package/src/agent/experience-rules.ts +188 -0
  11. package/src/agent/message-router.ts +83 -0
  12. package/src/agent/pattern-analyzer.ts +154 -0
  13. package/src/agent/sub-agent.ts +67 -0
  14. package/src/agent/types.ts +2 -0
  15. package/src/config/defaults.ts +12 -0
  16. package/src/config/loader.ts +33 -0
  17. package/src/core/credential-masker/env-filter.ts +39 -0
  18. package/src/core/credential-masker/index.ts +35 -0
  19. package/src/core/credential-masker/matcher.ts +106 -0
  20. package/src/core/credential-masker/output-scrub.ts +27 -0
  21. package/src/core/credential-masker/pipeline.ts +67 -0
  22. package/src/core/credential-masker/strategies/aws.ts +50 -0
  23. package/src/core/credential-masker/strategies/extract.ts +106 -0
  24. package/src/core/credential-masker/strategies/full.ts +15 -0
  25. package/src/core/credential-masker/strategies/jwt.ts +75 -0
  26. package/src/core/credential-masker/types.ts +16 -0
  27. package/src/core/credential-masker.ts +12 -187
  28. package/src/core/engine.ts +114 -10
  29. package/src/core/rule-engine.ts +179 -0
  30. package/src/core/session-store.ts +15 -2
  31. package/src/index.tsx +47 -2
  32. package/src/shared/types.ts +69 -5
  33. package/src/tools/agent/agent.ts +7 -1
  34. package/src/tools/agent/list-agents.ts +67 -0
  35. package/src/tools/agent/send-message.ts +21 -24
  36. package/src/tools/exec/bash.ts +67 -3
  37. package/src/tools/index.ts +2 -0
  38. package/src/ui/commands.ts +133 -0
@@ -3,6 +3,7 @@ import type {
3
3
  ToolDefinition,
4
4
  ToolResult,
5
5
  InferenceHookConfig,
6
+ CrsiConfig,
6
7
  } from '../shared/index.ts'
7
8
  import { ProviderRegistry } from '../providers/registry'
8
9
  import { ContextManager } from './context'
@@ -16,8 +17,13 @@ import type { AgentViewManager } from '../agent-view/agent-view-manager'
16
17
  import type { SkillsLoader } from '../skills/loader'
17
18
  import { getBackgroundAgentRegistry } from '../agent/background-registry'
18
19
  import { RulesLoader } from './rules-loader'
20
+ import { ExperienceRuleEngine } from './rule-engine.js'
21
+ import { PatternAnalyzer } from '../agent/pattern-analyzer.js'
22
+ import { EffectivenessTracker } from '../agent/effectiveness-tracker.js'
19
23
  import { UsageTracker } from './usage-tracker'
20
24
  import { buildRequest, sendInferenceCheck, isInferenceHookEnabled } from './inference-hook'
25
+ import { getFileInboxTransport } from '../agent/cross-session/file-inbox'
26
+ import { getMessageBus } from '../agent/message-bus'
21
27
  import { createT } from '../i18n-core/t'
22
28
  import enUS from '../i18n-core/locales/en-US.json'
23
29
  import zhCN from '../i18n-core/locales/zh-CN.json'
@@ -35,6 +41,9 @@ export class QueryEngine {
35
41
  private agentRegistry?: AgentRegistry
36
42
  private agentViewManager?: AgentViewManager
37
43
  private skillsLoader?: SkillsLoader
44
+ private ruleEngine?: ExperienceRuleEngine
45
+ private _patternAnalyzer?: PatternAnalyzer
46
+ private _effectivenessTracker?: EffectivenessTracker
38
47
  private goal?: string
39
48
  private maxGoalLoops = 20
40
49
  private lastAssistantContent?: string
@@ -52,7 +61,40 @@ export class QueryEngine {
52
61
  private context: ContextManager,
53
62
  private tools: Map<string, ToolDefinition>,
54
63
  private permission: PermissionSystem = new PermissionSystem('default'),
55
- ) {}
64
+ ruleEngine?: ExperienceRuleEngine,
65
+ ) {
66
+ this.ruleEngine = ruleEngine
67
+ }
68
+
69
+ /** Session identifier for cross-session messaging. Set by the app startup via setSessionId(). */
70
+ private sessionId: string = 'session-1'
71
+
72
+ /** Set the session identifier (called from index.tsx at startup with the actual session name). */
73
+ setSessionId(id: string): void {
74
+ this.sessionId = id
75
+ }
76
+
77
+ /** Get the current session identifier. */
78
+ getSessionId(): string {
79
+ return this.sessionId
80
+ }
81
+
82
+ /** Poll the cross-session file inbox for messages addressed to this session. */
83
+ async pollCrossSessionInbox(): Promise<void> {
84
+ try {
85
+ const transport = getFileInboxTransport()
86
+ const messages = await transport.poll(this.sessionId)
87
+ if (messages.length > 0) {
88
+ const bus = getMessageBus()
89
+ for (const msg of messages) {
90
+ bus.post(msg.from, msg.to, msg.summary, msg.message, msg.type)
91
+ }
92
+ }
93
+ } catch (err) {
94
+ // Best-effort: don't let a failed inbox poll break the turn
95
+ console.warn('Cross-session inbox poll failed:', err)
96
+ }
97
+ }
56
98
 
57
99
  /** Register a hook engine for pre/post tool-use lifecycle events. */
58
100
  setHookEngine(hooks: HookEngine): void {
@@ -96,6 +138,8 @@ export class QueryEngine {
96
138
  private usageTracker = new UsageTracker()
97
139
  /** Inference hook (DLP) configuration. */
98
140
  private inferenceHookConfig?: InferenceHookConfig
141
+ /** CRSI feature flags. */
142
+ private crsiConfig?: Partial<CrsiConfig>
99
143
 
100
144
  /** Register the rules loader. */
101
145
  setRulesLoader(loader: RulesLoader): void {
@@ -108,6 +152,11 @@ export class QueryEngine {
108
152
  this.inferenceHookConfig = config
109
153
  }
110
154
 
155
+ /** Register CRSI feature flags. */
156
+ setCrsiConfig(config: Partial<CrsiConfig> | undefined): void {
157
+ this.crsiConfig = config
158
+ }
159
+
111
160
  /** Pending task notifications from background agents (cleared after draining). */
112
161
  private pendingTaskNotifications: Array<StreamChunk> = []
113
162
 
@@ -255,9 +304,12 @@ export class QueryEngine {
255
304
  }
256
305
 
257
306
  async *process(userInput: string, signal?: AbortSignal): AsyncGenerator<StreamChunk> {
307
+ // Poll for cross-session messages before each turn
308
+ await this.pollCrossSessionInbox()
309
+
258
310
  // Fire UserPromptSubmit hooks before processing
259
311
  if (this.hookEngine) {
260
- const submitResult = await this.hookEngine.executeUserPromptSubmit(userInput, 'session-1')
312
+ const submitResult = await this.hookEngine.executeUserPromptSubmit(userInput, this.sessionId)
261
313
  if (submitResult.additionalContext) {
262
314
  this.context.addMessage({
263
315
  role: 'user',
@@ -292,7 +344,7 @@ export class QueryEngine {
292
344
  const model = this.registry.getActiveModel()
293
345
  const request = buildRequest(
294
346
  messages,
295
- 'session-1',
347
+ this.sessionId,
296
348
  provider,
297
349
  model,
298
350
  hookConfig.organization_id,
@@ -587,7 +639,7 @@ export class QueryEngine {
587
639
  const model = this.registry.getActiveModel()
588
640
  const request = buildRequest(
589
641
  messages,
590
- 'session-1',
642
+ this.sessionId,
591
643
  provider,
592
644
  model,
593
645
  hookConfig.organization_id,
@@ -761,8 +813,9 @@ export class QueryEngine {
761
813
 
762
814
  // Run PreToolUse hooks
763
815
  let effectiveParams = params
816
+ let hookWarnings: string[] = []
764
817
  if (this.hookEngine) {
765
- const preResult = await this.hookEngine.executePreToolUse(name, params, 'session-1')
818
+ const preResult = await this.hookEngine.executePreToolUse(name, params, this.sessionId)
766
819
  if (!preResult.allowed) {
767
820
  return {
768
821
  success: false,
@@ -775,10 +828,21 @@ export class QueryEngine {
775
828
  }
776
829
  }
777
830
 
831
+ // CRSI RuleEngine intercept — gated by crsi.preToolHook feature flag
832
+ if (this.crsiConfig?.preToolHook !== false && this.ruleEngine) {
833
+ const ruleResult = this.ruleEngine.intercept(name, effectiveParams)
834
+ if (Object.keys(ruleResult.modified).length > 0) {
835
+ effectiveParams = ruleResult.modified
836
+ }
837
+ if (ruleResult.warnings.length > 0) {
838
+ hookWarnings = ruleResult.warnings
839
+ }
840
+ }
841
+
778
842
  try {
779
843
  const result = await tool.execute(effectiveParams, {
780
844
  cwd: process.cwd(),
781
- sessionId: 'session-1',
845
+ sessionId: this.sessionId,
782
846
  provider: this.registry.getActive().config.id,
783
847
  model: this.registry.getActiveModel(),
784
848
  skillsLoader: this.skillsLoader,
@@ -788,14 +852,33 @@ export class QueryEngine {
788
852
  agentRegistry: this.agentRegistry,
789
853
  backgroundAgentRegistry: getBackgroundAgentRegistry(),
790
854
  permissionSystem: this.permission,
855
+ ruleEngine: this.ruleEngine,
791
856
  })
792
857
 
793
858
  // Track touched files for rules matching
794
859
  this.trackTouchedFile(name, effectiveParams)
795
860
 
861
+ // Prepend CRSI warnings to result content
862
+ if (hookWarnings.length > 0 && result.success) {
863
+ result.content = hookWarnings.join('\n') + '\n' + (result.content || '')
864
+ }
865
+
796
866
  // Run PostToolUse hooks
797
867
  if (this.hookEngine) {
798
- await this.hookEngine.executePostToolUse(name, effectiveParams, result, 'session-1')
868
+ await this.hookEngine.executePostToolUse(name, effectiveParams, result, this.sessionId)
869
+ }
870
+
871
+ // CRSI: Track rule effectiveness after tool execution
872
+ if (hookWarnings.length > 0) {
873
+ const tracker = this.getEffectivenessTracker()
874
+ for (const warning of hookWarnings) {
875
+ const match = warning.match(/^\[rule:([^\]]+)\]/)
876
+ const ruleId = match?.[1]
877
+ if (ruleId) {
878
+ tracker.recordApplication(ruleId, result.success)
879
+ }
880
+ }
881
+ tracker.persist()
799
882
  }
800
883
 
801
884
  return result
@@ -829,6 +912,27 @@ export class QueryEngine {
829
912
  return this.tools
830
913
  }
831
914
 
915
+ getRuleEngine(): ExperienceRuleEngine | undefined {
916
+ return this.ruleEngine
917
+ }
918
+
919
+ /** Lazily-initialized PatternAnalyzer singleton. */
920
+ getPatternAnalyzer(): PatternAnalyzer {
921
+ if (!this._patternAnalyzer) {
922
+ this._patternAnalyzer = new PatternAnalyzer()
923
+ }
924
+ return this._patternAnalyzer
925
+ }
926
+
927
+ /** Lazily-initialized EffectivenessTracker singleton. */
928
+ getEffectivenessTracker(): EffectivenessTracker {
929
+ if (!this._effectivenessTracker) {
930
+ this._effectivenessTracker = new EffectivenessTracker()
931
+ this._effectivenessTracker.load()
932
+ }
933
+ return this._effectivenessTracker
934
+ }
935
+
832
936
  /** Register a tool dynamically (used by MCP auto-registration). */
833
937
  registerTool(tool: ToolDefinition): void {
834
938
  if (this.tools.has(tool.name)) {
@@ -867,7 +971,7 @@ export class QueryEngine {
867
971
  /** Wrap context compaction with PreCompact/PostCompact hooks. */
868
972
  private async compactWithHooks(heading: string): Promise<void> {
869
973
  if (this.hookEngine) {
870
- const preResult = await this.hookEngine.executePreCompact('session-1')
974
+ const preResult = await this.hookEngine.executePreCompact(this.sessionId)
871
975
  if (preResult.additionalContext) {
872
976
  this.context.addMessage({
873
977
  role: 'user',
@@ -879,7 +983,7 @@ export class QueryEngine {
879
983
  await this.context.compact(heading)
880
984
 
881
985
  if (this.hookEngine) {
882
- const postResult = await this.hookEngine.executePostCompact('session-1')
986
+ const postResult = await this.hookEngine.executePostCompact(this.sessionId)
883
987
  if (postResult.additionalContext) {
884
988
  this.context.addMessage({
885
989
  role: 'user',
@@ -893,7 +997,7 @@ export class QueryEngine {
893
997
  private async *checkStopHook(signal?: AbortSignal): AsyncGenerator<StreamChunk> {
894
998
  if (!this.hookEngine) return
895
999
 
896
- const stopResult = await this.hookEngine.executeStop('session-1')
1000
+ const stopResult = await this.hookEngine.executeStop(this.sessionId)
897
1001
  if (stopResult.decision === 'block') {
898
1002
  // Feed the block reason back to the AI and continue
899
1003
  this.context.addMessage({
@@ -0,0 +1,179 @@
1
+ import type { ExperienceRule } from '../agent/experience-rules.js'
2
+ import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs'
3
+ import { join, dirname } from 'node:path'
4
+
5
+ export interface ToolRule {
6
+ id: string
7
+ toolName: string
8
+ category: string
9
+ match: (params: Record<string, unknown>) => boolean
10
+ fix: (params: Record<string, unknown>) => {
11
+ modified: Record<string, unknown>
12
+ warning: string
13
+ }
14
+ source: 'builtin' | 'pattern-analyzer' | 'manual'
15
+ enabled: boolean
16
+ }
17
+
18
+ const BUILTIN_RULES: ToolRule[] = [
19
+ {
20
+ id: 'rule-timeout-bash-heavy',
21
+ toolName: 'Bash',
22
+ category: 'timeout',
23
+ match: (p: Record<string, unknown>) => {
24
+ const cmd = String(p.command ?? '')
25
+ const heavy = /npm (install|ci|test)|docker build|pnpm install|cargo build|brew install/.test(
26
+ cmd,
27
+ )
28
+ if (!heavy) return false
29
+ const timeout = (p as Record<string, unknown>).timeout as number | undefined
30
+ return !timeout || timeout < 300_000
31
+ },
32
+ fix: (p: Record<string, unknown>) => {
33
+ const prevTimeout = (p as Record<string, unknown>).timeout as number | undefined
34
+ return {
35
+ modified: { ...p, timeout: 300_000 },
36
+ warning: `⏱️ timeout 已从 ${prevTimeout || 'default'}ms 自动提升至 300000ms(该命令类型历史超时率 > 50%)`,
37
+ }
38
+ },
39
+ source: 'builtin',
40
+ enabled: true,
41
+ },
42
+ {
43
+ id: 'rule-git-force-protection',
44
+ toolName: 'Bash',
45
+ category: 'tool-params',
46
+ match: (p: Record<string, unknown>) => {
47
+ const cmd = String(p.command ?? '')
48
+ return /git (push|reset) .*--force/.test(cmd) && !p.dangerouslyDisableSandbox
49
+ },
50
+ fix: (p: Record<string, unknown>) => ({
51
+ modified: p,
52
+ warning: '⚠️ 检测到 git --force 操作。如需执行请设置 dangerouslyDisableSandbox: true',
53
+ }),
54
+ source: 'builtin',
55
+ enabled: true,
56
+ },
57
+ ]
58
+
59
+ export class ExperienceRuleEngine {
60
+ private rules: ToolRule[]
61
+ private storePath: string
62
+
63
+ constructor(storeDir: string = join(process.env.HOME || '~', '.mipham', 'rule-engine')) {
64
+ this.rules = [...BUILTIN_RULES.map((r) => ({ ...r }))]
65
+ this.storePath = join(storeDir, 'rules.json')
66
+ this.load()
67
+ }
68
+
69
+ register(rule: ToolRule): void {
70
+ // Replace if same ID exists, otherwise append
71
+ const idx = this.rules.findIndex((r) => r.id === rule.id)
72
+ if (idx !== -1) {
73
+ this.rules[idx] = rule
74
+ } else {
75
+ this.rules.push(rule)
76
+ }
77
+ this.persist()
78
+ }
79
+
80
+ intercept(
81
+ toolName: string,
82
+ params: Record<string, unknown>,
83
+ ): { modified: Record<string, unknown>; warnings: string[] } {
84
+ let modified = params
85
+ const warnings: string[] = []
86
+
87
+ for (const rule of this.rules) {
88
+ if (!rule.enabled) continue
89
+ if (rule.toolName !== toolName) continue
90
+
91
+ try {
92
+ if (rule.match(modified)) {
93
+ const result = rule.fix(modified)
94
+ modified = result.modified
95
+ if (result.warning) {
96
+ warnings.push(`[rule:${rule.id}] ${result.warning}`)
97
+ }
98
+ }
99
+ } catch {
100
+ // Rule match/fix failures never block execution
101
+ }
102
+ }
103
+
104
+ return { modified, warnings }
105
+ }
106
+
107
+ convertFromExperienceRules(experienceRules: ExperienceRule[]): ToolRule[] {
108
+ return experienceRules.map((er): ToolRule => {
109
+ // Map experience rule category to tool name
110
+ const toolNameMap: Record<string, string> = {
111
+ timeout: 'Bash',
112
+ 'tool-params': 'Bash',
113
+ import: 'Write',
114
+ search: 'Grep',
115
+ semantic: 'Bash',
116
+ }
117
+ const toolName = toolNameMap[er.category] || 'Bash'
118
+
119
+ return {
120
+ id: er.id,
121
+ toolName,
122
+ category: er.category,
123
+ match: (p: Record<string, unknown>): boolean => {
124
+ if (er.category === 'timeout') {
125
+ const cmd = String(p.command ?? '')
126
+ const heavy = /npm|docker|pnpm|cargo|brew|install|build/.test(cmd)
127
+ if (!heavy) return false
128
+ const timeout = (p as Record<string, unknown>).timeout as number | undefined
129
+ return !timeout || timeout < 300_000
130
+ }
131
+ // Default: always match for the given tool
132
+ return true
133
+ },
134
+ fix: (p: Record<string, unknown>) => ({
135
+ modified: { ...p, timeout: 300_000 },
136
+ warning: `⏱️ [auto-rule] ${er.action} (${er.evidence.failureCount} 次历史失败)`,
137
+ }),
138
+ source: 'pattern-analyzer',
139
+ enabled: true,
140
+ }
141
+ })
142
+ }
143
+
144
+ getActiveRules(): ToolRule[] {
145
+ return this.rules.filter((r) => r.enabled)
146
+ }
147
+
148
+ setRuleEnabled(id: string, enabled: boolean): void {
149
+ const rule = this.rules.find((r) => r.id === id)
150
+ if (rule) {
151
+ rule.enabled = enabled
152
+ this.persist()
153
+ }
154
+ }
155
+
156
+ /** Persist non-builtin rules to disk. */
157
+ persist(): void {
158
+ const nonBuiltin = this.rules.filter((r) => r.source !== 'builtin')
159
+ const dir = dirname(this.storePath)
160
+ mkdirSync(dir, { recursive: true })
161
+ writeFileSync(this.storePath, JSON.stringify(nonBuiltin, null, 2), 'utf-8')
162
+ }
163
+
164
+ /** Load persisted non-builtin rules from disk. Rejects rules whose IDs conflict with builtins. */
165
+ load(): void {
166
+ if (!existsSync(this.storePath)) return
167
+ try {
168
+ const raw = JSON.parse(readFileSync(this.storePath, 'utf-8')) as ToolRule[]
169
+ const builtinIds = new Set(BUILTIN_RULES.map((r) => r.id))
170
+ for (const rule of raw) {
171
+ // Reject if a builtin with the same ID exists (builtins always win)
172
+ if (builtinIds.has(rule.id)) continue
173
+ this.rules.push(rule)
174
+ }
175
+ } catch {
176
+ // Corrupt file — start fresh with only builtins
177
+ }
178
+ }
179
+ }
@@ -9,6 +9,7 @@ import {
9
9
  statSync,
10
10
  } from 'node:fs'
11
11
  import { join } from 'node:path'
12
+ import { createHash } from 'node:crypto'
12
13
  import type { Message } from '../shared/types'
13
14
 
14
15
  export interface SessionMetadata {
@@ -50,7 +51,15 @@ function ensureDir(): void {
50
51
 
51
52
  function filePath(name: string): string {
52
53
  // Sanitize name for filesystem
53
- const safe = name.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 100)
54
+ const safe = name.replace(/[^a-zA-Z0-9_-]/g, '_')
55
+
56
+ // For very long names (>100 chars), use SHA-256 hash suffix to prevent collision
57
+ if (safe.length > 100) {
58
+ const hash = createHash('sha256').update(safe).digest('hex').slice(0, 16)
59
+ const truncated = safe.slice(0, 80)
60
+ return join(SESSIONS_DIR, `${truncated}-${hash}.jsonl`)
61
+ }
62
+
54
63
  return join(SESSIONS_DIR, `${safe}.jsonl`)
55
64
  }
56
65
 
@@ -233,7 +242,11 @@ export class SessionStore {
233
242
  ensureDir()
234
243
  mkdirSync(SUMMARIES_DIR, { recursive: true })
235
244
 
236
- const safe = name.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 100)
245
+ const safeRaw = name.replace(/[^a-zA-Z0-9_-]/g, '_')
246
+ const safe =
247
+ safeRaw.length > 100
248
+ ? `${safeRaw.slice(0, 80)}-${createHash('sha256').update(safeRaw).digest('hex').slice(0, 16)}`
249
+ : safeRaw
237
250
  const summaryPath = join(SUMMARIES_DIR, `${safe}.md`)
238
251
  writeFileSync(summaryPath, `# ${name}\n\n${summary}\n\nTags: ${tags.join(', ')}\n`, 'utf-8')
239
252
 
package/src/index.tsx CHANGED
@@ -3,12 +3,24 @@ import { join } from 'node:path'
3
3
  import { existsSync } from 'node:fs'
4
4
  import { render } from 'ink'
5
5
  import { App } from './ui/app'
6
- import { loadConfig, loadInferenceHookConfig, loadCredentialMaskingConfig } from './config/loader'
6
+ import {
7
+ loadConfig,
8
+ loadInferenceHookConfig,
9
+ loadCredentialMaskingConfig,
10
+ loadCrossSessionConfig,
11
+ } from './config/loader'
12
+ import {
13
+ registerActiveSession,
14
+ heartbeatSession,
15
+ unregisterSession,
16
+ createSessionInfo,
17
+ } from './agent/cross-session/discovery'
7
18
  import { bootstrapProviders } from './providers/bootstrap'
8
19
  import { InstructionsLoader } from './core/instructions'
9
20
  import { loadSessionMemories, getMemoryManager } from './core/memory/memory-loader'
10
21
  import { ContextManager } from './core/context'
11
22
  import { QueryEngine } from './core/engine'
23
+ import { ExperienceRuleEngine } from './core/rule-engine.js'
12
24
  import { SessionStore } from './core/session-store'
13
25
  import type { PermissionLevel } from './shared/types'
14
26
  import { SkillsLoader } from './skills/loader'
@@ -236,7 +248,8 @@ export async function runApp(options: RunOptions): Promise<void> {
236
248
  const artifactServer = new ArtifactServer(artifactsDir, ARTIFACT_PORT)
237
249
 
238
250
  // Create query engine
239
- const engine = new QueryEngine(registry, context, tools)
251
+ const ruleEngine = new ExperienceRuleEngine()
252
+ const engine = new QueryEngine(registry, context, tools, undefined, ruleEngine)
240
253
  engine.setHookEngine(hookEngine)
241
254
  engine.setArtifactServer(artifactServer)
242
255
  engine.setAgentViewManager(agentViewManager)
@@ -246,6 +259,10 @@ export async function runApp(options: RunOptions): Promise<void> {
246
259
  const inferenceHookConfig = loadInferenceHookConfig()
247
260
  engine.setInferenceHookConfig(inferenceHookConfig)
248
261
 
262
+ // Initialize credential masking pipeline (strategies: Full, Extract, JWT)
263
+ const { initializePipeline } = await import('./core/credential-masker/index')
264
+ initializePipeline()
265
+
249
266
  // Wire credential masking configuration into tools
250
267
  const credentialMaskingConfig = loadCredentialMaskingConfig()
251
268
  const { setCredentialMaskingConfigForRead } = await import('./tools/file/read')
@@ -280,10 +297,36 @@ export async function runApp(options: RunOptions): Promise<void> {
280
297
 
281
298
  engine.setupContextSummarizer()
282
299
 
300
+ // ── Cross-session messaging: register session, heartbeat, shutdown ──
301
+ engine.setSessionId(sessionName)
302
+ const sessionInfo = createSessionInfo(
303
+ sessionName,
304
+ sessionName,
305
+ process.cwd(),
306
+ defaultProvider,
307
+ defaultModel,
308
+ )
309
+ registerActiveSession(sessionInfo)
310
+
311
+ // 30-second heartbeat to keep the session file mtime fresh
312
+ const heartbeatInterval = setInterval(() => {
313
+ heartbeatSession(sessionName)
314
+ }, 30_000)
315
+
316
+ // Allow heartbeat to not keep the process alive
317
+ if (heartbeatInterval.unref) {
318
+ heartbeatInterval.unref()
319
+ }
320
+
321
+ // Load cross-session config (available for MessageRouter and future consumers)
322
+ loadCrossSessionConfig()
323
+
283
324
  // Auto-save session on exit
284
325
  let saved = false
285
326
  const saveAndExit = () => {
286
327
  saved = true
328
+ clearInterval(heartbeatInterval)
329
+ unregisterSession(sessionName)
287
330
  artifactServer.stop()
288
331
  if (context.getMessageCount() > 0) {
289
332
  SessionStore.save(sessionName, context.getMessages(), {
@@ -300,6 +343,8 @@ export async function runApp(options: RunOptions): Promise<void> {
300
343
 
301
344
  // Safety net: auto-save on exit for paths that bypass saveAndExit
302
345
  process.on('exit', () => {
346
+ clearInterval(heartbeatInterval)
347
+ unregisterSession(sessionName)
303
348
  if (!saved && context.getMessageCount() > 0) {
304
349
  SessionStore.autoSave(context.getMessages(), {
305
350
  provider: defaultProvider,
@@ -72,6 +72,7 @@ export interface ToolContext {
72
72
  agentRegistry?: import('../agent/agent-registry').AgentRegistry
73
73
  backgroundAgentRegistry?: import('../agent/background-registry').BackgroundAgentRegistry
74
74
  permissionSystem?: import('../core/permission').PermissionSystem
75
+ ruleEngine?: import('../core/rule-engine').ExperienceRuleEngine
75
76
  }
76
77
 
77
78
  // ── Artifact Types ──
@@ -162,6 +163,8 @@ export interface MiphamConfig {
162
163
  }
163
164
  /** Phase 9 feature flags. All default to true. */
164
165
  features?: Partial<FeatureFlags>
166
+ /** Phase 10 CRSI feature flags. All default to true. */
167
+ crsi?: Partial<CrsiConfig>
165
168
  }
166
169
 
167
170
  export interface FeatureFlags {
@@ -169,6 +172,17 @@ export interface FeatureFlags {
169
172
  context: { useRealTokenizer: boolean; adaptiveThresholds: boolean }
170
173
  }
171
174
 
175
+ export interface CrsiConfig {
176
+ /** Extract and inject experience rules into agent system prompts. */
177
+ ruleInjection: boolean
178
+ /** Intercept tool calls and auto-fix known failure patterns before execution. */
179
+ preToolHook: boolean
180
+ /** Analyze agent outcomes for recurring failure patterns. */
181
+ autoPatternAnalysis: boolean
182
+ /** Auto-degrade/disable low-effectiveness rules based on success-rate tracking. */
183
+ autoRuleManagement: boolean
184
+ }
185
+
172
186
  export interface McpServerConfig {
173
187
  name: string
174
188
  command: string
@@ -375,17 +389,44 @@ export interface CredentialFullMaskRule {
375
389
  mode: 'full'
376
390
  }
377
391
 
392
+ /** Per-extract-pattern configuration with optional field-based extraction. */
393
+ export interface CredentialExtractPattern {
394
+ /** Regex pattern to match. When `field` is set, applied to field value only. */
395
+ pattern: string
396
+ /** Optional replacement string. Defaults to CREDENTIAL_SENTINEL. */
397
+ replacement?: string
398
+ /** 🆕 JSON key to extract before applying pattern. If unset, pattern runs against full content. */
399
+ field?: string
400
+ }
401
+
378
402
  /** Extract-based masking rule: only regex-matched tokens are replaced. */
379
403
  export interface CredentialExtractRule {
380
404
  path: string
381
405
  mode: 'extract'
382
- extract: Array<{
383
- pattern: string
384
- replacement?: string
385
- }>
406
+ extract: CredentialExtractPattern[]
407
+ /** 🆕 Behavior when no extract pattern matches: 'mask' (replace all) or 'passthrough' (keep). Default: 'mask'. */
408
+ onExtractNoMatch?: 'mask' | 'passthrough'
409
+ }
410
+
411
+ /** 🆕 JWT-aware masking rule: decode payload and mask specified claims. */
412
+ export interface JwtMaskingRule {
413
+ path: string
414
+ type: 'jwt'
415
+ decode: 'jwt'
416
+ /** Claim names to mask in the JWT payload (e.g. ["sub", "email"]). */
417
+ maskClaims: string[]
418
+ }
419
+
420
+ /** 🆕 AWS credential pair masking rule: detect key pairs and optionally re-sign. */
421
+ export interface AwsMaskingRule {
422
+ path: string
423
+ type: 'aws'
424
+ awsPairs: boolean
425
+ sigv4: boolean
386
426
  }
387
427
 
388
- export type CredentialFileRule = CredentialFullMaskRule | CredentialExtractRule
428
+ export type CredentialFileRule =
429
+ CredentialFullMaskRule | CredentialExtractRule | JwtMaskingRule | AwsMaskingRule
389
430
 
390
431
  /** Configuration for credential masking, loaded from config.yml. */
391
432
  export interface CredentialMaskingConfig {
@@ -401,6 +442,29 @@ export interface CredentialMaskingConfig {
401
442
  }
402
443
  }
403
444
 
445
+ // ── Cross-Session Messaging Types ──
446
+
447
+ /** Controls how inbound cross-session messages are handled. */
448
+ export type CrossSessionInbound = 'allow' | 'ask' | 'deny'
449
+
450
+ /** 🆕 Configuration for cross-session messaging. */
451
+ export interface CrossSessionConfig {
452
+ crossSessionInbound: CrossSessionInbound
453
+ dialogExpiry: number // seconds
454
+ }
455
+
456
+ /** Session information exposed via ListAgents. */
457
+ export interface SessionInfo {
458
+ id: string
459
+ name: string
460
+ machine: string
461
+ pid: number
462
+ startedAt: string
463
+ cwd?: string
464
+ provider?: string
465
+ model?: string
466
+ }
467
+
404
468
  // ── Background Agent Types ──
405
469
 
406
470
  export interface BackgroundAgentConfig {
@@ -60,7 +60,13 @@ export const agentTool: ToolDefinition = {
60
60
  const agentDef = ctx.agentRegistry?.resolve(agentType)
61
61
 
62
62
  try {
63
- const sub = new SubAgent(registry, toolRegistry, ctx.permissionSystem)
63
+ const sub = new SubAgent(
64
+ registry,
65
+ toolRegistry,
66
+ ctx.permissionSystem,
67
+ undefined,
68
+ ctx.ruleEngine,
69
+ )
64
70
  const result = await sub.execute(prompt, description, {
65
71
  type: agentType,
66
72
  agentDef,