@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.
@@ -0,0 +1,103 @@
1
+ /**
2
+ * UsageTracker — per-session token usage tracking with per-tool attribution.
3
+ *
4
+ * Tracks actual API-reported token counts (when available) and falls back
5
+ * to character-estimated counts. Token costs are attributed to the tool
6
+ * invoked in each turn — MCP tools (prefixed `mcp__`) get correct
7
+ * per-invocation attribution, not inflated cumulative costs.
8
+ */
9
+
10
+ export interface ToolUsage {
11
+ /** Total input tokens attributed to this tool. */
12
+ inputTokens: number
13
+ /** Total output tokens attributed to this tool. */
14
+ outputTokens: number
15
+ /** Number of times this tool was invoked. */
16
+ calls: number
17
+ }
18
+
19
+ export interface UsageSummary {
20
+ /** Total input tokens from API (0 if API data unavailable). */
21
+ apiInputTokens: number
22
+ /** Total output tokens from API (0 if API data unavailable). */
23
+ apiOutputTokens: number
24
+ /** Estimated total tokens (chars/4 heuristic, always available). */
25
+ estimatedTokens: number
26
+ /** Per-tool breakdown, keyed by tool name. "chat" = text-only turns. */
27
+ tools: Record<string, ToolUsage>
28
+ }
29
+
30
+ export class UsageTracker {
31
+ private _apiInputTokens = 0
32
+ private _apiOutputTokens = 0
33
+ private _estimatedTokens = 0
34
+ private _toolUsage = new Map<string, ToolUsage>()
35
+
36
+ /** Record API-reported token usage for a turn, attributed to the given tool. */
37
+ recordApiUsage(inputTokens: number, outputTokens: number, toolName?: string): void {
38
+ this._apiInputTokens += inputTokens
39
+ this._apiOutputTokens += outputTokens
40
+ this._recordTool(toolName || 'chat', inputTokens, outputTokens)
41
+ }
42
+
43
+ /** Record character-estimated token usage for a turn. */
44
+ recordEstimatedUsage(tokenCount: number, toolName?: string): void {
45
+ this._estimatedTokens += tokenCount
46
+ this._recordToolEstimated(toolName || 'chat', tokenCount)
47
+ }
48
+
49
+ /** Get a summary of all usage for the current session. */
50
+ getSummary(): UsageSummary {
51
+ const tools: Record<string, ToolUsage> = {}
52
+ for (const [name, usage] of this._toolUsage) {
53
+ tools[name] = { ...usage }
54
+ }
55
+ return {
56
+ apiInputTokens: this._apiInputTokens,
57
+ apiOutputTokens: this._apiOutputTokens,
58
+ estimatedTokens: this._estimatedTokens,
59
+ tools,
60
+ }
61
+ }
62
+
63
+ /** Reset all counters for a new session. */
64
+ reset(): void {
65
+ this._apiInputTokens = 0
66
+ this._apiOutputTokens = 0
67
+ this._estimatedTokens = 0
68
+ this._toolUsage.clear()
69
+ }
70
+
71
+ /** Total API tokens (input + output). */
72
+ get totalApiTokens(): number {
73
+ return this._apiInputTokens + this._apiOutputTokens
74
+ }
75
+
76
+ /** Whether any real API usage data has been recorded. */
77
+ get hasApiData(): boolean {
78
+ return this._apiInputTokens > 0 || this._apiOutputTokens > 0
79
+ }
80
+
81
+ // ── Private helpers ──
82
+
83
+ private _recordTool(name: string, inputTokens: number, outputTokens: number): void {
84
+ const existing = this._toolUsage.get(name)
85
+ if (existing) {
86
+ existing.inputTokens += inputTokens
87
+ existing.outputTokens += outputTokens
88
+ existing.calls++
89
+ } else {
90
+ this._toolUsage.set(name, { inputTokens, outputTokens, calls: 1 })
91
+ }
92
+ }
93
+
94
+ private _recordToolEstimated(name: string, tokenCount: number): void {
95
+ const existing = this._toolUsage.get(name)
96
+ if (existing) {
97
+ existing.inputTokens += tokenCount
98
+ existing.calls++
99
+ } else {
100
+ this._toolUsage.set(name, { inputTokens: tokenCount, outputTokens: 0, calls: 1 })
101
+ }
102
+ }
103
+ }
@@ -194,7 +194,15 @@ export class AnthropicProvider implements ProviderInstance {
194
194
  }
