@miphamai/cli 0.9.0 → 0.10.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.
@@ -80,6 +80,7 @@ export interface ToolContext {
80
80
  toolRegistry?: Map<string, ToolDefinition>
81
81
  artifactServer?: import('../artifacts/server').ArtifactServer
82
82
  agentRegistry?: import('../agent/agent-registry').AgentRegistry
83
+ backgroundAgentRegistry?: import('../agent/background-registry').BackgroundAgentRegistry
83
84
  }
84
85
 
85
86
  // ── Artifact Types ──
@@ -116,9 +117,18 @@ export interface ToolDefinition {
116
117
  execute: (params: Record<string, unknown>, ctx: ToolContext) => Promise<ToolResult>
117
118
  }
118
119
 
120
+ // ── Task Notification Types ──
121
+ export interface TaskNotification {
122
+ taskId: string
123
+ status: 'started' | 'completed' | 'failed'
124
+ description: string
125
+ content?: string
126
+ error?: string
127
+ }
128
+
119
129
  // ── Stream Types ──
120
130
  export interface StreamChunk {
121
- type: 'text' | 'tool_use' | 'tool_result' | 'thinking' | 'stop' | 'error'
131
+ type: 'text' | 'tool_use' | 'tool_result' | 'thinking' | 'stop' | 'error' | 'task_notification'
122
132
  content?: string
123
133
  toolUse?: ToolUseContent
124
134
  tool_use_id?: string
@@ -127,6 +137,8 @@ export interface StreamChunk {
127
137
  reasoning_content?: string
128
138
  /** Anthropic thinking block content (DeepSeek Anthropic endpoint). */
129
139
  thinking?: string
140
+ /** Background task notification payload (type: 'task_notification'). */
141
+ taskNotification?: TaskNotification
130
142
  }
131
143
 
132
144
  // ── Config Types ──
@@ -173,6 +185,7 @@ export interface SkillDefinition {
173
185
  export type HookEvent =
174
186
  | 'PreToolUse'
175
187
  | 'PostToolUse'
188
+ | 'PostToolUseFailure'
176
189
  | 'SessionStart'
177
190
  | 'SessionEnd'
178
191
  | 'Notification'
@@ -181,6 +194,8 @@ export type HookEvent =
181
194
  | 'PreCompact'
182
195
  | 'PostCompact'
183
196
  | 'ConfigChange'
197
+ | 'SubagentStart'
198
+ | 'SubagentStop'
184
199
 
185
200
  export type HookType = 'command' | 'http' | 'code' | 'mcp_tool'
186
201
 
@@ -1,13 +1,17 @@
1
1
  import type { ToolDefinition } from '../../shared/index.ts'
2
2
  import { SubAgent } from '../../agent/sub-agent'
3
3
  import type { SubAgentType } from '../../agent/types'
4
+ import { getBackgroundAgentRegistry } from '../../agent/background-registry'
4
5
 
5
6
  const VALID_TYPES: SubAgentType[] = ['general', 'explore', 'plan', 'code-review']
6
7
 
7
8
  export const agentTool: ToolDefinition = {
8
9
  name: 'Agent',
9
10
  description:
10
- 'Launch a sub-agent to handle complex, multi-step tasks independently. Available types: general, explore, plan, code-review.',
11
+ 'Launch a sub-agent to handle complex, multi-step tasks independently. ' +
12
+ 'Available types: general (default), explore (code search), plan (design), code-review. ' +
13
+ 'Set run_in_background: true to execute asynchronously — returns a task ID immediately; ' +
14
+ 'results are retrievable via the Task tool (output action) or Agent View.',
11
15
  category: 'agent',
12
16
  permission: 'ask',
13
17
  parameters: {
@@ -19,6 +23,11 @@ export const agentTool: ToolDefinition = {
19
23
  type: 'string',
20
24
  description: 'Type: general (default), explore (code search), plan (design), code-review',
21
25
  },
26
+ run_in_background: {
27
+ type: 'boolean',
28
+ description:
29
+ 'When true, execute asynchronously and return a task ID immediately. Use Task output to retrieve results. Default: false.',
30
+ },
22
31
  },
23
32
  required: ['description', 'prompt'],
24
33
  },
@@ -26,6 +35,7 @@ export const agentTool: ToolDefinition = {
26
35
  const description = params.description as string
27
36
  const prompt = params.prompt as string
28
37
  const agentType = (params.subagent_type as SubAgentType) || 'general'
38
+ const runInBackground = params.run_in_background === true
29
39
 
30
40
  if (!VALID_TYPES.includes(agentType)) {
31
41
  return {
@@ -51,7 +61,36 @@ export const agentTool: ToolDefinition = {
51
61
 
52
62
  try {
53
63
  const sub = new SubAgent(registry, toolRegistry)
54
- const result = await sub.execute(prompt, description, { type: agentType, agentDef })
64
+ const result = await sub.execute(prompt, description, {
65
+ type: agentType,
66
+ agentDef,
67
+ runInBackground,
68
+ })
69
+
70
+ // If background execution, also register in the task system for Task tool integration
71
+ if (runInBackground) {
72
+ const bgMatch = result.match(/\[background-task:(.+?)\]/)
73
+ if (bgMatch) {
74
+ const bgTaskId = bgMatch[1]!
75
+ const bgRegistry = getBackgroundAgentRegistry()
76
+ const bgTask = bgRegistry.get(bgTaskId)
77
+
78
+ return {
79
+ success: true,
80
+ content:
81
+ `── Background Agent Started ──\n\n` +
82
+ `Task ID: ${bgTaskId}\n` +
83
+ `Type: ${agentType}\n` +
84
+ `Task: ${description}\n` +
85
+ `Status: ${bgTask?.status || 'running'}\n\n` +
86
+ `The agent is running in the background. You can continue working.\n` +
87
+ `Use Task output taskId="${bgTaskId}" to check results.\n` +
88
+ `Use Task stop taskId="${bgTaskId}" to cancel.\n` +
89
+ `Use /agents to view in Agent View dashboard.`,
90
+ }
91
+ }
92
+ }
93
+
55
94
  return { success: true, content: result }
56
95
  } catch (err) {
57
96
  return { success: false, content: '', error: String(err) }
@@ -0,0 +1,92 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import type { ToolDefinition } from '../../shared/index.ts'
4
+
5
+ export const enterPlanModeTool: ToolDefinition = {
6
+ name: 'EnterPlanMode',
7
+ description:
8
+ 'Enter plan mode — a read-only mode for analysis and design. ' +
9
+ 'In plan mode, only Read/Grep/Glob tools are auto-approved; all other tools require confirmation. ' +
10
+ 'Use this before writing code to design an implementation approach, ' +
11
+ 'explore the codebase, and get user approval before executing changes.',
12
+ category: 'agent',
13
+ permission: 'auto',
14
+ parameters: {
15
+ type: 'object',
16
+ properties: {
17
+ description: {
18
+ type: 'string',
19
+ description:
20
+ 'Brief description of what you are planning (e.g., "Add user authentication flow")',
21
+ },
22
+ },
23
+ required: [],
24
+ },
25
+ async execute(params, ctx) {
26
+ const description = (params.description as string) || 'Implementation Plan'
27
+
28
+ // Create plan file
29
+ const planDir = join(ctx.cwd, '.mipham', 'plans')
30
+ mkdirSync(planDir, { recursive: true })
31
+
32
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
33
+ const filename = `plan-${timestamp}.md`
34
+ const filepath = join(planDir, filename)
35
+
36
+ const content = [
37
+ `# ${description}`,
38
+ '',
39
+ `> Generated: ${new Date().toISOString()}`,
40
+ `> Status: Draft`,
41
+ '',
42
+ '## Overview',
43
+ '',
44
+ '[Describe what this plan aims to achieve]',
45
+ '',
46
+ '## Files to Modify',
47
+ '',
48
+ '| File | Change | Reason |',
49
+ '|------|--------|--------|',
50
+ '| | | |',
51
+ '',
52
+ '## Implementation Steps',
53
+ '',
54
+ '1. ',
55
+ '2. ',
56
+ '3. ',
57
+ '',
58
+ '## Verification',
59
+ '',
60
+ '- [ ] Tests pass',
61
+ '- [ ] Typecheck passes',
62
+ '- [ ] Manual verification',
63
+ '',
64
+ '## Notes',
65
+ '',
66
+ '[Any additional context, risks, or dependencies]',
67
+ ].join('\n')
68
+
69
+ writeFileSync(filepath, content, 'utf-8')
70
+
71
+ return {
72
+ success: true,
73
+ content: [
74
+ '── Plan Mode Activated ──',
75
+ '',
76
+ `Plan file: ${filepath}`,
77
+ '',
78
+ 'Plan mode is READ-ONLY:',
79
+ ' ✅ Read, Grep, Glob — auto-approved',
80
+ ' ⚠️ All other tools — require confirmation',
81
+ '',
82
+ 'Design your approach, explore the codebase, then:',
83
+ ' • Use ExitPlanMode to submit your plan for approval',
84
+ ' • The user will review and approve before code changes begin',
85
+ '',
86
+ 'ExitPlanMode parameters:',
87
+ ' approved: true → switch to acceptEdits mode (code changes allowed)',
88
+ ' approved: false → revert to default mode',
89
+ ].join('\n'),
90
+ }
91
+ },
92
+ }
@@ -0,0 +1,54 @@
1
+ import type { ToolDefinition } from '../../shared/index.ts'
2
+
3
+ export const exitPlanModeTool: ToolDefinition = {
4
+ name: 'ExitPlanMode',
5
+ description:
6
+ 'Exit plan mode and submit your plan for approval. ' +
7
+ 'Set approved: true to switch to acceptEdits mode (code changes allowed). ' +
8
+ 'Set approved: false to revert to default mode. ' +
9
+ 'Use this after you have finished designing your approach in plan mode.',
10
+ category: 'agent',
11
+ permission: 'auto',
12
+ parameters: {
13
+ type: 'object',
14
+ properties: {
15
+ approved: {
16
+ type: 'boolean',
17
+ description:
18
+ 'Whether the user approved the plan. true → switch to acceptEdits mode. false → revert to default.',
19
+ },
20
+ },
21
+ required: ['approved'],
22
+ },
23
+ async execute(params, _ctx) {
24
+ const approved = params.approved === true
25
+
26
+ if (approved) {
27
+ return {
28
+ success: true,
29
+ content: [
30
+ '── Plan Approved ──',
31
+ '',
32
+ '✓ Exiting plan mode.',
33
+ '✓ Switching to acceptEdits mode — reads and file edits are auto-approved.',
34
+ '',
35
+ 'You can now implement the plan. The plan file is in .mipham/plans/.',
36
+ '',
37
+ 'Use Shift+Tab to cycle permission modes if you need to change.',
38
+ ].join('\n'),
39
+ }
40
+ }
41
+
42
+ return {
43
+ success: true,
44
+ content: [
45
+ '── Plan Mode Exited ──',
46
+ '',
47
+ '✓ Returning to default permission mode.',
48
+ '',
49
+ 'No code changes were made. The plan file is preserved in .mipham/plans/.',
50
+ 'Use EnterPlanMode again when ready to resume planning.',
51
+ ].join('\n'),
52
+ }
53
+ },
54
+ }
@@ -0,0 +1,186 @@
1
+ import type { ToolDefinition } from '../../shared/index.ts'
2
+
3
+ export const reportFindingsTool: ToolDefinition = {
4
+ name: 'ReportFindings',
5
+ description:
6
+ 'Report code-review findings as a typed list. ' +
7
+ 'Use this to output structured review results with file, line, summary, ' +
8
+ 'failure_scenario, and category. Findings are ranked most-severe first.',
9
+ category: 'agent',
10
+ permission: 'auto',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ level: {
15
+ type: 'string',
16
+ enum: ['low', 'medium', 'high', 'xhigh', 'max'],
17
+ description: 'Effort level the review ran at.',
18
+ },
19
+ findings: {
20
+ type: 'array',
21
+ description: 'Verified findings, most-severe first; empty if none survived.',
22
+ minItems: 0,
23
+ maxItems: 32,
24
+ items: {
25
+ type: 'object',
26
+ properties: {
27
+ file: {
28
+ type: 'string',
29
+ description: 'Repo-relative path of the file the finding is in.',
30
+ },
31
+ line: {
32
+ type: 'integer',
33
+ description: '1-indexed line the finding anchors to.',
34
+ },
35
+ summary: {
36
+ type: 'string',
37
+ description: 'One-sentence statement of the defect.',
38
+ },
39
+ short_summary: {
40
+ type: 'string',
41
+ maxLength: 60,
42
+ description: 'Compressed label for compact UI (≤60 chars).',
43
+ },
44
+ failure_scenario: {
45
+ type: 'string',
46
+ description: 'Concrete inputs/state → wrong output/crash.',
47
+ },
48
+ category: {
49
+ type: 'string',
50
+ maxLength: 40,
51
+ description:
52
+ 'Short kebab-case slug: correctness, security, performance, simplification, test-coverage, etc.',
53
+ },
54
+ verdict: {
55
+ type: 'string',
56
+ enum: ['CONFIRMED', 'PLAUSIBLE'],
57
+ description: 'Set when a verify pass ran; absent on inline-only reviews.',
58
+ },
59
+ outcome: {
60
+ type: 'string',
61
+ enum: ['fixed', 'skipped', 'no_change_needed'],
62
+ description:
63
+ 'Set ONLY when re-reporting after applying fixes: what happened to this finding.',
64
+ },
65
+ },
66
+ required: ['file', 'summary', 'failure_scenario'],
67
+ },
68
+ },
69
+ },
70
+ required: ['findings'],
71
+ },
72
+
73
+ async execute(params, _ctx) {
74
+ const findings = params.findings as Array<Record<string, unknown>> | undefined
75
+ const level = (params.level as string) || 'medium'
76
+
77
+ if (!Array.isArray(findings)) {
78
+ return {
79
+ success: false,
80
+ content: '',
81
+ error: 'findings must be an array',
82
+ }
83
+ }
84
+
85
+ // Validate each finding has required fields
86
+ const errors: string[] = []
87
+ for (let i = 0; i < findings.length; i++) {
88
+ const f = findings[i]!
89
+ if (!f.file || typeof f.file !== 'string') {
90
+ errors.push(`findings[${i}]: "file" is required and must be a string`)
91
+ }
92
+ if (!f.summary || typeof f.summary !== 'string') {
93
+ errors.push(`findings[${i}]: "summary" is required and must be a string`)
94
+ }
95
+ if (!f.failure_scenario || typeof f.failure_scenario !== 'string') {
96
+ errors.push(`findings[${i}]: "failure_scenario" is required and must be a string`)
97
+ }
98
+ if (f.line !== undefined && typeof f.line !== 'number') {
99
+ errors.push(`findings[${i}]: "line" must be an integer if provided`)
100
+ }
101
+ if (f.verdict && !['CONFIRMED', 'PLAUSIBLE'].includes(f.verdict as string)) {
102
+ errors.push(`findings[${i}]: "verdict" must be CONFIRMED or PLAUSIBLE`)
103
+ }
104
+ if (f.outcome && !['fixed', 'skipped', 'no_change_needed'].includes(f.outcome as string)) {
105
+ errors.push(`findings[${i}]: "outcome" must be fixed, skipped, or no_change_needed`)
106
+ }
107
+ }
108
+
109
+ if (errors.length > 0) {
110
+ return {
111
+ success: false,
112
+ content: '',
113
+ error: `Validation errors:\n${errors.map((e) => ` • ${e}`).join('\n')}`,
114
+ }
115
+ }
116
+
117
+ if (findings.length === 0) {
118
+ return {
119
+ success: true,
120
+ content: `── Review Findings (${level} effort) ──\n\n✅ No findings to report.\n\nAll checks passed at ${level} effort level.`,
121
+ }
122
+ }
123
+
124
+ // Format findings as a structured report
125
+ const lines: string[] = [
126
+ `── Review Findings (${level} effort) ──`,
127
+ '',
128
+ `${findings.length} finding${findings.length === 1 ? '' : 's'}:`,
129
+ '',
130
+ ]
131
+
132
+ // Group by category for readability
133
+ const byCategory = new Map<string, Array<Record<string, unknown>>>()
134
+ for (const f of findings) {
135
+ const cat = (f.category as string) || 'uncategorized'
136
+ const list = byCategory.get(cat) || []
137
+ list.push(f)
138
+ byCategory.set(cat, list)
139
+ }
140
+
141
+ const CATEGORY_ICONS: Record<string, string> = {
142
+ correctness: '🔴',
143
+ security: '🔒',
144
+ performance: '⚡',
145
+ simplification: '🧹',
146
+ efficiency: '⏱️',
147
+ 'test-coverage': '🧪',
148
+ architecture: '🏗️',
149
+ maintainability: '🔧',
150
+ }
151
+
152
+ for (const [category, items] of byCategory) {
153
+ const icon = CATEGORY_ICONS[category] || '📌'
154
+ lines.push(`${icon} ${category} (${items.length}):`)
155
+ for (const f of items) {
156
+ const verdict = f.verdict ? ` [${f.verdict}]` : ''
157
+ const outcome = f.outcome ? ` → ${f.outcome}` : ''
158
+ const lineRef = f.line ? `:${f.line}` : ''
159
+ lines.push(` • ${f.file}${lineRef}${verdict}${outcome}`)
160
+ lines.push(` ${f.summary}`)
161
+ if (f.failure_scenario) {
162
+ const scenario =
163
+ (f.failure_scenario as string).length > 120
164
+ ? (f.failure_scenario as string).slice(0, 120) + '...'
165
+ : (f.failure_scenario as string)
166
+ lines.push(` 💥 ${scenario}`)
167
+ }
168
+ }
169
+ lines.push('')
170
+ }
171
+
172
+ // Severity summary
173
+ const confirmed = findings.filter((f) => f.verdict === 'CONFIRMED').length
174
+ const plausible = findings.filter((f) => f.verdict === 'PLAUSIBLE').length
175
+ if (confirmed > 0 || plausible > 0) {
176
+ lines.push(
177
+ `📊 ${confirmed} CONFIRMED · ${plausible} PLAUSIBLE · ${findings.length - confirmed - plausible} unverified`,
178
+ )
179
+ }
180
+
181
+ return {
182
+ success: true,
183
+ content: lines.join('\n'),
184
+ }
185
+ },
186
+ }
@@ -0,0 +1,67 @@
1
+ import type { ToolDefinition } from '../../shared/index.ts'
2
+ import { getMessageBus } from '../../agent/message-bus'
3
+
4
+ export const sendMessageTool: ToolDefinition = {
5
+ name: 'SendMessage',
6
+ description:
7
+ 'Send a message to another agent or the main conversation. ' +
8
+ 'Use "main" as the recipient to message the parent session, ' +
9
+ 'or a background task ID (e.g., "bg-1-xxx") to message a specific agent. ' +
10
+ 'Messages are stored in the AgentMessageBus and can be polled by the recipient.',
11
+ category: 'agent',
12
+ permission: 'auto',
13
+ parameters: {
14
+ type: 'object',
15
+ properties: {
16
+ to: {
17
+ type: 'string',
18
+ description:
19
+ 'Recipient: "main" for the parent conversation, a background task ID, or an agent name.',
20
+ },
21
+ summary: {
22
+ type: 'string',
23
+ description: 'A 5-10 word summary shown as a one-line preview (max 200 chars).',
24
+ },
25
+ message: {
26
+ type: 'string',
27
+ description: 'Plain text message content.',
28
+ },
29
+ },
30
+ required: ['to', 'message'],
31
+ },
32
+ async execute(params, ctx) {
33
+ const to = params.to as string
34
+ const summary = (params.summary as string) || '(no subject)'
35
+ const message = params.message as string
36
+
37
+ // Determine sender: use sessionId or 'main'
38
+ const from =
39
+ ctx.sessionId === 'sub-agent'
40
+ ? `sub-agent-${Date.now().toString(36)}`
41
+ : ctx.sessionId || 'main'
42
+
43
+ try {
44
+ const bus = getMessageBus()
45
+ const msgId = bus.post(from, to, summary, message)
46
+
47
+ const unreadForRecipient = bus.unreadCount(to)
48
+
49
+ return {
50
+ success: true,
51
+ content:
52
+ `── Message Sent ──\n\n` +
53
+ `ID: ${msgId}\n` +
54
+ `From: ${from}\n` +
55
+ `To: ${to}\n` +
56
+ `Summary: ${summary.slice(0, 100)}\n\n` +
57
+ `The recipient has ${unreadForRecipient} unread message(s).`,
58
+ }
59
+ } catch (err) {
60
+ return {
61
+ success: false,
62
+ content: '',
63
+ error: `Failed to send message: ${String(err)}`,
64
+ }
65
+ }
66
+ },
67
+ }
@@ -5,7 +5,9 @@ import type { QueryEngine } from '../../core/engine'
5
5
  export const workflowTool: ToolDefinition = {
6
6
  name: 'Workflow',
7
7
  description:
8
- 'Execute a multi-agent workflow script. The script uses agent(), parallel(), pipeline(), phase(), log(), args, budget primitives. Use for complex orchestrated tasks.',
8
+ 'Execute a multi-agent workflow script. The script uses agent(), parallel(), pipeline(), phase(), log(), args, budget primitives. ' +
9
+ 'Pass resumeFromRunId to resume a prior run — cached agent() calls are replayed from the journal, ' +
10
+ 'and only new/changed calls execute live.',
9
11
  category: 'agent',
10
12
  permission: 'ask',
11
13
  parameters: {
@@ -20,12 +22,20 @@ export const workflowTool: ToolDefinition = {
20
22
  type: 'object',
21
23
  description: 'Arguments to pass to the workflow script as the `args` global',
22
24
  },
25
+ resumeFromRunId: {
26
+ type: 'string',
27
+ description:
28
+ 'Run ID of a prior Workflow invocation to resume from. ' +
29
+ 'Completed agent() calls with unchanged (prompt, opts) return cached results; ' +
30
+ 'only edited or new calls re-run.',
31
+ },
23
32
  },
24
33
  required: ['script'],
25
34
  },
26
35
  async execute(params, ctx) {
27
36
  const script = params.script as string
28
37
  const args = (params.args as Record<string, unknown>) || {}
38
+ const resumeFromRunId = params.resumeFromRunId as string | undefined
29
39
 
30
40
  if (!ctx.registry || !ctx.toolRegistry) {
31
41
  return {
@@ -42,11 +52,21 @@ export const workflowTool: ToolDefinition = {
42
52
  } as QueryEngine
43
53
 
44
54
  try {
45
- const { runId, result } = await runWorkflow(script, engineStub, args)
46
- return {
47
- success: true,
48
- content: `Workflow ${runId} completed.\n\nResult:\n${typeof result === 'string' ? result : JSON.stringify(result, null, 2)}`,
55
+ const { runId, result, cacheHits, cacheMisses } = await runWorkflow(
56
+ script,
57
+ engineStub,
58
+ args,
59
+ null,
60
+ resumeFromRunId,
61
+ )
62
+
63
+ let content = `Workflow ${runId} completed.\n\n`
64
+ if (resumeFromRunId) {
65
+ content += `Cache: ${cacheHits} hits · ${cacheMisses} live\n\n`
49
66
  }
67
+ content += `Result:\n${typeof result === 'string' ? result : JSON.stringify(result, null, 2)}`
68
+
69
+ return { success: true, content }
50
70
  } catch (err) {
51
71
  return {
52
72
  success: false,