@shawnstack/quickforge 1.4.0 → 1.4.1

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.
Files changed (37) hide show
  1. package/README.md +12 -12
  2. package/dist/assets/{AgentProfilesPage-C79teCgh.js → AgentProfilesPage-CNK5PxA3.js} +1 -1
  3. package/dist/assets/ChatPanelHost-FqPQwwMO.js +217 -0
  4. package/dist/assets/PluginsPage-BCu1Ept0.js +1 -0
  5. package/dist/assets/{ScheduledTasksPage-C047y3p3.js → ScheduledTasksPage-Bx04rjui.js} +2 -2
  6. package/dist/assets/SharedConversationPage-55vX9sqe.js +1 -0
  7. package/dist/assets/TerminalDock-DLN_pLkJ.js +2 -0
  8. package/dist/assets/WorkspaceInspector-DoemHHnY.js +3 -0
  9. package/dist/assets/{WorkspaceReaderDialog-bTeERaGd.js → WorkspaceReaderDialog-C6xUHBCw.js} +2 -2
  10. package/dist/assets/{icons-Dsc5yL3l.js → icons-BWtivFsx.js} +1 -1
  11. package/dist/assets/index-CxOHP41X.css +3 -0
  12. package/dist/assets/index-Dcf73EL8.js +895 -0
  13. package/dist/assets/{monaco-DG4TcBMc.js → monaco-evITXh-m.js} +1 -1
  14. package/dist/assets/{react-vendor-CiCXOLb5.js → react-vendor-Mthyt1p4.js} +1 -1
  15. package/dist/index.html +4 -4
  16. package/package.json +1 -1
  17. package/server/agent-manager.mjs +85 -13
  18. package/server/approval-store.mjs +13 -1
  19. package/server/auto-compaction.mjs +29 -73
  20. package/server/context-usage.mjs +108 -0
  21. package/server/custom-commands.mjs +145 -28
  22. package/server/mcp/registry.mjs +40 -0
  23. package/server/routes/agent.mjs +1 -1
  24. package/server/routes/mcp.mjs +7 -1
  25. package/server/routes/project.mjs +32 -2
  26. package/server/routes/shared-conversation.mjs +1 -1
  27. package/server/storage.mjs +1 -0
  28. package/server/subagents.mjs +8 -6
  29. package/server/system-prompt.mjs +2 -2
  30. package/server/tools/definitions.mjs +1 -1
  31. package/dist/assets/ChatPanelHost-BjdIshtX.js +0 -195
  32. package/dist/assets/PluginsPage-Dt7Iiddo.js +0 -1
  33. package/dist/assets/SharedConversationPage-8X8kfztQ.js +0 -1
  34. package/dist/assets/TerminalDock-CEuJNf0m.js +0 -2
  35. package/dist/assets/WorkspaceInspector-BIa5gLVs.js +0 -3
  36. package/dist/assets/index-CPAWYhzz.css +0 -3
  37. package/dist/assets/index-YTL26wyJ.js +0 -814
@@ -1,11 +1,59 @@
1
1
  import { promises as fs } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { getEnabledPluginCommandSources } from './plugins/registry.mjs'
4
+ import { userCommandsDir } from './storage.mjs'
4
5
 
5
6
  const commandsRelativeDirs = ['.claude/commands', '.opencode/commands', '.ai/commands']
6
7
  const commandsRelativeDir = '.ai/commands'
7
8
  const commandNamePattern = /^(?!.*--)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/
8
9
 
