@miphamai/cli 0.12.0 → 0.14.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -0,0 +1,51 @@
1
+ export const meta = {
2
+ name: 'audit',
3
+ description: 'Security audit: fan-out per file → verify → report',
4
+ phases: [
5
+ { title: 'Scope', detail: 'discover targets' },
6
+ { title: 'Audit', detail: 'one agent per target' },
7
+ { title: 'Verify', detail: 'adversarial verification' },
8
+ { title: 'Report', detail: 'synthesize findings' },
9
+ ],
10
+ }
11
+
12
+ phase('Scope')
13
+ const targets = args.targets || (await agent(
14
+ 'List all source files that need security auditing. Return { files: [{ path, reason }] }',
15
+ { schema: { type: 'object', properties: { files: { type: 'array', items: { type: 'object', properties: { path: { type: 'string' }, reason: { type: 'string' } }, required: ['path', 'reason'] } } }, required: ['files'] } },
16
+ )).files
17
+
18
+ log(`Auditing ${targets.length} files`)
19
+
20
+ phase('Audit')
21
+ const raw = (await pipeline(
22
+ targets,
23
+ t => agent(`Security audit ${t.path}: injection, auth, crypto, secrets, input validation. Return { findings: [{ severity, file, line, summary }] }`,
24
+ { label: `audit:${t.path}`, schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'object', properties: { severity: { type: 'string', enum: ['critical', 'high', 'medium', 'low'] }, file: { type: 'string' }, line: { type: 'number' }, summary: { type: 'string' } }, required: ['severity', 'file', 'summary'] } } }, required: ['findings'] } },
25
+ )),
26
+ )
27
+ const findings = raw.flatMap(r => (r && r.findings) || [])
28
+
29
+ log(`Found ${findings.length} potential issues`)
30
+
31
+ phase('Verify')
32
+ const verified = await parallel(
33
+ findings.map(f => () =>
34
+ verify(f, {
35
+ mode: 'adversarial',
36
+ skeptics: 3,
37
+ threshold: 2,
38
+ schema: { type: 'object', properties: { real: { type: 'boolean' }, reason: { type: 'string' } }, required: ['real', 'reason'] },
39
+ })
40
+ ),
41
+ )
42
+
43
+ const confirmed = verified.filter(Boolean).filter(v => v.survives).map(v => v.finding)
44
+ log(`${confirmed.length}/${findings.length} findings verified`)
45
+
46
+ phase('Report')
47
+ const report = await agent(
48
+ `Synthesize audit report from confirmed findings:\n${JSON.stringify(confirmed)}`,
49
+ { label: 'report' },
50
+ )
51
+ return report
@@ -0,0 +1,95 @@
1
+ export const meta = {
2
+ name: 'hunt',
3
+ description: 'Bug hunt: loopUntilConvergence + adversarial verify',
4
+ phases: [
5
+ { title: 'Hunt', detail: 'iterative discovery + verify' },
6
+ { title: 'Report', detail: 'synthesize findings' },
7
+ ],
8
+ }
9
+
10
+ phase('Hunt')
11
+ const target = args.target || 'this codebase'
12
+
13
+ const { confirmed, totalSeen, rounds, converged } = await loopUntilConvergence({
14
+ finders: [
15
+ () =>
16
+ agent(
17
+ `Find bugs in ${target}. Look for: null safety, race conditions, resource leaks, edge cases. Return { items: [{ file, line, summary, type }] }`,
18
+ {
19
+ label: 'hunt:general',
20
+ schema: {
21
+ type: 'object',
22
+ properties: {
23
+ items: {
24
+ type: 'array',
25
+ items: {
26
+ type: 'object',
27
+ properties: {
28
+ file: { type: 'string' },
29
+ line: { type: 'number' },
30
+ summary: { type: 'string' },
31
+ type: { type: 'string' },
32
+ },
33
+ required: ['file', 'summary', 'type'],
34
+ },
35
+ },
36
+ },
37
+ required: ['items'],
38
+ },
39
+ },
40
+ ),
41
+ () =>
42
+ agent(
43
+ `Find security vulnerabilities in ${target}: injection, auth bypass, insecure crypto, exposed secrets. Return { items: [{ file, line, summary, type }] }`,
44
+ {
45
+ label: 'hunt:security',
46
+ schema: {
47
+ type: 'object',
48
+ properties: {
49
+ items: {
50
+ type: 'array',
51
+ items: {
52
+ type: 'object',
53
+ properties: {
54
+ file: { type: 'string' },
55
+ line: { type: 'number' },
56
+ summary: { type: 'string' },
57
+ type: { type: 'string' },
58
+ },
59
+ required: ['file', 'summary', 'type'],
60
+ },
61
+ },
62
+ },
63
+ required: ['items'],
64
+ },
65
+ },
66
+ ),
67
+ ],
68
+ keyFn: (bug) => `${bug.file}:${bug.line}:${bug.summary}`,
69
+ verify: async (bug) =>
70
+ verify(bug, {
71
+ mode: 'adversarial',
72
+ skeptics: 3,
73
+ threshold: 2,
74
+ schema: {
75
+ type: 'object',
76
+ properties: { real: { type: 'boolean' }, reason: { type: 'string' } },
77
+ required: ['real', 'reason'],
78
+ },
79
+ }),
80
+ dryRounds: 2,
81
+ maxRounds: 10,
82
+ })
83
+
84
+ log(
85
+ `${converged ? 'Converged' : 'Max rounds reached'} after ${rounds} rounds. ${totalSeen} unique bugs seen, ${confirmed.length} confirmed.`,
86
+ )
87
+
88
+ phase('Report')
89
+ if (confirmed.length === 0) {
90
+ return `No confirmed bugs found after ${rounds} rounds of hunting.`
91
+ }
92
+ return await agent(
93
+ `Write a bug report from ${confirmed.length} confirmed bugs:\n${JSON.stringify(confirmed)}`,
94
+ { label: 'report' },
95
+ )
@@ -0,0 +1,55 @@
1
+ export const meta = {
2
+ name: 'judge',
3
+ description: 'Judge panel: N competing approaches × M judges → winner + synthesis',
4
+ phases: [
5
+ { title: 'Generate', detail: 'generate competing approaches' },
6
+ { title: 'Judge', detail: 'score and rank' },
7
+ { title: 'Synthesize', detail: 'final recommendation' },
8
+ ],
9
+ }
10
+
11
+ phase('Generate')
12
+ const problem = args.problem || (await agent('What problem are we solving?', { label: 'query' }))
13
+
14
+ const approaches = await parallel([
15
+ () => agent(`Solve "${problem}" with an MVP-first approach.`, { label: 'gen:mvp' }),
16
+ () => agent(`Solve "${problem}" with a risk-first approach.`, { label: 'gen:risk' }),
17
+ () => agent(`Solve "${problem}" with a user-first approach.`, { label: 'gen:user' }),
18
+ ])
19
+
20
+ const validApproaches = approaches.filter(Boolean)
21
+ log(`Generated ${validApproaches.length} approaches`)
22
+
23
+ phase('Judge')
24
+ const { winner, winnerIndex, scores, synthesis } = await judge(validApproaches, {
25
+ criteria: ['feasibility', 'impact', 'simplicity', 'risk'],
26
+ judges: 3,
27
+ synthesize: true,
28
+ schema: {
29
+ type: 'object',
30
+ properties: {
31
+ scores: {
32
+ type: 'object',
33
+ properties: {
34
+ feasibility: { type: 'number' },
35
+ impact: { type: 'number' },
36
+ simplicity: { type: 'number' },
37
+ risk: { type: 'number' },
38
+ },
39
+ required: ['feasibility', 'impact', 'simplicity', 'risk'],
40
+ },
41
+ notes: { type: 'string' },
42
+ },
43
+ required: ['scores', 'notes'],
44
+ },
45
+ })
46
+
47
+ phase('Synthesize')
48
+ return {
49
+ problem,
50
+ winner: `Approach #${winnerIndex + 1}`,
51
+ scores_summary: scores.map(
52
+ (s) => `Judge ${s.judgeIndex + 1}: approach ${s.attemptIndex + 1} = ${s.total}`,
53
+ ),
54
+ synthesis,
55
+ }
@@ -0,0 +1,92 @@
1
+ export const meta = {
2
+ name: 'migrate',
3
+ description: 'Code migration: discover → fan-out transform → verify → integrate',
4
+ phases: [
5
+ { title: 'Discover', detail: 'find migration targets' },
6
+ { title: 'Transform', detail: 'one agent per file' },
7
+ { title: 'Verify', detail: 'validate transformations' },
8
+ ],
9
+ }
10
+
11
+ phase('Discover')
12
+ const pattern =
13
+ args.pattern ||
14
+ (await agent('What code pattern needs migration? Return { pattern, replacement, reason }', {
15
+ schema: {
16
+ type: 'object',
17
+ properties: {
18
+ pattern: { type: 'string' },
19
+ replacement: { type: 'string' },
20
+ reason: { type: 'string' },
21
+ },
22
+ required: ['pattern', 'replacement'],
23
+ },
24
+ }))
25
+
26
+ const files = await agent(
27
+ `Find all files matching pattern: ${pattern.pattern}. Return { files: [{ path }] }`,
28
+ {
29
+ schema: {
30
+ type: 'object',
31
+ properties: {
32
+ files: {
33
+ type: 'array',
34
+ items: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
35
+ },
36
+ },
37
+ required: ['files'],
38
+ },
39
+ },
40
+ )
41
+
42
+ log(`Migrating ${files.files.length} files: ${pattern.pattern} → ${pattern.replacement}`)
43
+
44
+ phase('Transform')
45
+ const results = await pipeline(files.files, (f) =>
46
+ agent(
47
+ `In ${f.path}, migrate "${pattern.pattern}" to "${pattern.replacement}". Reason: ${pattern.reason}. Return { path, changes, success }`,
48
+ {
49
+ label: `migrate:${f.path}`,
50
+ isolation: 'worktree',
51
+ schema: {
52
+ type: 'object',
53
+ properties: {
54
+ path: { type: 'string' },
55
+ changes: { type: 'number' },
56
+ success: { type: 'boolean' },
57
+ },
58
+ required: ['path', 'success'],
59
+ },
60
+ },
61
+ ),
62
+ )
63
+
64
+ const succeeded = results.filter(Boolean).filter((r) => r.success)
65
+ const failed = results.filter(Boolean).filter((r) => !r.success)
66
+ log(`${succeeded.length} migrated, ${failed.length} failed`)
67
+
68
+ phase('Verify')
69
+ const verified = await parallel(
70
+ succeeded.map(
71
+ (f) => () =>
72
+ verify(
73
+ { file: f.path, migration: pattern.replacement },
74
+ {
75
+ mode: 'perspective',
76
+ lenses: ['correctness', 'style'],
77
+ threshold: 1,
78
+ schema: {
79
+ type: 'object',
80
+ properties: { real: { type: 'boolean' }, reason: { type: 'string' } },
81
+ required: ['real', 'reason'],
82
+ },
83
+ },
84
+ ),
85
+ ),
86
+ )
87
+
88
+ return {
89
+ migrated: succeeded.length,
90
+ failed: failed.length,
91
+ verified: verified.filter(Boolean).filter((v) => v.survives).length,
92
+ }
@@ -0,0 +1,87 @@
1
+ export const meta = {
2
+ name: 'research',
3
+ description: 'Deep research: scope → parallel search → verify → synthesize',
4
+ phases: [
5
+ { title: 'Scope', detail: 'define research angles' },
6
+ { title: 'Research', detail: 'parallel web searches' },
7
+ { title: 'Verify', detail: 'adversarial verification' },
8
+ { title: 'Synthesize', detail: 'final report' },
9
+ ],
10
+ }
11
+
12
+ phase('Scope')
13
+ const topic = args.topic || (await agent('What topic should we research?', { label: 'query' }))
14
+
15
+ phase('Research')
16
+ const angles = ['overview', 'technical-details', 'competitors', 'criticism', 'future-trends']
17
+ const raw = await parallel(
18
+ angles.map(
19
+ (angle) => () =>
20
+ agent(
21
+ `Research "${topic}" from angle: ${angle}. Return { sources: [{ title, url, keyPoint }] }`,
22
+ {
23
+ label: `research:${angle}`,
24
+ schema: {
25
+ type: 'object',
26
+ properties: {
27
+ sources: {
28
+ type: 'array',
29
+ items: {
30
+ type: 'object',
31
+ properties: {
32
+ title: { type: 'string' },
33
+ url: { type: 'string' },
34
+ keyPoint: { type: 'string' },
35
+ },
36
+ required: ['title', 'keyPoint'],
37
+ },
38
+ },
39
+ },
40
+ required: ['sources'],
41
+ },
42
+ },
43
+ ),
44
+ ),
45
+ )
46
+
47
+ const allSources = raw.filter(Boolean).flatMap((r) => r.sources)
48
+ const seen = new Set()
49
+ const unique = allSources.filter((s) => {
50
+ const k = s.url
51
+ if (seen.has(k)) return false
52
+ seen.add(k)
53
+ return true
54
+ })
55
+ log(`Collected ${unique.length} unique sources`)
56
+
57
+ phase('Verify')
58
+ const verified = await parallel(
59
+ unique.map(
60
+ (s) => () =>
61
+ verify(
62
+ { claim: s.keyPoint, source: s.title },
63
+ {
64
+ mode: 'adversarial',
65
+ skeptics: 2,
66
+ threshold: 1,
67
+ schema: {
68
+ type: 'object',
69
+ properties: { real: { type: 'boolean' }, reason: { type: 'string' } },
70
+ required: ['real', 'reason'],
71
+ },
72
+ },
73
+ ),
74
+ ),
75
+ )
76
+
77
+ const credible = verified
78
+ .filter(Boolean)
79
+ .filter((v) => v.survives)
80
+ .map((v) => v.finding)
81
+
82
+ phase('Synthesize')
83
+ const report = await agent(
84
+ `Synthesize research report on "${topic}" from credible sources:\n${JSON.stringify(credible)}`,
85
+ { label: 'synthesize' },
86
+ )
87
+ return report
@@ -0,0 +1,92 @@
1
+ export const meta = {
2
+ name: 'review',
3
+ description: 'Code review: fan-out per dimension → judge panel → report',
4
+ phases: [
5
+ { title: 'Review', detail: 'multi-dimensional review' },
6
+ { title: 'Judge', detail: 'judge panel scores findings' },
7
+ { title: 'Report', detail: 'final review report' },
8
+ ],
9
+ }
10
+
11
+ phase('Review')
12
+ const dimensions = ['correctness', 'security', 'performance', 'maintainability']
13
+ const rawFindings = await parallel(
14
+ dimensions.map(
15
+ (d) => () =>
16
+ agent(
17
+ `Review the code from the "${d}" lens. Return { findings: [{ severity, file, line, summary }] }`,
18
+ {
19
+ label: `review:${d}`,
20
+ schema: {
21
+ type: 'object',
22
+ properties: {
23
+ findings: {
24
+ type: 'array',
25
+ items: {
26
+ type: 'object',
27
+ properties: {
28
+ severity: { type: 'string', enum: ['blocker', 'high', 'medium', 'low'] },
29
+ file: { type: 'string' },
30
+ line: { type: 'number' },
31
+ summary: { type: 'string' },
32
+ dimension: { type: 'string' },
33
+ },
34
+ required: ['severity', 'file', 'summary'],
35
+ },
36
+ },
37
+ },
38
+ required: ['findings'],
39
+ },
40
+ },
41
+ ),
42
+ ),
43
+ )
44
+
45
+ // Edge logic: dedup across dimensions (pure JS)
46
+ const allFindings = rawFindings.filter(Boolean).flatMap((r) => r.findings)
47
+ const seen = new Set()
48
+ const uniqueFindings = allFindings.filter((f) => {
49
+ const k = `${f.file}:${f.line}:${f.summary}`
50
+ if (seen.has(k)) return false
51
+ seen.add(k)
52
+ return true
53
+ })
54
+
55
+ log(`${uniqueFindings.length} unique findings across ${dimensions.length} dimensions`)
56
+
57
+ phase('Judge')
58
+ if (uniqueFindings.length === 0) {
59
+ return 'No findings — code looks clean across all dimensions.'
60
+ }
61
+
62
+ // Judge panel: rank findings by severity
63
+ const judged = await judge(
64
+ uniqueFindings.map((f) => ({ finding: f })),
65
+ {
66
+ criteria: ['severity', 'actionability', 'confidence'],
67
+ judges: 2,
68
+ synthesize: false,
69
+ schema: {
70
+ type: 'object',
71
+ properties: {
72
+ scores: {
73
+ type: 'object',
74
+ properties: {
75
+ severity: { type: 'number' },
76
+ actionability: { type: 'number' },
77
+ confidence: { type: 'number' },
78
+ },
79
+ required: ['severity', 'actionability', 'confidence'],
80
+ },
81
+ notes: { type: 'string' },
82
+ },
83
+ required: ['scores', 'notes'],
84
+ },
85
+ },
86
+ )
87
+
88
+ phase('Report')
89
+ return await agent(
90
+ `Write a code review report from ${uniqueFindings.length} findings (ranked by judge panel). Top finding: ${JSON.stringify(judged.winner)}`,
91
+ { label: 'report' },
92
+ )
@@ -6,6 +6,8 @@
6
6
  * can be queried via the Task tool (output/stop actions) or the Agent View.
