@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 +1 -1
- package/skills/workflows/audit.js +51 -0
- package/skills/workflows/hunt.js +95 -0
- package/skills/workflows/judge.js +55 -0
- package/skills/workflows/migrate.js +92 -0
- package/skills/workflows/research.js +87 -0
- package/skills/workflows/review.js +92 -0
- package/src/agent/background-registry.ts +11 -0
- package/src/agent-view/agent-view-manager.ts +10 -0
- package/src/config/defaults.ts +26 -1
- package/src/config/loader.ts +79 -1
- package/src/core/credential-masker.ts +203 -0
- package/src/core/instructions.ts +28 -0
- package/src/core/permission.ts +62 -8
- package/src/index.tsx +8 -1
- package/src/plugin/plugin-manager.ts +39 -2
- package/src/shared/types.ts +43 -0
- package/src/tools/agent/workflow.ts +67 -3
- package/src/tools/exec/bash.ts +29 -3
- package/src/tools/file/read.ts +30 -1
- package/src/ui/app.tsx +6 -1
- package/src/ui/chat.tsx +50 -17
- package/src/ui/commands.ts +451 -0
- package/src/workflow/primitives/loop.ts +103 -0
- package/src/workflow/primitives/verify.ts +275 -0
- package/src/workflow/runtime.ts +17 -1
package/package.json
CHANGED
|
@@ -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,
|
package/src/config/defaults.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
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
|
+
}
|
package/src/config/loader.ts
CHANGED
|
@@ -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 {
|
|
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
|
+
}
|