195
195
 
196
196
  case 'message_delta': {
197
- // Contains stop_reason and usage info
197
+ // Capture token usage for accurate cost tracking
198
+ if (event.usage) {
199
+ yield {
200
+ type: 'usage',
201
+ inputTokens: event.usage.input_tokens,
202
+ outputTokens: event.usage.output_tokens,
203
+ }
204
+ }
205
+ // Contains stop_reason; also handles late input_json_delta
198
206
  if (event.delta?.type === 'input_json_delta' && event.delta.partial_json) {
199
207
  accumulatedToolInput += event.delta.partial_json
200
208
  }
@@ -99,6 +99,16 @@ export class OpenAICompatProvider implements ProviderInstance {
99
99
  try {
100
100
  const parsed = JSON.parse(data)
101
101
  const choice = parsed.choices?.[0]
102
+
103
+ // Capture token usage when available (final chunk with stream_options.include_usage)
104
+ if (parsed.usage) {
105
+ yield {
106
+ type: 'usage',
107
+ inputTokens: parsed.usage.prompt_tokens,
108
+ outputTokens: parsed.usage.completion_tokens,
109
+ }
110
+ }
111
+
102
112
  if (!choice) continue
103
113
 
104
114
  const delta = choice.delta
@@ -81,6 +81,7 @@ export interface ToolContext {
81
81
  artifactServer?: import('../artifacts/server').ArtifactServer
82
82
  agentRegistry?: import('../agent/agent-registry').AgentRegistry
83
83
  backgroundAgentRegistry?: import('../agent/background-registry').BackgroundAgentRegistry
84
+ permissionSystem?: import('../core/permission').PermissionSystem
84
85
  }
85
86
 
86
87
  // ── Artifact Types ──
@@ -128,7 +129,15 @@ export interface TaskNotification {
128
129
 
129
130
  // ── Stream Types ──
130
131
  export interface StreamChunk {
131
- type: 'text' | 'tool_use' | 'tool_result' | 'thinking' | 'stop' | 'error' | 'task_notification'
132
+ type:
133
+ | 'text'
134
+ | 'tool_use'
135
+ | 'tool_result'
136
+ | 'thinking'
137
+ | 'stop'
138
+ | 'error'
139
+ | 'task_notification'
140
+ | 'usage'
132
141
  content?: string
133
142
  toolUse?: ToolUseContent
134
143
  tool_use_id?: string
@@ -139,6 +148,10 @@ export interface StreamChunk {
139
148
  thinking?: string
140
149
  /** Background task notification payload (type: 'task_notification'). */
141
150
  taskNotification?: TaskNotification
151
+ /** API-reported input token count (type: 'usage'). */
152
+ inputTokens?: number
153
+ /** API-reported output token count (type: 'usage'). */
154
+ outputTokens?: number
142
155
  }
143
156
 
144
157
  // ── Config Types ──
@@ -2,6 +2,7 @@ import { SubAgent } from '../agent/sub-agent'
2
2
  import type { ProviderRegistry } from '../providers/registry'
3
3
  import type { ToolDefinition, SkillDefinition } from '../shared/index.ts'
4
4
  import type { AgentDefinition } from '../agent/types'
5
+ import type { PermissionSystem } from '../core/permission'
5
6
 
6
7
  /**
7
8
  * Execute a skill in an isolated subagent context (context: fork).
@@ -15,6 +16,7 @@ export async function executeForkedSkill(
15
16
  args: string,
16
17
  registry: ProviderRegistry,
17
18
  toolRegistry: Map<string, ToolDefinition>,
19
+ permissionSystem?: PermissionSystem,
18
20
  ): Promise<string> {
19
21
  const agentDef: AgentDefinition = {
20
22
  name: `skill:${skill.name}`,
@@ -27,7 +29,7 @@ export async function executeForkedSkill(
27
29
  source: 'builtin',
28
30
  }
29
31
 
30
- const sub = new SubAgent(registry, toolRegistry)
32
+ const sub = new SubAgent(registry, toolRegistry, permissionSystem)
31
33
  const prompt = args
32
34
  ? `Execute the "${skill.name}" skill with arguments: ${args}`
33
35
  : `Execute the "${skill.name}" skill.`
@@ -60,7 +60,7 @@ export const agentTool: ToolDefinition = {
60
60
  const agentDef = ctx.agentRegistry?.resolve(agentType)
61
61
 
62
62
  try {
63
- const sub = new SubAgent(registry, toolRegistry)
63
+ const sub = new SubAgent(registry, toolRegistry, ctx.permissionSystem)
64
64
  const result = await sub.execute(prompt, description, {
65
65
  type: agentType,
66
66
  agentDef,
@@ -52,6 +52,7 @@ export const skillTool: ToolDefinition = {
52
52
  args,
53
53
  registry,
54
54
  ctx.toolRegistry || new Map(),
55
+ ctx.permissionSystem,
55
56
  )
56
57
  // Return to AI as internal context
57
58
  return { success: true, content: `[Forked skill "${skillName}" result]:\n${result}` }
@@ -5,9 +5,56 @@ 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. ' +
9
- 'Pass resumeFromRunId to resume a prior run — cached agent() calls are replayed from the journal, ' +
10
- 'and only new/changed calls execute live.',
8
+ 'Execute a workflow script that orchestrates multiple subagents deterministically. ' +
9
+ 'Workflows run in the background — this tool returns immediately with a task ID, ' +
10
+ 'and a <task-notification> arrives when the workflow completes. Use /workflows to watch live progress.\n\n' +
11
+ 'A workflow structures work across many agents — to be comprehensive (decompose and cover in parallel), ' +
12
+ 'to be confident (independent perspectives and adversarial checks before committing), ' +
13
+ 'or to take on scale one context cannot hold (migrations, audits, broad sweeps). ' +
14
+ 'The script is where you encode that structure: what fans out, what verifies, what synthesizes.\n\n' +
15
+ 'ONLY call this tool when the task benefits from multi-agent orchestration. ' +
16
+ 'For a simple single-agent lookup or edit, use the Agent tool or direct tools instead.\n\n' +
17
+ '## Primitives\n\n' +
18
+ '- agent(prompt: string, opts?: {label?, phase?, schema?, model?, effort?, isolation?}): Promise<any> — spawn a subagent. ' +
19
+ 'Without schema, returns final text as string. With schema (JSON Schema), returns validated object — retries on mismatch.\n' +
20
+ '- parallel(thunks: Array<() => Promise<any>>): Promise<any[]> — BARRIER: runs all thunks concurrently, waits for all. ' +
21
+ 'Failed thunks resolve to null. Use filter(Boolean) before consuming results.\n' +
22
+ '- pipeline(items: T[], ...stages): Promise<any[]> — NO barrier: each item flows through all stages independently. ' +
23
+ 'Item A can be in stage 3 while item B is still in stage 1. DEFAULT choice for multi-stage work.\n' +
24
+ '- verify(finding, opts): Promise<VerifyResult> — adversarial/perspective/consensus quality gate. ' +
25
+ 'Spawns skeptics or lens-based judges, applies threshold, returns {survives, votes, score}.\n' +
26
+ '- judge(attempts, opts): Promise<JudgeResult> — judge panel: N attempts scored by M judges across K criteria. ' +
27
+ 'Returns winner with optional synthesis grafting runner-up ideas.\n' +
28
+ '- loopUntilConvergence(opts): Promise<LoopUntilConvergenceResult> — convergent discovery loop. ' +
29
+ 'Fans out finders repeatedly, deduplicates against seen-set (NOT confirmed-set), ' +
30
+ 'stops after N consecutive dry rounds or maxRounds. Optionally verifies each finding.\n' +
31
+ '- phase(title: string): void — start a new progress group\n' +
32
+ '- log(message: string): void — emit progress message\n' +
33
+ '- args: any — verbatim args passed to Workflow tool\n' +
34
+ '- budget: {total, spent(), remaining()} — token budget tracking\n\n' +
35
+ '## Topology Selection Guide\n\n' +
36
+ 'DEFAULT TO pipeline(). Only reach for a barrier (parallel between stages) when you genuinely ' +
37
+ 'need ALL prior-stage results together.\n\n' +
38
+ 'A barrier is correct ONLY when stage N needs cross-item context from all of stage N-1: ' +
39
+ 'dedup/merge across the full result set, early-exit if total count is zero, cross-finding comparison.\n\n' +
40
+ 'A barrier is NOT justified by: flatten/map/filter (do it inside a pipeline stage), ' +
41
+ 'conceptually separate stages, cleaner code — barrier latency is real and measurable.\n\n' +
42
+ '- Diamond (fan-out → reduce → synthesize): market scans, audits, research\n' +
43
+ '- Pipeline (no barrier): each item flows independently — DEFAULT\n' +
44
+ '- Loop-until-convergence: unknown-size discovery (bugs, vulnerabilities, edge cases)\n' +
45
+ '- Judge panel: multiple competing approaches, pick best + graft runner-ups\n' +
46
+ '- Verifier-on-edge: quality gates before results reach downstream\n\n' +
47
+ '## Critical Rules\n\n' +
48
+ '- EDGE LOGIC IS FREE: flatten, dedupe, filter in plain JavaScript — NOT agent calls. ' +
49
+ 'results.flatMap(...) and a Set are deterministic, instant, zero tokens.\n' +
50
+ '- seen-set dedup for loops, NOT confirmed-set — rejected findings would otherwise revive every round.\n' +
51
+ '- Each node should have bounded input, validated output (schema), and one clear purpose.\n' +
52
+ '- Model tiering: use cheaper models for repetitive extraction/classification nodes, ' +
53
+ 'expensive models for synthesis/judgment nodes.\n\n' +
54
+ '## Script Format\n\n' +
55
+ 'Every script MUST begin with: export const meta = { name, description, phases: [{title, detail}] }\n' +
56
+ 'The meta object must be a PURE LITERAL — no variables, function calls, or template interpolation.\n' +
57
+ 'Use the SAME phase titles in meta.phases as in phase() calls.',
11
58
  category: 'agent',
12
59
  permission: 'ask',
13
60
  parameters: {
@@ -60,6 +107,23 @@ export const workflowTool: ToolDefinition = {
60
107
  resumeFromRunId,
61
108
  )
62
109
 
110
+ // Persist last-run state for /workflow save
111
+ try {
112
+ const { existsSync, mkdirSync, writeFileSync } = await import('node:fs')
113
+ const { join } = await import('node:path')
114
+ const workflowsDir = join(process.cwd(), '.claude', 'workflows')
115
+ if (!existsSync(workflowsDir)) {
116
+ mkdirSync(workflowsDir, { recursive: true })
117
+ }
118
+ writeFileSync(
119
+ join(workflowsDir, '.last-run.json'),
120
+ JSON.stringify({ runId, script, timestamp: new Date().toISOString() }),
121
+ 'utf-8',
122
+ )
123
+ } catch {
124
+ // best-effort — don't fail the workflow if state persistence fails
125
+ }
126
+
63
127
  let content = `Workflow ${runId} completed.\n\n`
64
128
  if (resumeFromRunId) {
65
129
  content += `Cache: ${cacheHits} hits · ${cacheMisses} live\n\n`
@@ -689,22 +689,47 @@ const recapCmd: CommandHandler = (ctx) => {
689
689
 
690
690
  const usageCmd: CommandHandler = (ctx) => {
691
691
  const c = ctx.engine.getContext()
692
- const tokens = c.getEstimatedTokens()
692
+ const estTokens = c.getEstimatedTokens()
693
693
  const msgs = c.getMessages()
694
694
  const maxTokens = 200_000
695
- const pct = ((tokens / maxTokens) * 100).toFixed(1)
695
+ const pct = ((estTokens / maxTokens) * 100).toFixed(1)
696
+
697
+ const tracker = ctx.engine.getUsageTracker()
698
+ const summary = tracker.getSummary()
699
+
700
+ // Build per-tool breakdown
701
+ const toolLines: string[] = []
702
+ const sortedTools = Object.entries(summary.tools).sort(
703
+ (a, b) => b[1].inputTokens + b[1].outputTokens - (a[1].inputTokens + a[1].outputTokens),
704
+ )
705
+ for (const [name, usage] of sortedTools) {
706
+ const total = usage.inputTokens + usage.outputTokens
707
+ const prefix = name.startsWith('mcp__') ? '🔌 ' : ' '
708
+ toolLines.push(
709
+ `${prefix}${name.padEnd(18)} ${total.toLocaleString().padStart(8)} tokens (${usage.calls} call${usage.calls !== 1 ? 's' : ''})`,
710
+ )
711
+ }
712
+
713
+ const apiTotal = summary.apiInputTokens + summary.apiOutputTokens
714
+ const apiLine =
715
+ summary.apiInputTokens > 0 || summary.apiOutputTokens > 0
716
+ ? `API tokens: ${summary.apiInputTokens.toLocaleString().padStart(8)} in / ${summary.apiOutputTokens.toLocaleString().padStart(6)} out (${apiTotal.toLocaleString()} total)`
717
+ : 'API tokens: (no API usage data yet)'
718
+
719
+ const toolSection = toolLines.length > 0 ? `\n── Per-Tool ──\n${toolLines.join('\n')}` : ''
696
720
 
697
721
  return {
698
722
  content: stripIndent`
699
723
  ── Usage Dashboard ──
700
- Context tokens: ~${tokens.toLocaleString()} / ${maxTokens.toLocaleString()} (${pct}%)
724
+ ${apiLine}
725
+ Context tokens: ~${estTokens.toLocaleString()} / ${maxTokens.toLocaleString()} (${pct}%)
701
726
  Messages: ${msgs.length}
702
727
  Provider: ${ctx.providerId}
703
728
  Model: ${ctx.modelId}
704
729
 
705
730
  ${'█'.repeat(Math.ceil(Number(pct) / 5))}${'░'.repeat(20 - Math.ceil(Number(pct) / 5))} ${pct}%
731
+ ${toolSection}
706
732
 
707
- Note: Token counting is approximate (chars/4).
708
733
  Use /context for detailed stats, /compact to free space.
709
734
  `,
710
735
  }
@@ -2388,6 +2413,111 @@ const workflowsCmd: CommandHandler = async () => {
2388
2413
  return { content: lines.join('\n') }
2389
2414
  }
2390
2415
 
2416
+ // ═══════════════════════════════════════════════════════════════
2417
+ // /workflow <task> — auto-generate + execute
2418
+ // ═══════════════════════════════════════════════════════════════
2419
+
2420
+ const workflowSaveCmd = async (name: string): Promise<CommandResult> => {
2421
+ const { existsSync, mkdirSync, writeFileSync, readFileSync } = await import('node:fs')
2422
+ const { join } = await import('node:path')
2423
+
2424
+ const targetDir = join(process.cwd(), '.claude', 'workflows')
2425
+ if (!existsSync(targetDir)) {
2426
+ mkdirSync(targetDir, { recursive: true })
2427
+ }
2428
+
2429
+ // Read the last-run state persisted by the Workflow tool
2430
+ const stateFile = join(targetDir, '.last-run.json')
2431
+ if (!existsSync(stateFile)) {
2432
+ return { content: 'No recent workflow run found. Run a workflow first with /workflow <task>.' }
2433
+ }
2434
+
2435
+ try {
2436
+ const state = JSON.parse(readFileSync(stateFile, 'utf-8'))
2437
+ const script = state.script as string
2438
+ if (!script) {
2439
+ return { content: 'No script found in last run state.' }
2440
+ }
2441
+
2442
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '-')
2443
+ const scriptPath = join(targetDir, `${safeName}.js`)
2444
+ writeFileSync(scriptPath, script, 'utf-8')
2445
+
2446
+ return {
2447
+ content: `Workflow saved to ${scriptPath}\nUse /workflow run ${safeName} to run it again.`,
2448
+ }
2449
+ } catch (err) {
2450
+ return { content: `Failed to save workflow: ${String(err)}` }
2451
+ }
2452
+ }
2453
+
2454
+ const workflowRunCmd = async (name: string): Promise<CommandResult> => {
2455
+ const { existsSync } = await import('node:fs')
2456
+ const { join } = await import('node:path')
2457
+
2458
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '-')
2459
+
2460
+ const locations = [
2461
+ join(process.cwd(), '.claude', 'workflows'),
2462
+ join(process.env.HOME || '~', '.claude', 'workflows'),
2463
+ ]
2464
+
2465
+ for (const loc of locations) {
2466
+ const scriptPath = join(loc, `${safeName}.js`)
2467
+ if (existsSync(scriptPath)) {
2468
+ return {
2469
+ content: '',
2470
+ forwardToAI:
2471
+ `Read the workflow script at ${scriptPath}, then call the Workflow tool with ` +
2472
+ `the file contents as the "script" parameter to execute it. Report the results.`,
2473
+ }
2474
+ }
2475
+ }
2476
+
2477
+ return {
2478
+ content: `Workflow "${safeName}" not found in .claude/workflows/ or ~/.claude/workflows/`,
2479
+ }
2480
+ }
2481
+
2482
+ const workflowAutoCmd: CommandHandler = async (_ctx, args) => {
2483
+ const task = args.join(' ').trim()
2484
+
2485
+ if (!task) {
2486
+ return {
2487
+ content:
2488
+ 'Usage: /workflow <task description>\n\n' +
2489
+ 'Describes the task, and the AI will generate a workflow script to execute it.\n' +
2490
+ 'Examples:\n' +
2491
+ ' /workflow audit all routes for missing auth\n' +
2492
+ ' /workflow research the impact of React 19 on our codebase\n' +
2493
+ ' /workflow find all hardcoded credentials in the codebase\n\n' +
2494
+ 'Sub-commands:\n' +
2495
+ ' /workflow save <name> — save last successful workflow script\n' +
2496
+ ' /workflow run <name> — run a saved workflow by name\n' +
2497
+ ' /workflows — list all saved workflow scripts',
2498
+ }
2499
+ }
2500
+
2501
+ // Sub-commands
2502
+ const firstArg = args[0] || ''
2503
+ if (firstArg === 'save' && args[1]) {
2504
+ return workflowSaveCmd(args.slice(1).join(' '))
2505
+ }
2506
+ if (firstArg === 'run' && args[1]) {
2507
+ return workflowRunCmd(args.slice(1).join(' '))
2508
+ }
2509
+
2510
+ // Default: forward to AI to generate + execute workflow
2511
+ return {
2512
+ content: '',
2513
+ forwardToAI:
2514
+ `Write and execute a workflow script for this task: ${task}\n\n` +
2515
+ `Use the Workflow tool to execute the generated script. ` +
2516
+ `After the workflow completes, summarize the results and offer to save the script ` +
2517
+ `with /workflow save <name> if it is reusable.`,
2518
+ }
2519
+ }
2520
+
2391
2521
  // ═══════════════════════════════════════════════════════════════
2392
2522
  // Permissions
2393
2523
  // ═══════════════════════════════════════════════════════════════
@@ -3988,8 +4118,14 @@ const forkCmd: CommandHandler = async (ctx, args) => {
3988
4118
  'general',
3989
4119
  async (_signal) => {
3990
4120
  const { SubAgent } = await import('../agent/sub-agent')
3991
- const sa = new SubAgent(ctx.engine.getRegistry(), ctx.engine.getTools())
3992
- const result = await sa.execute(prompt, 'fork: ' + prompt.slice(0, 60), {})
4121
+ const sa = new SubAgent(
4122
+ ctx.engine.getRegistry(),
4123
+ ctx.engine.getTools(),
4124
+ ctx.engine.getPermission(),
4125
+ )
4126
+ const result = await sa.execute(prompt, 'fork: ' + prompt.slice(0, 60), {
4127
+ worktreePath: wtPath,
4128
+ })
3993
4129
  try {
3994
4130
  const { execSync: ex } = await import('node:child_process')
3995
4131
  ex(`git -C ${wtPath} add -A`, { stdio: 'ignore', timeout: 10_000 })
@@ -4318,6 +4454,7 @@ registry.set('/tasks', tasksCmd)
4318
4454
  registry.set('/diff', diffCmd)
4319
4455
  registry.set('/loop', loopCmd)
4320
4456
  registry.set('/no-plan', noPlanCmd)
4457
+ registry.set('/workflow', workflowAutoCmd)
4321
4458
  registry.set('/workflows', workflowsCmd)
4322
4459
  registry.set('/review', reviewCmd)
4323
4460
  registry.set('/pr-comments', prCommentsCmd)