7
7
  */
8
8
 
9
+ export type BackgroundTaskKind = 'interactive' | 'forked' | 'attached' | 'unattended'
10
+
9
11
  export type BackgroundTaskStatus = 'running' | 'completed' | 'failed'
10
12
 
11
13
  export interface BackgroundTask {
@@ -13,10 +15,17 @@ export interface BackgroundTask {
13
15
  description: string
14
16
  agentType: string
15
17
  status: BackgroundTaskStatus
18
+ kind: BackgroundTaskKind
16
19
  startedAt: Date
17
20
  completedAt?: Date
18
21
  result?: string
19
22
  error?: string
23
+ /** Git worktree path (forked tasks) */
24
+ worktree?: string
25
+ /** Git branch name (forked tasks) */
26
+ branch?: string
27
+ /** Created PR URL (if requested) */
28
+ prUrl?: string
20
29
  abortController: AbortController
21
30
  }
22
31
 
@@ -39,6 +48,7 @@ export class BackgroundAgentRegistry {
39
48
  description: string,
40
49
  agentType: string,
41
50
  executor: (signal: AbortSignal) => Promise<string>,
51
+ kind: BackgroundTaskKind = 'interactive',
42
52
  ): string {
43
53
  const id = `bg-${++this.idCounter}-${Date.now().toString(36)}`
44
54
 
@@ -47,6 +57,7 @@ export class BackgroundAgentRegistry {
47
57
  description,
48
58
  agentType,
49
59
  status: 'running',
60
+ kind,
50
61
  startedAt: new Date(),
51
62
  abortController: new AbortController(),
52
63
  }
@@ -13,10 +13,13 @@ export interface SessionMessage {
13
13
  content: string
14
14
  }
15
15
 
16
+ export type SessionKind = 'interactive' | 'forked' | 'attached' | 'unattended'
17
+
16
18
  export interface AgentSession {
17
19
  id: string
18
20
  title: string
19
21
  status: SessionStatus
22
+ kind: SessionKind
20
23
  provider: string
21
24
  model: string
22
25
  task: string
@@ -25,6 +28,12 @@ export interface AgentSession {
25
28
  completedAt?: Date
26
29
  elapsedMs: number
27
30
  messages: SessionMessage[]
31
+ /** Git worktree path (forked sessions) */
32
+ worktree?: string
33
+ /** Git branch name (forked sessions) */
34
+ branch?: string
35
+ /** Created PR URL (if requested) */
36
+ prUrl?: string
28
37
  }
29
38
 
30
39
  export interface CreateSessionOptions {
@@ -53,6 +62,7 @@ export class AgentViewManager {
53
62
  id,
54
63
  title,
55
64
  status: 'needs-input',
65
+ kind: 'interactive',
56
66
  provider: options.provider ?? 'unknown',
57
67
  model: options.model ?? 'unknown',
58
68
  task,
@@ -1,4 +1,9 @@
1
- import type { MiphamConfig, InferenceHookConfig } from '../shared/index.ts'
1
+ import type {
2
+ MiphamConfig,
3
+ InferenceHookConfig,
4
+ CredentialMaskingConfig,
5
+ BackgroundAgentConfig,
6
+ } from '../shared/index.ts'
2
7
  import { DEFAULT_PROVIDERS } from '../shared/index.ts'
3
8
  import { PACKAGE_VERSION } from '../shared/index.ts'
4
9
 
@@ -18,3 +23,23 @@ export const DEFAULT_INFERENCE_HOOK_CONFIG: InferenceHookConfig = {
18
23
  organization_id: '',
19
24
  headers: {},
20
25
  }
26
+
27
+ export const DEFAULT_CREDENTIAL_MASKING_CONFIG: CredentialMaskingConfig = {
28
+ enabled: true,
29
+ files: [],
30
+ output_scrubbing: {
31
+ enabled: true,
32
+ patterns: ['(?i)(api[_-]?key|secret|token|password|credential)\\s*[:=]\\s*\\S+'],
33
+ },
34
+ env_filter: {
35
+ enabled: true,
36
+ patterns: ['(?i)(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|AUTH)$'],
37
+ },
38
+ }
39
+
40
+ export const DEFAULT_BACKGROUND_AGENT_CONFIG: BackgroundAgentConfig = {
41
+ auto_commit: true,
42
+ auto_push: true,
43
+ auto_worktree: true,
44
+ commit_coauthors: true,
45
+ }
@@ -15,8 +15,15 @@ import type {
15
15
  ProviderConfig,
16
16
  McpServerConfig,
17
17
  InferenceHookConfig,
18
+ CredentialMaskingConfig,
19
+ BackgroundAgentConfig,
18
20
  } from '../shared/index.ts'
19
- import { DEFAULT_CONFIG, DEFAULT_INFERENCE_HOOK_CONFIG } from './defaults'
21
+ import {
22
+ DEFAULT_CONFIG,
23
+ DEFAULT_INFERENCE_HOOK_CONFIG,
24
+ DEFAULT_CREDENTIAL_MASKING_CONFIG,
25
+ DEFAULT_BACKGROUND_AGENT_CONFIG,
26
+ } from './defaults'
20
27
 
21
28
  const MIPHAM_HOME = join(homedir(), '.mipham')
22
29
  const BACKUP_PREFIX = 'config.backup-'
@@ -298,3 +305,74 @@ export function loadInferenceHookConfig(cwd: string = process.cwd()): InferenceH
298
305
 
299
306
  return merged
300
307
  }
308
+
309
+ /**
310
+ * Load credential masking configuration from the same config sources.
311
+ * Merges project-level over user-level. Returns defaults if no section present.
312
+ */
313
+ export function loadCredentialMaskingConfig(cwd: string = process.cwd()): CredentialMaskingConfig {
314
+ const configPath = join(cwd, '.mipham', 'config.yml')
315
+ const userConfigPath = join(MIPHAM_HOME, 'config.yml')
316
+
317
+ let merged = { ...DEFAULT_CREDENTIAL_MASKING_CONFIG }
318
+
319
+ const paths = [userConfigPath, configPath] // project wins (loaded last)
320
+ for (const path of paths) {
321
+ try {
322
+ if (!existsSync(path)) continue
323
+ const raw = readFileSync(path, 'utf-8')
324
+ const parsed = parseYaml(raw) as Record<string, unknown>
325
+ const section = parsed.credential_masking as Partial<CredentialMaskingConfig> | undefined
326
+ if (section) {
327
+ merged = {
328
+ enabled: section.enabled ?? merged.enabled,
329
+ files: section.files ?? merged.files,
330
+ output_scrubbing: {
331
+ enabled: section.output_scrubbing?.enabled ?? merged.output_scrubbing.enabled,
332
+ patterns: section.output_scrubbing?.patterns ?? merged.output_scrubbing.patterns,
333
+ },
334
+ env_filter: {
335
+ enabled: section.env_filter?.enabled ?? merged.env_filter.enabled,
336
+ patterns: section.env_filter?.patterns ?? merged.env_filter.patterns,
337
+ },
338
+ }
339
+ }
340
+ } catch {
341
+ // Silently skip malformed configs
342
+ }
343
+ }
344
+
345
+ return merged
346
+ }
347
+
348
+ /**
349
+ * Load background agent configuration from config sources.
350
+ */
351
+ export function loadBackgroundAgentConfig(cwd: string = process.cwd()): BackgroundAgentConfig {
352
+ const configPath = join(cwd, '.mipham', 'config.yml')
353
+ const userConfigPath = join(MIPHAM_HOME, 'config.yml')
354
+
355
+ let merged = { ...DEFAULT_BACKGROUND_AGENT_CONFIG }
356
+
357
+ const paths = [userConfigPath, configPath]
358
+ for (const path of paths) {
359
+ try {
360
+ if (!existsSync(path)) continue
361
+ const raw = readFileSync(path, 'utf-8')
362
+ const parsed = parseYaml(raw) as Record<string, unknown>
363
+ const section = parsed.background_agent as Partial<BackgroundAgentConfig> | undefined
364
+ if (section) {
365
+ merged = {
366
+ auto_commit: section.auto_commit ?? merged.auto_commit,
367
+ auto_push: section.auto_push ?? merged.auto_push,
368
+ auto_worktree: section.auto_worktree ?? merged.auto_worktree,
369
+ commit_coauthors: section.commit_coauthors ?? merged.commit_coauthors,
370
+ }
371
+ }
372
+ } catch {
373
+ // Silently skip malformed configs
374
+ }
375
+ }
376
+
377
+ return merged
378
+ }