10
+ /**
11
+ * Centralized metadata for all built-in slash commands.
12
+ * This is the single source of truth used by /help output.
13
+ * Front-end i18n descriptions (i18n.ts) should be kept in sync with the
14
+ * `description` values here.
15
+ */
16
+ const builtinCommandCatalog = [
17
+ {
18
+ name: 'plan',
19
+ description: 'Create a plan first; this turn cannot edit files or run commands.',
20
+ argumentHint: '[task]',
21
+ permissionNote: 'read-only',
22
+ },
23
+ {
24
+ name: 'review',
25
+ description: 'Review pending code changes before commit; this turn cannot edit files.',
26
+ argumentHint: '[scope]',
27
+ permissionNote: 'no edits',
28
+ },
29
+ {
30
+ name: 'compact',
31
+ description: 'Create a new chat with this conversation compacted to reduce context usage.',
32
+ argumentHint: '',
33
+ },
34
+ {
35
+ name: 'clear',
36
+ description: 'Clear the current chat history and context without calling the model.',
37
+ argumentHint: '',
38
+ },
39
+ {
40
+ name: 'commands',
41
+ description: 'List custom commands (project, user-level, and plugin).',
42
+ argumentHint: '',
43
+ },
44
+ {
45
+ name: 'command new',
46
+ description: 'Create a project custom command template.',
47
+ argumentHint: '<name>',
48
+ },
49
+ {
50
+ name: 'help',
51
+ aliases: ['?'],
52
+ description: 'Show available commands and their usage.',
53
+ argumentHint: '',
54
+ },
55
+ ]
56
+
9
57
  function normalizeCommandName(value) {
10
58
  const name = String(value || '').trim().toLowerCase()
11
59
  return commandNamePattern.test(name) ? name : null
@@ -103,7 +151,7 @@ function firstOptionalBoolean(...values) {
103
151
  return undefined
104
152
  }
105
153
 
106
- function commandFromFile(file, text, options = {}) {
154
+ export function commandFromFile(file, text, options = {}) {
107
155
  const parsed = parseFrontmatter(text)
108
156
  if (!parsed.body) return null
109
157
 
@@ -229,13 +277,27 @@ async function listPluginCommands(workspaceRoot) {
229
277
  return commands
230
278
  }
231
279
 
280
+ async function listUserCommands() {
281
+ return listCommandsFromDirectory(userCommandsDir, {
282
+ source: 'user',
283
+ relativeRoot: '~/.quickforge/commands',
284
+ })
285
+ }
286
+
232
287
  export async function listProjectCommands(workspaceRoot, commandDir) {
233
288
  const byName = new Map()
234
289
 
290
+ // 1. Plugin commands (lowest priority)
235
291
  for (const command of (await listPluginCommands(workspaceRoot)).sort((a, b) => a.name.localeCompare(b.name))) {
236
292
  byName.set(command.name, command)
237
293
  }
238
294
 
295
+ // 2. User-level commands (~/.quickforge/commands/) — override plugins, overridden by project
296
+ for (const command of (await listUserCommands()).sort((a, b) => a.name.localeCompare(b.name))) {
297
+ byName.set(command.name, command)
298
+ }
299
+
300
+ // 3. Project directories: .claude → .opencode → .ai → configured (highest priority)
239
301
  for (const dir of commandDirectories(workspaceRoot, commandDir)) {
240
302
  const commands = await listCommandsFromDirectory(dir)
241
303
  for (const command of commands.sort((a, b) => a.name.localeCompare(b.name))) {
@@ -297,6 +359,7 @@ function escapeXml(value) {
297
359
 
298
360
  export function parseInternalCommandInvocation(message) {
299
361
  const text = textFromUserMessage(message).trim()
362
+ if (/^\/(?:help|\?)(?:\s+.*)?$/i.test(text)) return { type: 'help' }
300
363
  if (/^\/commands(?:\s+.*)?$/i.test(text)) return { type: 'list' }
301
364
  if (/^\/clear\s*$/i.test(text)) return { type: 'clear' }
302
365
  if (/^\/clear(?:\s+[\s\S]+)$/i.test(text)) return { type: 'invalid-clear-args' }
@@ -344,14 +407,19 @@ export async function handleInternalCommand(invocation, workspaceRoot, commandDi
344
407
  return 'Usage: /clear'
345
408
  }
346
409
 
347
- if (!workspaceRoot) {
348
- return 'Custom commands require an active project chat.'
410
+ if (invocation.type === 'help') {
411
+ return formatHelpText(await listProjectCommands(workspaceRoot, commandDir))
349
412
  }
350
413
 
414
+ // /commands and /help work without a project (user-level commands are global)
351
415
  if (invocation.type === 'list') {
352
416
  return formatCommandList(await listProjectCommands(workspaceRoot, commandDir))
353
417
  }
354
418
 
419
+ if (!workspaceRoot) {
420
+ return 'Custom commands require an active project chat.'
421
+ }
422
+
355
423
  if (invocation.type === 'new') {
356
424
  return createCommandTemplate(workspaceRoot, invocation.name)
357
425
  }
@@ -367,17 +435,17 @@ function formatPermission(value) {
367
435
  return value === false ? 'false' : 'true'
368
436
  }
369
437
 
370
- function formatCommandList(commands) {
438
+ export function formatCommandList(commands) {
371
439
  if (commands.length === 0) {
372
440
  return [
373
- 'No project custom commands found.',
441
+ 'No custom commands found.',
374
442
  '',
375
443
  'Create one with:',
376
444
  '```text',
377
445
  '/command new review',
378
446
  '```',
379
447
  '',
380
- 'Or add Markdown files under `.claude/commands/`, `.opencode/commands/`, or `.ai/commands/`, for example `.ai/commands/review.md`.',
448
+ 'Or add Markdown files under `~/.quickforge/commands/` (user-level), or `.claude/commands/`, `.opencode/commands/`, `.ai/commands/` (project-level), for example `.ai/commands/review.md`.',
381
449
  ].join('\n')
382
450
  }
383
451
 
@@ -389,37 +457,59 @@ function formatCommandList(commands) {
389
457
  })
390
458
 
391
459
  return [
392
- 'Project custom commands:',
460
+ 'Custom commands:',
393
461
  '',
394
462
  ...rows,
395
463
  '',
396
- 'Command files live in `.claude/commands/*.md`, `.opencode/commands/*.md`, `.ai/commands/*.md`, or configured directories. Use `$ARGUMENTS` inside a command file to insert invocation arguments.',
464
+ 'Command files live in `~/.quickforge/commands/*.md` (user-level), `.claude/commands/*.md`, `.opencode/commands/*.md`, `.ai/commands/*.md`, or configured directories. Use `$ARGUMENTS` inside a command file to insert invocation arguments.',
397
465
  ].join('\n')
398
466
  }
399
467
 
400
- async function createCommandTemplate(workspaceRoot, name) {
401
- const commandName = normalizeCommandName(name)
402
- if (!commandName) {
403
- return `Invalid command name: ${name}\n\nUse lowercase letters, numbers, and hyphens.`
404
- }
468
+ function formatBuiltinCommandRows() {
469
+ return builtinCommandCatalog.map((cmd) => {
470
+ const hint = cmd.argumentHint ? ` ${cmd.argumentHint}` : ''
471
+ const aliases = cmd.aliases?.length
472
+ ? ` (alias: ${cmd.aliases.map((alias) => `/${alias}`).join(', ')})`
473
+ : ''
474
+ const perm = cmd.permissionNote ? ` \[${cmd.permissionNote}\]` : ''
475
+ return `- \`/${cmd.name}${hint}\`${aliases} — ${cmd.description}${perm}`
476
+ })
477
+ }
405
478
 
406
- const dir = commandDirectory(workspaceRoot)
407
- const file = path.join(dir, `${commandName}.md`)
408
- try {
409
- await fs.mkdir(dir, { recursive: true })
410
- await fs.writeFile(file, commandTemplate(commandName), { encoding: 'utf8', flag: 'wx' })
411
- } catch (error) {
412
- if (error?.code === 'EEXIST') {
413
- return `Custom command already exists: ${commandsRelativeDir}/${commandName}.md`
414
- }
415
- throw error
479
+ export function formatHelpText(customCommands = []) {
480
+ const sections = [
481
+ 'QuickForge command reference',
482
+ '',
483
+ 'Built-in commands:',
484
+ '',
485
+ ...formatBuiltinCommandRows(),
486
+ ]
487
+
488
+ if (customCommands.length > 0) {
489
+ sections.push('', formatCommandList(customCommands))
490
+ } else {
491
+ sections.push(
492
+ '',
493
+ 'No custom commands found. Add Markdown files under `~/.quickforge/commands/` (user-level) or `.claude/commands/`, `.opencode/commands/`, `.ai/commands/` (project-level).',
494
+ )
416
495
  }
417
496
 
418
- return [
419
- `Created custom command: ${commandsRelativeDir}/${commandName}.md`,
420
- '',
421
- `Run it with: /${commandName} your arguments`,
422
- ].join('\n')
497
+ return sections.join('\n')
498
+ }
499
+
500
+ async function createCommandTemplate(workspaceRoot, name) {
501
+ const result = await createCommandFile(workspaceRoot, name)
502
+ if (result.ok) {
503
+ return [
504
+ `Created custom command: ${result.relativePath}`,
505
+ '',
506
+ `Run it with: /${result.name} your arguments`,
507
+ ].join('\n')
508
+ }
509
+ if (result.reason === 'exists') {
510
+ return `Custom command already exists: ${commandsRelativeDir}/${result.name}.md`
511
+ }
512
+ return `Invalid command name: ${name}\n\nUse lowercase letters, numbers, and hyphens.`
423
513
  }
424
514
 
425
515
  function commandTemplate(name) {
@@ -436,3 +526,30 @@ allow_commands: false
436
526
  $ARGUMENTS
437
527
  `
438
528
  }
529
+
530
+ export async function createCommandFile(workspaceRoot, name) {
531
+ const commandName = normalizeCommandName(name)
532
+ if (!commandName) {
533
+ return { ok: false, reason: 'invalid' }
534
+ }
535
+ const dir = commandDirectory(workspaceRoot)
536
+ if (!dir) {
537
+ return { ok: false, reason: 'no-project' }
538
+ }
539
+ const file = path.join(dir, `${commandName}.md`)
540
+ try {
541
+ await fs.mkdir(dir, { recursive: true })
542
+ await fs.writeFile(file, commandTemplate(commandName), { encoding: 'utf8', flag: 'wx' })
543
+ } catch (error) {
544
+ if (error?.code === 'EEXIST') {
545
+ return { ok: false, reason: 'exists', name: commandName, filePath: file }
546
+ }
547
+ throw error
548
+ }
549
+ return {
550
+ ok: true,
551
+ name: commandName,
552
+ filePath: file,
553
+ relativePath: `${commandsRelativeDir}/${commandName}.md`,
554
+ }
555
+ }
@@ -231,6 +231,46 @@ export async function refreshMcpConnections() {
231
231
  return refreshPromise
232
232
  }
233
233
 
234
+ export async function reconnectMcpServer(name) {
235
+ const normalizedName = String(name || '').trim()
236
+ // close any existing connection first (bypasses retry backoff)
237
+ const existing = connections.get(normalizedName)
238
+ if (existing) {
239
+ connections.delete(normalizedName)
240
+ await closeConnection(existing)
241
+ }
242
+ const servers = await readMcpServers()
243
+ const config = servers.find((server) => server.name === normalizedName)
244
+ if (!config) {
245
+ const error = new Error(`MCP server not found: ${normalizedName}`)
246
+ error.statusCode = 404
247
+ throw error
248
+ }
249
+ if (!config.enabled) {
250
+ const error = new Error(`MCP server is disabled: ${normalizedName}`)
251
+ error.statusCode = 400
252
+ throw error
253
+ }
254
+ try {
255
+ const connection = await connectServer(config)
256
+ connections.set(config.name, connection)
257
+ } catch (error) {
258
+ logger.error(`Failed to reconnect MCP server ${config.name}:`, error)
259
+ connections.set(config.name, {
260
+ config,
261
+ client: null,
262
+ transport: null,
263
+ status: 'error',
264
+ error: error?.message || 'Failed to connect MCP server',
265
+ tools: [],
266
+ connectedAt: null,
267
+ stderr: '',
268
+ lastAttemptAt: Date.now(),
269
+ })
270
+ }
271
+ return connections
272
+ }
273
+
234
274
  export async function getMcpStatus() {
235
275
  await refreshMcpConnections()
236
276
  const servers = await readMcpServers()
@@ -83,7 +83,7 @@ export async function handleAgentApi(req, res, url) {
83
83
  error.statusCode = 400
84
84
  throw error
85
85
  }
86
- const result = await runPrompt(sessionId, message, body?.selectedCapabilities)
86
+ const result = await runPrompt(sessionId, message, body?.selectedCapabilities, body?.command)
87
87
  sendJson(res, 200, result)
88
88
  return
89
89
  }
@@ -1,7 +1,7 @@
1
1
  import { sendJson, readJsonBody } from '../utils/response.mjs'
2
2
  import { refreshAllSessionTools } from '../agent-manager.mjs'
3
3
  import { deleteMcpServer, normalizeMcpServers, readMcpServers, setMcpServerEnabled, upsertMcpServer, writeMcpServers } from '../mcp/config.mjs'
4
- import { getMcpStatus, refreshMcpConnections } from '../mcp/registry.mjs'
4
+ import { getMcpStatus, reconnectMcpServer, refreshMcpConnections } from '../mcp/registry.mjs'
5
5
 
6
6
  async function refreshMcpAndAgentTools() {
7
7
  await refreshMcpConnections()
@@ -54,6 +54,12 @@ export async function handleMcpApi(req, res, url) {
54
54
  return
55
55
  }
56
56
 
57
+ if (req.method === 'POST' && parts[0] === 'api' && parts[1] === 'mcp' && parts[2] === 'reconnect' && parts[3]) {
58
+ await reconnectMcpServer(decodeURIComponent(parts[3]))
59
+ sendJson(res, 200, await refreshMcpAndAgentTools())
60
+ return
61
+ }
62
+
57
63
  if (req.method === 'DELETE' && parts[0] === 'api' && parts[1] === 'mcp' && parts[2] === 'servers' && parts[3]) {
58
64
  await deleteMcpServer(decodeURIComponent(parts[3]))
59
65
  sendJson(res, 200, await refreshMcpAndAgentTools())
@@ -1,6 +1,6 @@
1
1
  import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
2
2
  import { getActiveProject, setActiveProjectPath, readProjectConfig } from '../project-config.mjs'
3
- import { listProjectCommands } from '../custom-commands.mjs'
3
+ import { listProjectCommands, createCommandFile } from '../custom-commands.mjs'
4
4
  import { atomicProjectConfigUpdate } from '../storage.mjs'
5
5
  import { getWorkspaceRoot, setWorkspaceRoot } from '../utils/workspace.mjs'
6
6
  import { selectDirectoryDialog, openPathInFileManager } from '../utils/platform.mjs'
@@ -20,7 +20,7 @@ export async function handleProjectApi(req, res, url) {
20
20
  const project = projectId
21
21
  ? config.projects.find((item) => item.id === projectId)
22
22
  : getActiveProject(config)
23
- const commands = project?.path ? await listProjectCommands(project.path, project.commandDir) : []
23
+ const commands = await listProjectCommands(project?.path, project?.commandDir)
24
24
  sendJson(res, 200, {
25
25
  commands: commands.map((command) => ({
26
26
  name: command.name,
@@ -29,6 +29,7 @@ export async function handleProjectApi(req, res, url) {
29
29
  allowEdit: command.allowEdit,
30
30
  allowCommands: command.allowCommands,
31
31
  relativePath: command.relativePath,
32
+ filePath: command.filePath,
32
33
  source: command.source,
33
34
  pluginName: command.pluginName,
34
35
  })),
@@ -107,6 +108,35 @@ export async function handleProjectApi(req, res, url) {
107
108
  return
108
109
  }
109
110
 
111
+ if (req.method === 'POST' && url.pathname === '/api/project/open-path') {
112
+ const body = await readJsonBody(req)
113
+ const active = body?.projectId
114
+ ? config.projects.find((project) => project.id === body.projectId) ?? getActiveProject(config)
115
+ : getActiveProject(config)
116
+ const target = String(body?.path || '')
117
+ const resolved = target && path.isAbsolute(target)
118
+ ? path.resolve(target)
119
+ : path.resolve(active?.path || '', target)
120
+ await openPathInFileManager(resolved)
121
+ sendJson(res, 200, { ok: true })
122
+ return
123
+ }
124
+
125
+ if (req.method === 'POST' && url.pathname === '/api/project/command') {
126
+ const body = await readJsonBody(req)
127
+ const active = body?.projectId
128
+ ? config.projects.find((project) => project.id === body.projectId) ?? getActiveProject(config)
129
+ : getActiveProject(config)
130
+ if (!active?.path) {
131
+ const error = new Error('No active project')
132
+ error.statusCode = 400
133
+ throw error
134
+ }
135
+ const result = await createCommandFile(active.path, body?.name)
136
+ sendJson(res, 200, result)
137
+ return
138
+ }
139
+
110
140
  if (req.method === 'DELETE' && url.pathname.startsWith('/api/project/')) {
111
141
  const id = decodeSegment(url.pathname.split('/').filter(Boolean)[2])
112
142
  const updated = await atomicProjectConfigUpdate((cfg) => {
@@ -363,7 +363,7 @@ export async function handleSharedConversationApi(req, res, url) {
363
363
  assertOperate(record)
364
364
  const body = await readJsonBody(req)
365
365
  await restoreAgent(record.sessionId)
366
- const result = await runPrompt(record.sessionId, messageFromBody(body, record, req))
366
+ const result = await runPrompt(record.sessionId, messageFromBody(body, record, req), [], body?.command)
367
367
  sendJson(res, 200, result)
368
368
  return
369
369
  }
@@ -94,6 +94,7 @@ export const configDir = path.join(dataDir, 'config')
94
94
  export const storageDir = path.join(dataDir, 'storage')
95
95
  export const cacheDir = path.join(dataDir, 'cache')
96
96
  export const logsDir = path.join(dataDir, 'logs')
97
+ export const userCommandsDir = path.join(dataDir, 'commands')
97
98
 
98
99
  const quickForgeConfigFile = path.join(configDir, 'config.json')
99
100
  const configMigrationMarkerFile = path.join(configDir, '.layout-migrated')
@@ -21,23 +21,23 @@ export const subagentDefinitions = [
21
21
  name: 'general',
22
22
  label: 'General',
23
23
  mode: 'subagent',
24
- description: 'A general-purpose agent for researching complex problems and executing multi-step tasks. It has full built-in workspace tool access, excluding MCP tools and Agent Skills, so it can modify files when needed. Use it for substantial independent work units, including parallelizable work.',
24
+ description: 'A general-purpose agent for bounded complex multi-step implementation or broader independent work. It has full built-in workspace tool access, excluding MCP tools and Agent Skills, so it can modify files when needed. Prefer Explore for focused read-only repository discovery, source search, call-chain lookup, tests/docs discovery, and impact analysis.',
25
25
  allowedTools: ['read_file', 'grep_files', 'write_file', 'edit_file', 'run_command'],
26
26
  allowFileMutations: true,
27
27
  maxRuntimeMs: 30 * 60 * 1000,
28
28
  maxToolCalls: 300,
29
- systemPrompt: `You are General, a general-purpose subagent for complex research and multi-step implementation tasks. You may inspect, edit, write files, and run commands using the built-in workspace tools when needed. You do not have MCP tools or Agent Skills. Make focused, minimal changes that satisfy the delegated task, and verify your changes when appropriate.`,
29
+ systemPrompt: `You are General, a general-purpose subagent for bounded complex multi-step implementation tasks and broader independent work. You may inspect, edit, write files, and run commands using the built-in workspace tools when needed. You do not have MCP tools or Agent Skills. Prefer Explore for focused read-only repository discovery, source search, call-chain lookup, tests/docs discovery, and impact analysis. Make focused, minimal changes that satisfy the delegated task, and verify your changes when appropriate.`,
30
30
  },
31
31
  {
32
32
  name: 'explore',
33
33
  label: 'Explore',
34
34
  mode: 'subagent',
35
- description: 'A fast read-only agent for targeted exploration and focused questions. It cannot modify files. Use it to quickly find relevant files, search keywords, identify patterns, or summarize findings.',
36
- allowedTools: ['read_file', 'grep_files'],
35
+ description: 'The preferred subagent for focused read-only repository exploration, file discovery, source search, call-chain lookup, related tests/docs/wiki discovery, safe inspection commands, pattern lookup, and impact analysis before non-trivial implementation. It cannot modify files.',
36
+ allowedTools: ['read_file', 'grep_files', 'run_command'],
37
37
  allowFileMutations: false,
38
38
  maxRuntimeMs: 30 * 60 * 1000,
39
39
  maxToolCalls: 300,
40
- systemPrompt: `You are Explore, a fast read-only exploration subagent. Use read_file and grep_files to locate relevant files, search keywords, identify patterns, and answer focused questions. You cannot modify files or run commands.`,
40
+ systemPrompt: `You are Explore, the preferred read-only repository exploration subagent. Use read_file, grep_files, and safe read-only run_command calls to locate files, inspect project structure, search source, trace call chains, find related tests/docs/wiki pages, run diagnostics, identify patterns, assess impact, and answer focused questions before non-trivial implementation. You cannot modify files.`,
41
41
  },
42
42
  ]
43
43
 
@@ -80,7 +80,9 @@ export function composeSubagentSystemPrompt({ definition, parentSystemPrompt, pr
80
80
  '- run_subagent is not available to subagents.',
81
81
  definition.allowFileMutations
82
82
  ? '- File modification tools are available when needed, subject to the parent session approval/YOLO policy.'
83
- : '- This subagent is read-only. Do not modify files or run commands.',
83
+ : definition.allowedTools.includes('run_command')
84
+ ? '- This subagent is read-only. Do not modify files. Use run_command only for safe inspection or diagnostic commands.'
85
+ : '- This subagent is read-only. Do not modify files or run commands.',
84
86
  workspaceLines.length ? `\nWorkspace context:\n${workspaceLines.join('\n')}` : '',
85
87
  '</subagent_instructions>',
86
88
  ].filter(Boolean).join('\n')
@@ -8,7 +8,7 @@ For project tasks:
8
8
  - For multi-step work, use a brief plan.
9
9
  - Before changing files, gather sufficient context: relevant files, entry points or call chains, existing patterns, tests or validation commands, and docs/wiki impact.
10
10
  - Before taking action, confirm with the user.
11
- - Unless the change is trivial and localized to an already-known file, use Explore first for read-only repository research; prefer Explore for broad searches, pattern lookup, impact analysis, and locating related tests, docs, or build scripts.
11
+ - Unless the change is trivial and localized to an already-known file, use Explore first for read-only repository research before implementation decisions; prefer Explore for file discovery, source location, broad searches, call-chain lookup, pattern lookup, impact analysis, and locating related tests, docs, wiki pages, or build scripts.
12
12
  - For complex multi-step work, use General only for bounded assistance; the parent assistant remains responsible for final decisions, minimal edits, and verification.
13
13
  - Make minimal, focused changes.
14
14
  - Prefer dedicated workspace tools for reading, editing, and searching files.
@@ -63,7 +63,7 @@ function appendSubagentCatalog(parts, subagents) {
63
63
  const subagentParts = subagents.map(formatSubagentCatalogItem)
64
64
  parts.push(`
65
65
  <available_subagents>
66
- The run_subagent tool can delegate work to an enabled temporary Agent Profile. Built-in profiles include Explore for fast read-only codebase search and General for complex multi-step implementation work; custom profiles may also be available when enabled.
66
+ The run_subagent tool can delegate work to an enabled temporary Agent Profile. Prefer Explore for focused read-only repository discovery before implementation decisions, including locating files, searching source, tracing call chains, finding related tests/docs/wiki pages, and impact analysis. Use General for bounded complex multi-step implementation or broader independent work; custom profiles may also be available when enabled.
67
67
 
68
68
  Choose the most appropriate subagent by name, keep delegation concrete, and include relevant context. Treat subagent output as advisory; you remain responsible for the final answer.
69
69
 
@@ -15,7 +15,7 @@ import { loadSelectedGlobalSkills, loadSelectedProjectSkills, mergeSkills } from
15
15
  export const subagentTool = {
16
16
  name: 'run_subagent',
17
17
  label: 'Run subagent',
18
- description: 'Delegate a bounded task to an enabled temporary Agent Profile. Built-in profiles include general for complex multi-step work and explore for fast read-only lookup. Custom profiles can also be enabled as subagents. Subagents are short-lived and do not receive MCP or Agent Skill tools.',
18
+ description: 'Delegate a bounded task to an enabled temporary Agent Profile. Prefer explore for focused read-only repository discovery before implementation decisions, including locating files, searching source, tracing call chains, finding related tests/docs/wiki pages, and impact analysis. Use general for bounded complex multi-step implementation or broader independent work. Custom profiles can also be enabled as subagents. Subagents are short-lived and do not receive MCP or Agent Skill tools.',
19
19
  parameters: Type.Object({
20
20
  subagent: Type.String({ description: 'Agent Profile name to invoke.' }),
21
21
  task: Type.String({ description: 'Concrete, bounded task for the subagent. Do not delegate vague or open-ended work.' }),