@miphamai/cli 0.13.0 → 0.15.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/sub-agent.ts +18 -1
- package/src/agent/types.ts +2 -0
- package/src/core/engine.ts +40 -0
- package/src/core/instructions.ts +28 -0
- package/src/core/permission.ts +5 -0
- package/src/core/usage-tracker.ts +103 -0
- package/src/providers/anthropic.ts +9 -1
- package/src/providers/openai-compat.ts +10 -0
- package/src/shared/types.ts +14 -1
- package/src/skills/fork-executor.ts +3 -1
- package/src/tools/agent/agent.ts +1 -1
- package/src/tools/agent/skill.ts +1 -0
- package/src/tools/agent/workflow.ts +67 -3
- package/src/ui/commands.ts +143 -6
- package/src/workflow/primitives/agent.ts +132 -46
- package/src/workflow/primitives/loop.ts +103 -0
- package/src/workflow/primitives/verify.ts +275 -0
- package/src/workflow/runtime.ts +22 -7
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
|
+
)
|
package/src/agent/sub-agent.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { SubAgentType, SubAgentOptions, AgentDefinition } from './types'
|
|
|
4
4
|
import { createAgentContext } from './agent-context'
|
|
5
5
|
import { getBackgroundAgentRegistry } from './background-registry'
|
|
6
6
|
import type { HookEngine } from '../core/hooks'
|
|
7
|
+
import type { PermissionSystem } from '../core/permission'
|
|
7
8
|
|
|
8
9
|
const TYPE_SYSTEM_PROMPTS: Record<SubAgentType, string> = {
|
|
9
10
|
general: 'You are a focused sub-agent. Complete the assigned task thoroughly and return results.',
|
|
@@ -27,6 +28,7 @@ export class SubAgent {
|
|
|
27
28
|
constructor(
|
|
28
29
|
private registry: ProviderRegistry,
|
|
29
30
|
private toolRegistry: Map<string, ToolDefinition>,
|
|
31
|
+
private permission?: PermissionSystem,
|
|
30
32
|
private hookEngine?: HookEngine,
|
|
31
33
|
) {}
|
|
32
34
|
|
|
@@ -113,6 +115,9 @@ export class SubAgent {
|
|
|
113
115
|
const agentType = options.type || 'general'
|
|
114
116
|
const agentDef = options.agentDef
|
|
115
117
|
|
|
118
|
+
// Resolve execution directory: worktree isolation or process cwd
|
|
119
|
+
const execCwd = options.worktreePath || process.cwd()
|
|
120
|
+
|
|
116
121
|
// Resolve system prompt: agentDef > options.systemPrompt > builtin type
|
|
117
122
|
const systemPrompt =
|
|
118
123
|
agentDef?.systemPrompt || options.systemPrompt || TYPE_SYSTEM_PROMPTS[agentType]
|
|
@@ -229,9 +234,21 @@ export class SubAgent {
|
|
|
229
234
|
continue
|
|
230
235
|
}
|
|
231
236
|
|
|
237
|
+
// Security: check permission before executing
|
|
238
|
+
// Sub-agents run without user interaction — tools requiring approval are rejected
|
|
239
|
+
if (this.permission?.needsApproval(tool, tu.input)) {
|
|
240
|
+
currentMessages.push({
|
|
241
|
+
role: 'user' as const,
|
|
242
|
+
content:
|
|
243
|
+
`Tool "${tu.name}" requires user approval (permission: ask). ` +
|
|
244
|
+
`Cannot execute in non-interactive sub-agent context.`,
|
|
245
|
+
})
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
|
|
232
249
|
try {
|
|
233
250
|
const result = await tool.execute(tu.input, {
|
|
234
|
-
cwd:
|
|
251
|
+
cwd: execCwd,
|
|
235
252
|
sessionId: 'sub-agent',
|
|
236
253
|
provider: '',
|
|
237
254
|
model: resolvedModel,
|
package/src/agent/types.ts
CHANGED
|
@@ -44,4 +44,6 @@ export interface SubAgentOptions {
|
|
|
44
44
|
runInBackground?: boolean
|
|
45
45
|
/** Optional callback for streaming progress chunks during background execution. */
|
|
46
46
|
onProgress?: (chunk: string) => void
|
|
47
|
+
/** When set, tool executions use this path as cwd (git worktree isolation). */
|
|
48
|
+
worktreePath?: string
|
|
47
49
|
}
|
package/src/core/engine.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { AgentViewManager } from '../agent-view/agent-view-manager'
|
|
|
16
16
|
import type { SkillsLoader } from '../skills/loader'
|
|
17
17
|
import { getBackgroundAgentRegistry } from '../agent/background-registry'
|
|
18
18
|
import { RulesLoader } from './rules-loader'
|
|
19
|
+
import { UsageTracker } from './usage-tracker'
|
|
19
20
|
import { buildRequest, sendInferenceCheck, isInferenceHookEnabled } from './inference-hook'
|
|
20
21
|
|
|
21
22
|
export class QueryEngine {
|
|
@@ -82,6 +83,7 @@ export class QueryEngine {
|
|
|
82
83
|
private rulesLoader?: RulesLoader
|
|
83
84
|
/** Files touched in the current turn (for rules matching). */
|
|
84
85
|
private touchedFiles: Set<string> = new Set()
|
|
86
|
+
private usageTracker = new UsageTracker()
|
|
85
87
|
/** Inference hook (DLP) configuration. */
|
|
86
88
|
private inferenceHookConfig?: InferenceHookConfig
|
|
87
89
|
|
|
@@ -238,6 +240,10 @@ export class QueryEngine {
|
|
|
238
240
|
return this.permission
|
|
239
241
|
}
|
|
240
242
|
|
|
243
|
+
getUsageTracker(): UsageTracker {
|
|
244
|
+
return this.usageTracker
|
|
245
|
+
}
|
|
246
|
+
|
|
241
247
|
async *process(userInput: string, signal?: AbortSignal): AsyncGenerator<StreamChunk> {
|
|
242
248
|
// Fire UserPromptSubmit hooks before processing
|
|
243
249
|
if (this.hookEngine) {
|
|
@@ -293,6 +299,8 @@ export class QueryEngine {
|
|
|
293
299
|
let assistantContent = ''
|
|
294
300
|
let reasoningContent = ''
|
|
295
301
|
let thinkingContent = ''
|
|
302
|
+
let turnApiInputTokens = 0
|
|
303
|
+
let turnApiOutputTokens = 0
|
|
296
304
|
const toolUses: Array<{ id: string; name: string; input: Record<string, unknown> }> = []
|
|
297
305
|
|
|
298
306
|
// Stream model response
|
|
@@ -331,6 +339,12 @@ export class QueryEngine {
|
|
|
331
339
|
})
|
|
332
340
|
}
|
|
333
341
|
|
|
342
|
+
if (chunk.type === 'usage' && chunk.inputTokens !== undefined) {
|
|
343
|
+
// Accumulate API-reported token counts for this turn
|
|
344
|
+
turnApiInputTokens += chunk.inputTokens
|
|
345
|
+
turnApiOutputTokens += chunk.outputTokens || 0
|
|
346
|
+
}
|
|
347
|
+
|
|
334
348
|
if (chunk.type === 'stop') {
|
|
335
349
|
// Add assistant response to context
|
|
336
350
|
if (assistantContent || reasoningContent || thinkingContent) {
|
|
@@ -417,6 +431,31 @@ export class QueryEngine {
|
|
|
417
431
|
})
|
|
418
432
|
}
|
|
419
433
|
|
|
434
|
+
// Record API token usage for this turn, attributed to executed tools
|
|
435
|
+
if (turnApiInputTokens > 0 || turnApiOutputTokens > 0) {
|
|
436
|
+
if (toolUses.length > 0) {
|
|
437
|
+
// Attribute tokens equally across all tools invoked this turn
|
|
438
|
+
const perTool = toolUses.length
|
|
439
|
+
for (const tu of toolUses) {
|
|
440
|
+
this.usageTracker.recordApiUsage(
|
|
441
|
+
Math.round(turnApiInputTokens / perTool),
|
|
442
|
+
Math.round(turnApiOutputTokens / perTool),
|
|
443
|
+
tu.name,
|
|
444
|
+
)
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
this.usageTracker.recordApiUsage(turnApiInputTokens, turnApiOutputTokens, 'chat')
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
// Fallback: API doesn't report usage — use char-based estimate
|
|
451
|
+
const estimated = Math.round((assistantContent.length + userInput.length) / 4)
|
|
452
|
+
if (toolUses.length > 0) {
|
|
453
|
+
for (const tu of toolUses) {
|
|
454
|
+
this.usageTracker.recordEstimatedUsage(Math.round(estimated / toolUses.length), tu.name)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
420
459
|
// Inject path-scoped rules for touched files
|
|
421
460
|
this.injectRules()
|
|
422
461
|
|
|
@@ -721,6 +760,7 @@ export class QueryEngine {
|
|
|
721
760
|
artifactServer: this.artifactServer,
|
|
722
761
|
agentRegistry: this.agentRegistry,
|
|
723
762
|
backgroundAgentRegistry: getBackgroundAgentRegistry(),
|
|
763
|
+
permissionSystem: this.permission,
|
|
724
764
|
})
|
|
725
765
|
|
|
726
766
|
// Track touched files for rules matching
|
package/src/core/instructions.ts
CHANGED
|
@@ -66,6 +66,34 @@ export class InstructionsLoader {
|
|
|
66
66
|
parts.push(this.skillsReminder)
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// Inject workflow auto-generation guidance
|
|
70
|
+
parts.push(`## Workflow Auto-Generation
|
|
71
|
+
|
|
72
|
+
When a task involves 3+ independent subtasks, multi-file operations,
|
|
73
|
+
or unknown-size discovery, generate a workflow script and execute it
|
|
74
|
+
via the Workflow tool instead of running agents sequentially. The
|
|
75
|
+
orchestration itself is code (zero tokens for inter-agent coordination).
|
|
76
|
+
|
|
77
|
+
Prefer workflows for: audits across many files, web research with multiple
|
|
78
|
+
sources, code migrations touching many files, security scans, bug hunts
|
|
79
|
+
with unknown scope, multi-dimensional code reviews.
|
|
80
|
+
|
|
81
|
+
Available primitives: agent(), parallel(), pipeline(), verify(),
|
|
82
|
+
judge(), loopUntilConvergence(), phase(), log(), args, budget.
|
|
83
|
+
|
|
84
|
+
Key rules:
|
|
85
|
+
- Default to pipeline() — only use parallel() barrier when a stage
|
|
86
|
+
genuinely needs all prior results at once
|
|
87
|
+
- Edge logic (flatten, dedupe, filter) is plain JS — not agent calls
|
|
88
|
+
- Use verify() on edges where confidence matters
|
|
89
|
+
- Use loopUntilConvergence() for discovery tasks with unknown size
|
|
90
|
+
|
|
91
|
+
When a workflow completes successfully, offer to save it:
|
|
92
|
+
"Workflow complete. Save this script? /workflow save <name>"
|
|
93
|
+
|
|
94
|
+
Script format: export const meta = { name, description, phases: [...] }
|
|
95
|
+
// script body using primitives...`)
|
|
96
|
+
|
|
69
97
|
return parts.join('\n\n---\n\n')
|
|
70
98
|
}
|
|
71
99
|
|
package/src/core/permission.ts
CHANGED
|
@@ -223,6 +223,11 @@ export class PermissionSystem {
|
|
|
223
223
|
case 'auto':
|
|
224
224
|
// Safety checks handled by hook layer (PreToolUse hooks).
|
|
225
225
|
// Bypass the static permission system so hooks are the sole gate.
|
|
226
|
+
// Exception: SendMessage always goes through the permission classifier
|
|
227
|
+
// so deny/allow rules are honored for cross-session messages.
|
|
228
|
+
if (tool.name === 'SendMessage') {
|
|
229
|
+
return 'mode-baseline'
|
|
230
|
+
}
|
|
226
231
|
return 'bypass'
|
|
227
232
|
|
|
228
233
|
case 'dontAsk':
|