@miphamai/cli 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,14 @@
1
1
  import { readFileSync, existsSync, statSync } from 'node:fs'
2
- import type { ToolDefinition } from '../../shared/index.ts'
2
+ import type { ToolDefinition, CredentialMaskingConfig } from '../../shared/index.ts'
3
3
  import { resolveSafe } from '../../security/path'
4
4
 
5
+ // Injected at startup — set via index.tsx
6
+ let credentialConfig: CredentialMaskingConfig | undefined
7
+
8
+ export function setCredentialMaskingConfigForRead(config: CredentialMaskingConfig): void {
9
+ credentialConfig = config
10
+ }
11
+
5
12
  export const readTool: ToolDefinition = {
6
13
  name: 'Read',
7
14
  description: 'Read a file from the local filesystem. Supports offset and limit for large files.',
@@ -37,6 +44,28 @@ export const readTool: ToolDefinition = {
37
44
  const offset = (params.offset as number) || 0
38
45
  const limit = (params.limit as number) || 2000
39
46
  const content = readFileSync(filePath, 'utf-8')
47
+
48
+ // ── Credential masking ──
49
+ if (credentialConfig) {
50
+ const { matchCredentialFile, maskContent, CREDENTIAL_SENTINEL } =
51
+ await import('../../core/credential-masker')
52
+ const rule = matchCredentialFile(filePath, credentialConfig)
53
+ if (rule) {
54
+ const masked = maskContent(content, rule)
55
+ // Full-file mask: return sentinel immediately (offset/limit don't apply)
56
+ if (masked === CREDENTIAL_SENTINEL) {
57
+ return { success: true, content: masked }
58
+ }
59
+ // Extract mode: apply offset/limit on masked content
60
+ const maskedLines = masked.split('\n')
61
+ const maskedSlice = maskedLines.slice(offset, offset + limit)
62
+ const maskedResult = maskedSlice
63
+ .map((l, i) => `${String(offset + i + 1).padStart(6, ' ')}\t${l}`)
64
+ .join('\n')
65
+ return { success: true, content: maskedResult }
66
+ }
67
+ }
68
+
40
69
  const lines = content.split('\n')
41
70
  const slice = lines.slice(offset, offset + limit)
42
71
  const result = slice
package/src/ui/app.tsx CHANGED
@@ -372,6 +372,11 @@ export function App({
372
372
  setPickerOpen((prev) => !prev)
373
373
  return
374
374
  }
375
+ // Ctrl+F → toggle focus mode
376
+ if (_input === '\x06') {
377
+ setFocusMode((prev) => !prev)
378
+ return
379
+ }
375
380
  // Ctrl+O → toggle last tool call expand/collapse
376
381
  if (_input === '\x0f') {
377
382
  setMessages((prev) => {
@@ -480,7 +485,7 @@ export function App({
480
485
  ● {PERMISSION_LABELS[permissionMode]}
481
486
  </Text>
482
487
  <Text dimColor> · Shift+Tab mode</Text>
483
- <Text dimColor> · Ctrl+P model · Ctrl+O expand</Text>
488
+ <Text dimColor> · Ctrl+P model · Ctrl+F focus · Ctrl+O expand</Text>
484
489
  <Text dimColor> · Esc cancel · /help · ← agents</Text>
485
490
  </Box>
486
491
  </Box>
package/src/ui/chat.tsx CHANGED
@@ -18,16 +18,16 @@ function displayCwd(): string {
18
18
  }
19
19
 
20
20
  export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
21
- // In focus mode, show only the last user+assistant exchange
22
- const displayMessages = focusMode ? getLastExchange(messages) : messages
21
+ // In focus mode, compact tool activity into summary lines
22
+ const displayMessages = focusMode ? compactForFocus(messages) : messages
23
23
 
24
24
  return (
25
25
  <Box flexDirection="column" marginY={1} flexGrow={1}>
26
26
  {focusMode && messages.length > 0 && (
27
27
  <Box marginBottom={1}>
28
28
  <Text dimColor>
29
- 🔍 Focus mode — showing last exchange only ({displayMessages.length} of{' '}
30
- {messages.length} messages hidden)
29
+ 🔍 Focus — {countHidden(messages, displayMessages)} hidden · /focus to toggle · Ctrl+O
30
+ to expand
31
31
  </Text>
32
32
  </Box>
33
33
  )}
@@ -92,22 +92,55 @@ export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
92
92
  }
93
93
 
94
94
  /**
95
- * In focus mode, show only the last user→assistant exchange.
96
- * Walk backwards from the end to find the last user message, then include
97
- * everything from that point onward.
95
+ * Compact messages for focus view:
96
+ * - Groups consecutive tool calls into [▶ N tools] ↳ name1 ↳ name2 summary lines
97
+ * - Keeps all user, assistant, and system messages intact
98
98
  */
99
- function getLastExchange(messages: ChatMessage[]): ChatMessage[] {
100
- if (messages.length <= 2) return messages
99
+ function compactForFocus(messages: ChatMessage[]): ChatMessage[] {
100
+ const result: ChatMessage[] = []
101
+ let toolGroup: ChatMessage[] = []
101
102
 
102
- // Find the last user message index
103
- let lastUserIdx = -1
104
- for (let i = messages.length - 1; i >= 0; i--) {
105
- if (messages[i]!.role === 'user') {
106
- lastUserIdx = i
107
- break
103
+ for (const msg of messages) {
104
+ if (msg.toolMeta) {
105
+ toolGroup.push(msg)
106
+ } else {
107
+ // Flush pending tool group
108
+ if (toolGroup.length > 0) {
109
+ result.push(summarizeToolGroup(toolGroup))
110
+ toolGroup = []
111
+ }
112
+ result.push(msg)
108
113
  }
109
114
  }
115
+ // Flush remaining
116
+ if (toolGroup.length > 0) {
117
+ result.push(summarizeToolGroup(toolGroup))
118
+ }
119
+
120
+ return result
121
+ }
122
+
123
+ /** Summarize a group of tool calls into a single compact message. */
124
+ function summarizeToolGroup(tools: ChatMessage[]): ChatMessage {
125
+ const names = tools.map((t) => t.toolMeta!.name)
126
+ const uniqueNames = [...new Set(names)]
127
+ const toolList = uniqueNames.map((n) => `↳ ${n}`).join(' ')
128
+ return {
129
+ role: 'system',
130
+ content: `[▶ ${tools.length} tool${tools.length > 1 ? 's' : ''}] ${toolList}`,
131
+ toolMeta: {
132
+ name: `tools (${tools.length})`,
133
+ input: '',
134
+ output: tools.map((t) => t.content).join('\n'),
135
+ collapsed: true,
136
+ },
137
+ }
138
+ }
110
139
 
111
- if (lastUserIdx === -1) return messages
112
- return messages.slice(lastUserIdx)
140
+ /** Count how many messages are hidden in focus mode. */
141
+ function countHidden(all: ChatMessage[], compacted: ChatMessage[]): string {
142
+ const toolCount = all.filter((m) => m.toolMeta).length
143
+ const hidden = all.length - compacted.length
144
+ if (hidden <= 0) return `showing all ${all.length} messages`
145
+ return `${hidden} tool outputs folded (${toolCount} total tool calls)`
113
146
  }
@@ -2388,6 +2388,111 @@ const workflowsCmd: CommandHandler = async () => {
2388
2388
  return { content: lines.join('\n') }
2389
2389
  }
2390
2390
 
2391
+ // ═══════════════════════════════════════════════════════════════
2392
+ // /workflow <task> — auto-generate + execute
2393
+ // ═══════════════════════════════════════════════════════════════
2394
+
2395
+ const workflowSaveCmd = async (name: string): Promise<CommandResult> => {
2396
+ const { existsSync, mkdirSync, writeFileSync, readFileSync } = await import('node:fs')
2397
+ const { join } = await import('node:path')
2398
+
2399
+ const targetDir = join(process.cwd(), '.claude', 'workflows')
2400
+ if (!existsSync(targetDir)) {
2401
+ mkdirSync(targetDir, { recursive: true })
2402
+ }
2403
+
2404
+ // Read the last-run state persisted by the Workflow tool
2405
+ const stateFile = join(targetDir, '.last-run.json')
2406
+ if (!existsSync(stateFile)) {
2407
+ return { content: 'No recent workflow run found. Run a workflow first with /workflow <task>.' }
2408
+ }
2409
+
2410
+ try {
2411
+ const state = JSON.parse(readFileSync(stateFile, 'utf-8'))
2412
+ const script = state.script as string
2413
+ if (!script) {
2414
+ return { content: 'No script found in last run state.' }
2415
+ }
2416
+
2417
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '-')
2418
+ const scriptPath = join(targetDir, `${safeName}.js`)
2419
+ writeFileSync(scriptPath, script, 'utf-8')
2420
+
2421
+ return {
2422
+ content: `Workflow saved to ${scriptPath}\nUse /workflow run ${safeName} to run it again.`,
2423
+ }
2424
+ } catch (err) {
2425
+ return { content: `Failed to save workflow: ${String(err)}` }
2426
+ }
2427
+ }
2428
+
2429
+ const workflowRunCmd = async (name: string): Promise<CommandResult> => {
2430
+ const { existsSync } = await import('node:fs')
2431
+ const { join } = await import('node:path')
2432
+
2433
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '-')
2434
+
2435
+ const locations = [
2436
+ join(process.cwd(), '.claude', 'workflows'),
2437
+ join(process.env.HOME || '~', '.claude', 'workflows'),
2438
+ ]
2439
+
2440
+ for (const loc of locations) {
2441
+ const scriptPath = join(loc, `${safeName}.js`)
2442
+ if (existsSync(scriptPath)) {
2443
+ return {
2444
+ content: '',
2445
+ forwardToAI:
2446
+ `Read the workflow script at ${scriptPath}, then call the Workflow tool with ` +
2447
+ `the file contents as the "script" parameter to execute it. Report the results.`,
2448
+ }
2449
+ }
2450
+ }
2451
+
2452
+ return {
2453
+ content: `Workflow "${safeName}" not found in .claude/workflows/ or ~/.claude/workflows/`,
2454
+ }
2455
+ }
2456
+
2457
+ const workflowAutoCmd: CommandHandler = async (_ctx, args) => {
2458
+ const task = args.join(' ').trim()
2459
+
2460
+ if (!task) {
2461
+ return {
2462
+ content:
2463
+ 'Usage: /workflow <task description>\n\n' +
2464
+ 'Describes the task, and the AI will generate a workflow script to execute it.\n' +
2465
+ 'Examples:\n' +
2466
+ ' /workflow audit all routes for missing auth\n' +
2467
+ ' /workflow research the impact of React 19 on our codebase\n' +
2468
+ ' /workflow find all hardcoded credentials in the codebase\n\n' +
2469
+ 'Sub-commands:\n' +
2470
+ ' /workflow save <name> — save last successful workflow script\n' +
2471
+ ' /workflow run <name> — run a saved workflow by name\n' +
2472
+ ' /workflows — list all saved workflow scripts',
2473
+ }
2474
+ }
2475
+
2476
+ // Sub-commands
2477
+ const firstArg = args[0] || ''
2478
+ if (firstArg === 'save' && args[1]) {
2479
+ return workflowSaveCmd(args.slice(1).join(' '))
2480
+ }
2481
+ if (firstArg === 'run' && args[1]) {
2482
+ return workflowRunCmd(args.slice(1).join(' '))
2483
+ }
2484
+
2485
+ // Default: forward to AI to generate + execute workflow
2486
+ return {
2487
+ content: '',
2488
+ forwardToAI:
2489
+ `Write and execute a workflow script for this task: ${task}\n\n` +
2490
+ `Use the Workflow tool to execute the generated script. ` +
2491
+ `After the workflow completes, summarize the results and offer to save the script ` +
2492
+ `with /workflow save <name> if it is reusable.`,
2493
+ }
2494
+ }
2495
+
2391
2496
  // ═══════════════════════════════════════════════════════════════
2392
2497
  // Permissions
2393
2498
  // ═══════════════════════════════════════════════════════════════
@@ -3052,6 +3157,223 @@ Permission level is controlled by /config permission <level>.`,
3052
3157
  // Security / Audit — security review checklist
3053
3158
  // ═══════════════════════════════════════════════════════════════
3054
3159
 
3160
+ const promptAuditCmd: CommandHandler = async () => {
3161
+ const { existsSync, readFileSync, readdirSync, statSync } = await import('node:fs')
3162
+ const { join, extname, basename } = await import('node:path')
3163
+ const cwd = process.cwd()
3164
+
3165
+ // Patterns that suggest prompts written for older/less-capable models
3166
+ const AUDIT_RULES: Array<{
3167
+ id: string
3168
+ pattern: RegExp
3169
+ severity: 'low' | 'medium' | 'high'
3170
+ message: string
3171
+ suggestion: string
3172
+ }> = [
3173
+ {
3174
+ id: 'over-explaining',
3175
+ pattern:
3176
+ /you are (a|an) (helpful |friendly |knowledgeable )?(AI |language model |assistant)/i,
3177
+ severity: 'low',
3178
+ message: 'Self-identification preamble ("You are a...")',
3179
+ suggestion: "Modern models don't need role preamble. Remove or shorten to 1 line.",
3180
+ },
3181
+ {
3182
+ id: 'step-by-step',
3183
+ pattern: /(think|reason|work) (step[ -]?by[ -]?step|through this|carefully about this)/i,
3184
+ severity: 'low',
3185
+ message: 'Step-by-step reasoning prompt',
3186
+ suggestion: 'Reasoning models handle this natively. Remove for token savings.',
3187
+ },
3188
+ {
3189
+ id: 'do-not-list',
3190
+ pattern:
3191
+ /(do not|never|you must not|don't|under no circumstances).*\n.*(do not|never|you must not|don't)/i,
3192
+ severity: 'medium',
3193
+ message: 'Multiple "do not" constraints',
3194
+ suggestion: 'Modern models need fewer prohibitions. Consolidate to 1-2 key constraints.',
3195
+ },
3196
+ {
3197
+ id: 'token-waste',
3198
+ pattern:
3199
+ /(remember|keep in mind|it is important to note|please note that|i want to emphasize)/i,
3200
+ severity: 'low',
3201
+ message: 'Filler emphasis phrases ("remember", "please note")',
3202
+ suggestion: 'Remove filler. State the instruction directly.',
3203
+ },
3204
+ {
3205
+ id: 'format-over-spec',
3206
+ pattern: /(you must (always |only )?respond (in|with|using) (JSON|XML|YAML|markdown|HTML))/i,
3207
+ severity: 'low',
3208
+ message: 'Over-specified output format',
3209
+ suggestion: 'Modern models follow format instructions with fewer words. Shorten.',
3210
+ },
3211
+ {
3212
+ id: 'example-overload',
3213
+ pattern: /((?:example|for instance|e\.g\.,).*\n){3,}/i,
3214
+ severity: 'low',
3215
+ message: 'Three or more examples in sequence',
3216
+ suggestion: '1-2 examples suffice for modern models. Remove redundant ones.',
3217
+ },
3218
+ {
3219
+ id: 'old-model-ref',
3220
+ pattern: /(GPT-3|GPT-3\.5|Claude\s*[12][^3]|text-davinci|command-r|llama\s*2)/i,
3221
+ severity: 'high',
3222
+ message: 'Reference to older model',
3223
+ suggestion: 'Update model references to current generation (Sonnet 5, Opus 5, GPT-5, etc.).',
3224
+ },
3225
+ {
3226
+ id: 'long-preamble',
3227
+ pattern: /^.{200,}$/m,
3228
+ severity: 'medium',
3229
+ message: 'Very long single-line instructions (200+ chars)',
3230
+ suggestion: 'Break into bullet points. Modern models parse structured prompts better.',
3231
+ },
3232
+ {
3233
+ id: 'verbose-constraint',
3234
+ pattern:
3235
+ /you (must|should|shall) (always |only |never )?(ensure|guarantee|verify|validate|confirm|cross-check|double-check)/i,
3236
+ severity: 'medium',
3237
+ message: 'Overly verbose constraint language',
3238
+ suggestion: 'Replace "you must ensure that" with imperative: "Ensure".',
3239
+ },
3240
+ ]
3241
+
3242
+ // Files to scan
3243
+ const scanDirs = [
3244
+ join(cwd, '.mipham', 'skills'),
3245
+ join(cwd, '.mipham', 'rules'),
3246
+ join(cwd, '.claude'),
3247
+ ]
3248
+ const skillFiles: string[] = []
3249
+ // Also check standard project files
3250
+ const projectFiles = [join(cwd, 'CLAUDE.md'), join(cwd, 'MIPHAM.md'), join(cwd, 'PRODUCT.md')]
3251
+
3252
+ // Collect skill/rules files
3253
+ for (const dir of scanDirs) {
3254
+ try {
3255
+ if (!existsSync(dir)) continue
3256
+ const walk = (d: string) => {
3257
+ const entries = readdirSync(d)
3258
+ for (const e of entries) {
3259
+ const fp = join(d, e)
3260
+ const st = statSync(fp)
3261
+ if (st.isDirectory()) {
3262
+ walk(fp)
3263
+ continue
3264
+ }
3265
+ if (['.md', '.SKILL.md', '.mipham-skill.md', '.txt'].some((ext) => fp.endsWith(ext))) {
3266
+ skillFiles.push(fp)
3267
+ }
3268
+ }
3269
+ }
3270
+ walk(dir)
3271
+ } catch {
3272
+ /* skip */
3273
+ }
3274
+ }
3275
+
3276
+ const allFiles = [...new Set([...projectFiles.filter((f) => existsSync(f)), ...skillFiles])]
3277
+
3278
+ if (allFiles.length === 0) {
3279
+ return {
3280
+ content: [
3281
+ '🔍 Prompt Audit',
3282
+ '',
3283
+ 'No prompt files found to audit.',
3284
+ 'Place skills in .mipham/skills/ and rules in .mipham/rules/.',
3285
+ ].join('\n'),
3286
+ }
3287
+ }
3288
+
3289
+ // Scan each file
3290
+ const results: Array<{
3291
+ file: string
3292
+ line: number
3293
+ ruleId: string
3294
+ severity: string
3295
+ message: string
3296
+ suggestion: string
3297
+ }> = []
3298
+
3299
+ for (const fp of allFiles) {
3300
+ try {
3301
+ const content = readFileSync(fp, 'utf-8')
3302
+ const lines = content.split('\n')
3303
+ for (const rule of AUDIT_RULES) {
3304
+ // Multi-line pattern: check full content
3305
+ if (rule.id === 'do-not-list' || rule.id === 'example-overload') {
3306
+ if (rule.pattern.test(content)) {
3307
+ // Find approx line
3308
+ const match = rule.pattern.exec(content)
3309
+ const idx = match ? content.slice(0, match.index).split('\n').length : 1
3310
+ results.push({
3311
+ file: fp.replace(cwd + '/', ''),
3312
+ line: idx,
3313
+ ruleId: rule.id,
3314
+ severity: rule.severity,
3315
+ message: rule.message,
3316
+ suggestion: rule.suggestion,
3317
+ })
3318
+ }
3319
+ continue
3320
+ }
3321
+
3322
+ // Line-by-line patterns
3323
+ for (let i = 0; i < lines.length; i++) {
3324
+ const line = lines[i]!
3325
+ if (rule.pattern.test(line)) {
3326
+ results.push({
3327
+ file: fp.replace(cwd + '/', ''),
3328
+ line: i + 1,
3329
+ ruleId: rule.id,
3330
+ severity: rule.severity,
3331
+ message: rule.message,
3332
+ suggestion: rule.suggestion,
3333
+ })
3334
+ }
3335
+ }
3336
+ }
3337
+ } catch {
3338
+ /* skip unreadable files */
3339
+ }
3340
+ }
3341
+
3342
+ // Format output
3343
+ const severityIcon: Record<string, string> = { high: '🔴', medium: '🟡', low: '🟢' }
3344
+ const grouped: Record<string, typeof results> = {}
3345
+ for (const r of results) {
3346
+ const key = r.file
3347
+ if (!grouped[key]) grouped[key] = []
3348
+ grouped[key]!.push(r)
3349
+ }
3350
+
3351
+ const output: string[] = [
3352
+ `🔍 Prompt Audit — ${allFiles.length} file(s) scanned, ${results.length} finding(s)`,
3353
+ '',
3354
+ ]
3355
+
3356
+ if (results.length === 0) {
3357
+ output.push('✅ All prompts look optimized for modern models. No issues found.')
3358
+ } else {
3359
+ for (const [file, items] of Object.entries(grouped)) {
3360
+ output.push(`📄 ${file} (${items!.length} finding${items!.length > 1 ? 's' : ''})`)
3361
+ for (const item of items!) {
3362
+ output.push(
3363
+ ` ${severityIcon[item.severity] || '⚪'} L${item.line}: [${item.ruleId}] ${item.message}`,
3364
+ )
3365
+ output.push(` → ${item.suggestion}`)
3366
+ }
3367
+ output.push('')
3368
+ }
3369
+ }
3370
+
3371
+ output.push('─'.repeat(60))
3372
+ output.push('Scanned: ' + allFiles.map((f) => basename(f)).join(', '))
3373
+
3374
+ return { content: output.join('\n') }
3375
+ }
3376
+
3055
3377
  const securityCmd: CommandHandler = async () => {
3056
3378
  const findings: string[] = []
3057
3379
  const ok: string[] = []
@@ -3691,6 +4013,128 @@ Run \`mipham agents\` for the interactive dashboard.`,
3691
4013
  // Artifacts
3692
4014
  // ═══════════════════════════════════════════════════════════════
3693
4015
 
4016
+ const forkCmd: CommandHandler = async (ctx, args) => {
4017
+ const prompt = args.join(' ')
4018
+ if (!prompt.trim()) {
4019
+ return {
4020
+ content: [
4021
+ 'Usage: /fork <task description>',
4022
+ '',
4023
+ 'Fork a task into an isolated worktree with auto commit/push.',
4024
+ '',
4025
+ 'Workflow:',
4026
+ ' 1. Creates a new git worktree on branch worktree/<slug>',
4027
+ ' 2. Spawns a background agent to work on the task',
4028
+ ' 3. Auto-commits changes with Co-Authored-By on completion',
4029
+ ' 4. Auto-pushes to origin',
4030
+ ' 5. Reports branch and worktree path in the result',
4031
+ '',
4032
+ 'Examples:',
4033
+ ' /fork Refactor the auth module',
4034
+ ' /fork Add input validation to all API endpoints',
4035
+ ].join('\n'),
4036
+ }
4037
+ }
4038
+
4039
+ const { execSync } = await import('node:child_process')
4040
+ const { join } = await import('node:path')
4041
+
4042
+ try {
4043
+ execSync('git rev-parse --git-dir', { stdio: 'ignore' })
4044
+ } catch {
4045
+ return { content: '/fork requires a git repository.' }
4046
+ }
4047
+
4048
+ const agentViewManager = ctx.engine.getAgentViewManager?.()
4049
+ if (!agentViewManager) {
4050
+ return { content: 'Agent View manager is initializing...' }
4051
+ }
4052
+
4053
+ const slug = prompt
4054
+ .toLowerCase()
4055
+ .replace(/[^a-z0-9]+/g, '-')
4056
+ .replace(/^-|-$/g, '')
4057
+ .slice(0, 40)
4058
+ const name = `${slug}-${Date.now().toString(36)}`
4059
+ const branch = `worktree/${name}`
4060
+ const wtPath = join(process.cwd(), '.claude', 'worktrees', name)
4061
+
4062
+ try {
4063
+ execSync(`git worktree add -b ${branch} ${wtPath} HEAD`, { stdio: 'ignore', timeout: 30_000 })
4064
+ } catch (err) {
4065
+ try {
4066
+ execSync(`git worktree remove --force ${wtPath}`, { stdio: 'ignore' })
4067
+ } catch {
4068
+ /* ok */
4069
+ }
4070
+ try {
4071
+ execSync(`git branch -D ${branch}`, { stdio: 'ignore' })
4072
+ } catch {
4073
+ /* ok */
4074
+ }
4075
+ return {
4076
+ content: `Worktree creation failed: ${err instanceof Error ? err.message : String(err)}`,
4077
+ }
4078
+ }
4079
+
4080
+ const session = agentViewManager.create(prompt, prompt, {
4081
+ provider: ctx.providerId,
4082
+ model: ctx.modelId,
4083
+ })
4084
+ agentViewManager.addMessage(session.id, { role: 'user', content: prompt })
4085
+ agentViewManager.updateStatus(session.id, 'working')
4086
+ session.worktree = wtPath
4087
+ session.branch = branch
4088
+ session.kind = 'forked'
4089
+
4090
+ const bgReg = (await import('../agent/background-registry')).getBackgroundAgentRegistry()
4091
+ bgReg.spawn(
4092
+ prompt,
4093
+ 'general',
4094
+ async (_signal) => {
4095
+ const { SubAgent } = await import('../agent/sub-agent')
4096
+ const sa = new SubAgent(ctx.engine.getRegistry(), ctx.engine.getTools())
4097
+ const result = await sa.execute(prompt, 'fork: ' + prompt.slice(0, 60), {})
4098
+ try {
4099
+ const { execSync: ex } = await import('node:child_process')
4100
+ ex(`git -C ${wtPath} add -A`, { stdio: 'ignore', timeout: 10_000 })
4101
+ const st = ex(`git -C ${wtPath} status --porcelain`, { encoding: 'utf-8', timeout: 10_000 })
4102
+ if (st.trim()) {
4103
+ ex(
4104
+ `git -C ${wtPath} commit -m "feat: ${prompt.slice(0, 60)}\n\nCo-Authored-By: Claude <noreply@anthropic.com>"`,
4105
+ { stdio: 'ignore', timeout: 10_000 },
4106
+ )
4107
+ ex(`git push origin ${branch}`, { stdio: 'ignore', timeout: 30_000 })
4108
+ }
4109
+ } catch {
4110
+ /* best-effort */
4111
+ }
4112
+ return [
4113
+ `## Fork done: ${prompt}`,
4114
+ '',
4115
+ `Branch: \`${branch}\``,
4116
+ `Worktree: \`${wtPath}\``,
4117
+ '',
4118
+ result || '(no output)',
4119
+ ].join('\n')
4120
+ },
4121
+ 'forked',
4122
+ )
4123
+
4124
+ return {
4125
+ content: [
4126
+ `✓ Forked: ${session.id}`,
4127
+ ` Task: ${prompt.slice(0, 80)}`,
4128
+ ` Branch: ${branch}`,
4129
+ ` Worktree: ${wtPath}`,
4130
+ ` Status: working`,
4131
+ '',
4132
+ 'Agent will auto-commit + push on completion.',
4133
+ 'Use /agents to track.',
4134
+ ].join('\n'),
4135
+ }
4136
+ }
4137
+
3694
4138
  async function openBrowser(url: string): Promise<void> {
3695
4139
  const cmd =
3696
4140
  process.platform === 'darwin'
@@ -3857,6 +4301,7 @@ const commandsListCmd: CommandHandler = () => {
3857
4301
  '/recommend': 'Project',
3858
4302
  '/security': 'Project',
3859
4303
  '/audit': 'Project',
4304
+ '/prompt-audit': 'Code Quality',
3860
4305
  '/ide': 'Environment',
3861
4306
  '/terminal-setup': 'Environment',
3862
4307
  '/memory': 'Environment',
@@ -3866,6 +4311,7 @@ const commandsListCmd: CommandHandler = () => {
3866
4311
  '/feedback': 'Account',
3867
4312
  '/agents': 'Agents',
3868
4313
  '/bg': 'Agents',
4314
+ '/fork': 'Agents',
3869
4315
  '/artifact': 'Artifacts',
3870
4316
  '/schedule': 'Other',
3871
4317
  '/commit': 'Workflow',
@@ -3977,6 +4423,7 @@ registry.set('/tasks', tasksCmd)
3977
4423
  registry.set('/diff', diffCmd)
3978
4424
  registry.set('/loop', loopCmd)
3979
4425
  registry.set('/no-plan', noPlanCmd)
4426
+ registry.set('/workflow', workflowAutoCmd)
3980
4427
  registry.set('/workflows', workflowsCmd)
3981
4428
  registry.set('/review', reviewCmd)
3982
4429
  registry.set('/pr-comments', prCommentsCmd)
@@ -3996,6 +4443,7 @@ registry.set('/permissions', permissionsCmd)
3996
4443
  registry.set('/add-dir', addDirCmd)
3997
4444
  registry.set('/security', securityCmd)
3998
4445
  registry.set('/audit', securityCmd)
4446
+ registry.set('/prompt-audit', promptAuditCmd)
3999
4447
 
4000
4448
  // Environment
4001
4449
  registry.set('/theme', themeCmd)
@@ -4010,6 +4458,7 @@ registry.set('/logout', logoutCmd)
4010
4458
  registry.set('/feedback', feedbackCmd)
4011
4459
  registry.set('/agents', agentsCmd)
4012
4460
  registry.set('/bg', bgCmd)
4461
+ registry.set('/fork', forkCmd)
4013
4462
 
4014
4463
  // Artifacts
4015
4464
  registry.set('/artifact', artifactBaseCmd)
@@ -4119,6 +4568,7 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
4119
4568
  '/recommend': 'Analyze project + recommend skills & setup',
4120
4569
  '/security': 'Security review checklist',
4121
4570
  '/audit': 'Same as /security',
4571
+ '/prompt-audit': 'Audit prompts for modern model optimization',
4122
4572
  '/ide': 'IDE integration guide',
4123
4573
  '/terminal-setup': 'Shell & terminal config',
4124
4574
  '/memory': 'Manage AI memories',
@@ -4129,6 +4579,7 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
4129
4579
  '/feedback': 'Send feedback',
4130
4580
  '/agents': 'Agent view dashboard',
4131
4581
  '/bg': 'Run a background agent task',
4582
+ '/fork': 'Fork task into isolated worktree',
4132
4583
  '/artifact': 'Manage artifacts',
4133
4584
  '/schedule': 'View scheduled tasks and cron jobs',
4134
4585
  '/commit': 'Generate commit message and review staged changes',