@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.13.0",
3
+ "version": "0.15.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
+ )
@@ -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: process.cwd(),
251
+ cwd: execCwd,
235
252
  sessionId: 'sub-agent',
236
253
  provider: '',
237
254
  model: resolvedModel,
@@ -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
  }
@@ -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
@@ -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
 
@@ -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':