@shawnstack/quickforge 1.7.11 → 1.8.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.
Files changed (122) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/AgentProfilesPage-lKGIE5Dj.js +1 -0
  3. package/dist/assets/ChatPanelHost-CWUYIkXG.js +11 -0
  4. package/dist/assets/CloudAccountSettingsPage-BhXH0ups.js +1 -0
  5. package/dist/assets/{PluginsPage-mLvwvTmL.js → PluginsPage-DHWF13mN.js} +1 -1
  6. package/dist/assets/ScheduledTasksPage-DLAg-i8c.js +2 -0
  7. package/dist/assets/{SettingsWorkspacePage-0OINfEqK.js → SettingsWorkspacePage-BhUzL5oi.js} +313 -313
  8. package/dist/assets/ShareLinksSettingsPage-DogGa3lH.js +1 -0
  9. package/dist/assets/SharedConversationPage-7QpeaG_P.js +1 -0
  10. package/dist/assets/TerminalDock-CWSKcuiU.js +2 -0
  11. package/dist/assets/WorkspaceInspector-CEfyYqWy.js +36 -0
  12. package/dist/assets/{abnfDiagram-VRR7QNED-CMhVoYUh.js → abnfDiagram-VRR7QNED-DGklo3PO.js} +1 -1
  13. package/dist/assets/{architectureDiagram-ZJ3FMSHR-C52qL6Hg.js → architectureDiagram-ZJ3FMSHR-CioWLVTp.js} +1 -1
  14. package/dist/assets/{blockDiagram-677ZJIJ3-CM3XbLol.js → blockDiagram-677ZJIJ3-BJWBq5e9.js} +1 -1
  15. package/dist/assets/{c4Diagram-LMCZKHZV-D7Bh1ui1.js → c4Diagram-LMCZKHZV-BacRvqFK.js} +1 -1
  16. package/dist/assets/channel-CeXY-wZo.js +1 -0
  17. package/dist/assets/{chunk-32BRIVSS-xgrtbT8e.js → chunk-32BRIVSS-B53BNE2C.js} +1 -1
  18. package/dist/assets/{chunk-52WLFC77-CDbaHw80.js → chunk-52WLFC77-CYq4hT4f.js} +1 -1
  19. package/dist/assets/{chunk-C7G6YPKG-ZMLUvfLy.js → chunk-C7G6YPKG-KFaGpP_Z.js} +1 -1
  20. package/dist/assets/{chunk-EX3LRPZG-BcQhaqt7.js → chunk-EX3LRPZG-DqVd_ihG.js} +1 -1
  21. package/dist/assets/{chunk-FWX5IMBZ-DrnrdrSu.js → chunk-FWX5IMBZ-BxRjZkOP.js} +2 -2
  22. package/dist/assets/{chunk-HOUHSVGY-tdc-Z9eW.js → chunk-HOUHSVGY-C9QO8omv.js} +1 -1
  23. package/dist/assets/{chunk-ICXQ74PX-Cd7igVGt.js → chunk-ICXQ74PX-ClbGPgLV.js} +1 -1
  24. package/dist/assets/{chunk-MOJQB5TN-CLTjV5ov.js → chunk-MOJQB5TN-Ba4wlF1J.js} +1 -1
  25. package/dist/assets/{chunk-OGEWGWER-TgvkXkFX.js → chunk-OGEWGWER-DczhQmcx.js} +1 -1
  26. package/dist/assets/{chunk-PUDLZKDR-C5GMVYup.js → chunk-PUDLZKDR-CPWpcKUF.js} +1 -1
  27. package/dist/assets/{chunk-Q4XR5HBZ-fo5-HsqY.js → chunk-Q4XR5HBZ-CiF6T--X.js} +1 -1
  28. package/dist/assets/{chunk-V7JOEXUC-CEDazV6w.js → chunk-V7JOEXUC-Dxv0zwmw.js} +1 -1
  29. package/dist/assets/{chunk-VAUOI2AC-CWYjg8zE.js → chunk-VAUOI2AC-D3J5bw1P.js} +1 -1
  30. package/dist/assets/{chunk-VR4S4FIN-C_-eEj-9.js → chunk-VR4S4FIN-BXQSwwII.js} +1 -1
  31. package/dist/assets/{chunk-WYO6CB5R-NF-MsJJR.js → chunk-WYO6CB5R-C0azl-fk.js} +2 -2
  32. package/dist/assets/{chunk-ZGVPDNZ5-CRrAYi4g.js → chunk-ZGVPDNZ5-D_8F4Akx.js} +1 -1
  33. package/dist/assets/classDiagram-OUVF2IWQ-B8M6sf49.js +1 -0
  34. package/dist/assets/classDiagram-v2-EOCWNBFH-BY7fzBDH.js +1 -0
  35. package/dist/assets/{cynefinDiagram-TSTJHNR4-D4sZ7LYI.js → cynefinDiagram-TSTJHNR4-BFlsWz0B.js} +1 -1
  36. package/dist/assets/{dagre-VKFMJZFB-BlsnSh-Q.js → dagre-VKFMJZFB-DXhpjlEs.js} +1 -1
  37. package/dist/assets/{diagram-FQU43EPY-DW1yQiRO.js → diagram-FQU43EPY-DRVVOD9k.js} +1 -1
  38. package/dist/assets/{diagram-G47NLZAW-BTUAsFjb.js → diagram-G47NLZAW-BV9Gw1tL.js} +1 -1
  39. package/dist/assets/{diagram-NH7WQ7WH-BcaomqSd.js → diagram-NH7WQ7WH-CrUkXfRY.js} +1 -1
  40. package/dist/assets/{diagram-OA4YK3LP-DDLX7Xwr.js → diagram-OA4YK3LP-CuILjn7v.js} +1 -1
  41. package/dist/assets/{diagram-WEI45ONY-CIave0An.js → diagram-WEI45ONY-nlNN3tWB.js} +1 -1
  42. package/dist/assets/{ebnfDiagram-CCIWWBDH-Cr1Y6lPu.js → ebnfDiagram-CCIWWBDH-BuDgZt9n.js} +1 -1
  43. package/dist/assets/{erDiagram-Q63AITRT-D3EgiVVH.js → erDiagram-Q63AITRT-MEwxjmtP.js} +1 -1
  44. package/dist/assets/flowDiagram-23GEKE2U-BiUP-Scq.js +1 -0
  45. package/dist/assets/{ganttDiagram-NO4QXBWP-B0oyDIqK.js → ganttDiagram-NO4QXBWP-DNw5qghE.js} +1 -1
  46. package/dist/assets/{gitGraphDiagram-IHSO6WYX-uQtr6IqK.js → gitGraphDiagram-IHSO6WYX-CaTcakAt.js} +1 -1
  47. package/dist/assets/index-CX36XjVA.css +3 -0
  48. package/dist/assets/index-CgVTwN15.js +66 -0
  49. package/dist/assets/{infoDiagram-FWYZ7A6U-B_GzlLIM.js → infoDiagram-FWYZ7A6U-DwfyP7dU.js} +1 -1
  50. package/dist/assets/{ishikawaDiagram-FXEZZL3T-CKQIsSdU.js → ishikawaDiagram-FXEZZL3T-CmrzbOCV.js} +1 -1
  51. package/dist/assets/{journeyDiagram-5HDEW3XC-DrrvzZL1.js → journeyDiagram-5HDEW3XC-CYqe3sgr.js} +1 -1
  52. package/dist/assets/{kanban-definition-HUTT4EX6-CJQRTdXz.js → kanban-definition-HUTT4EX6-BMDO_mv0.js} +1 -1
  53. package/dist/assets/{line-B4DxX7tC.js → line-SbxJR8ZT.js} +1 -1
  54. package/dist/assets/lit-vendor-CinchGmI.js +2 -0
  55. package/dist/assets/local-tools-CJVNO1aW.js +388 -0
  56. package/dist/assets/{mcp-servers-dialog-B2lALtTE.js → mcp-servers-dialog-xf3UR49J.js} +2 -2
  57. package/dist/assets/{mermaid.core-DWpShXCs.js → mermaid.core-CwXTzYjs.js} +3 -3
  58. package/dist/assets/{mindmap-definition-LN4V7U3C-CEVwZD78.js → mindmap-definition-LN4V7U3C-CLyJ78mO.js} +1 -1
  59. package/dist/assets/{pegDiagram-2B236MQR-CBLkBdnQ.js → pegDiagram-2B236MQR-4qFznrSw.js} +1 -1
  60. package/dist/assets/{pi-web-ui-DV5jKjSg.js → pi-web-ui-BNuvqKDT.js} +289 -289
  61. package/dist/assets/{pieDiagram-ENE6RG2P-8tCnwtWS.js → pieDiagram-ENE6RG2P-141sGxVe.js} +1 -1
  62. package/dist/assets/{quadrantDiagram-ABIIQ3AL-CWd28cHF.js → quadrantDiagram-ABIIQ3AL-SUI44pAX.js} +1 -1
  63. package/dist/assets/{railroadDiagram-RFXS5EU6-CI4ouOBs.js → railroadDiagram-RFXS5EU6-1oiZuYBB.js} +1 -1
  64. package/dist/assets/{requirementDiagram-TGXJPOKE-DtFz7sw4.js → requirementDiagram-TGXJPOKE-CfVishLi.js} +1 -1
  65. package/dist/assets/{sankeyDiagram-HTMAVEWB--VeOwwk9.js → sankeyDiagram-HTMAVEWB-DVu7J_Z9.js} +1 -1
  66. package/dist/assets/{sequenceDiagram-DBY2YBRQ-B8AHk5Oe.js → sequenceDiagram-DBY2YBRQ-D-Ip2yiS.js} +1 -1
  67. package/dist/assets/{skills-dialog-DYp35JM3.js → skills-dialog-Bbqtuy2h.js} +3 -3
  68. package/dist/assets/{stateDiagram-2N3HPSRC-etXnJ8X8.js → stateDiagram-2N3HPSRC-Cg0hWwrp.js} +1 -1
  69. package/dist/assets/stateDiagram-v2-6OUMAXLB-DhmzNQd9.js +1 -0
  70. package/dist/assets/{swimlanes-5IMT3BWC-pabIKAmn.js → swimlanes-5IMT3BWC-Co5D-cGU.js} +1 -1
  71. package/dist/assets/swimlanesDiagram-G3AALYLV-BEvomQNa.js +8 -0
  72. package/dist/assets/{timeline-definition-FHXFAJF6-C55MKm3L.js → timeline-definition-FHXFAJF6-CBk4c_Ua.js} +1 -1
  73. package/dist/assets/{vennDiagram-L72KCM5P-CgNf1Q9W.js → vennDiagram-L72KCM5P-BII7wdRx.js} +1 -1
  74. package/dist/assets/{wardleyDiagram-EHGQE667-Drt5ytec.js → wardleyDiagram-EHGQE667-CkyJJIXP.js} +1 -1
  75. package/dist/assets/{xychartDiagram-FW5EYKEG-hhoEo_Ar.js → xychartDiagram-FW5EYKEG-B1A66Ks0.js} +1 -1
  76. package/dist/index.html +6 -6
  77. package/dist/licenses/material-icon-theme.txt +8 -8
  78. package/package.json +1 -1
  79. package/server/agent-manager.mjs +305 -49
  80. package/server/approval-store.mjs +1 -0
  81. package/server/ask-store.mjs +78 -0
  82. package/server/context-references.mjs +139 -0
  83. package/server/custom-commands.mjs +43 -0
  84. package/server/index.mjs +1 -1
  85. package/server/project-config.mjs +17 -5
  86. package/server/routes/agent-profiles.mjs +16 -1
  87. package/server/routes/agent.mjs +10 -1
  88. package/server/routes/shared-conversation.mjs +13 -1
  89. package/server/routes/skills.mjs +22 -0
  90. package/server/routes/workspace.mjs +166 -2
  91. package/server/selected-capabilities.mjs +76 -0
  92. package/server/session-state-import.mjs +20 -3
  93. package/server/skills.mjs +1 -1
  94. package/server/system-prompt.mjs +1 -3
  95. package/server/tools/definitions.mjs +18 -10
  96. package/server/tools/index.mjs +14 -2
  97. package/server/utils/workspace.mjs +13 -4
  98. package/skills/skill-creator/SKILL.md +485 -485
  99. package/skills/skill-creator/agents/analyzer.md +274 -274
  100. package/skills/skill-creator/agents/comparator.md +202 -202
  101. package/skills/skill-creator/agents/grader.md +223 -223
  102. package/skills/skill-creator/assets/eval_review.html +146 -146
  103. package/skills/skill-creator/eval-viewer/viewer.html +1325 -1325
  104. package/skills/skill-creator/references/schemas.md +430 -430
  105. package/dist/assets/AgentProfilesPage-Cvb1KTz5.js +0 -1
  106. package/dist/assets/ChatPanelHost-C7zV-xE_.js +0 -48
  107. package/dist/assets/CloudAccountSettingsPage-mAs_nbvw.js +0 -1
  108. package/dist/assets/ScheduledTasksPage-CuUW_9lq.js +0 -2
  109. package/dist/assets/ShareLinksSettingsPage-DqeyJYoD.js +0 -1
  110. package/dist/assets/SharedConversationPage-C7CDEoX9.js +0 -1
  111. package/dist/assets/TerminalDock-CZXcuMpq.js +0 -2
  112. package/dist/assets/WorkspaceInspector-CNSBOEvP.js +0 -36
  113. package/dist/assets/channel-Duu1CuH9.js +0 -1
  114. package/dist/assets/classDiagram-OUVF2IWQ-Cc63fLWP.js +0 -1
  115. package/dist/assets/classDiagram-v2-EOCWNBFH-CoXU0qT6.js +0 -1
  116. package/dist/assets/flowDiagram-23GEKE2U-BS7KXcqn.js +0 -1
  117. package/dist/assets/index-BgMvQW1b.js +0 -66
  118. package/dist/assets/index-CB1g2D0f.css +0 -3
  119. package/dist/assets/lit-vendor-DvaQX_UQ.js +0 -2
  120. package/dist/assets/local-tools-Dopjg9xq.js +0 -270
  121. package/dist/assets/stateDiagram-v2-6OUMAXLB-Dms3pICj.js +0 -1
  122. package/dist/assets/swimlanesDiagram-G3AALYLV-B14aWjv6.js +0 -8
@@ -13,7 +13,7 @@ import {
13
13
  composeSubagentSystemPrompt,
14
14
  formatSubagentTask,
15
15
  } from './subagents.mjs'
16
- import { agentProfileSnapshot, getAgentProfile } from './agent-profiles.mjs'
16
+ import { agentProfileSnapshot, getAgentProfile, listSubagentProfiles } from './agent-profiles.mjs'
17
17
  import { modelBindingFromModel, resolveImplicitModelPreference, resolveModelBinding } from './model-catalog.mjs'
18
18
  import { agentProfileFromMarkdown } from './agent-profile-files.mjs'
19
19
  import {
@@ -52,11 +52,27 @@ import {
52
52
  readAutoCompactSettings,
53
53
  } from './auto-compaction.mjs'
54
54
  import {
55
+ formatAgentCommandPrompt,
56
+ formatSkillCommandPrompt,
55
57
  handleInternalCommand,
56
58
  parseInternalCommandInvocation,
57
59
  resolveCustomCommandInvocation,
58
60
  } from './custom-commands.mjs'
61
+ import { mergeSkills, normalizeSkillNames } from './skills.mjs'
62
+ import {
63
+ contextReferencesFromMessage,
64
+ contextReferencesPrompt,
65
+ validatePromptContextReferences,
66
+ validateContextReferences,
67
+ withCanonicalContextReferences,
68
+ } from './context-references.mjs'
59
69
  import { serverConvertToLlm, messageText, lastAssistantText } from './message-converters.mjs'
70
+ import {
71
+ normalizeSelectedCapabilities,
72
+ selectedCapabilitiesFromMessage,
73
+ selectedCapabilityPrompt,
74
+ withCanonicalSelectedCapabilities,
75
+ } from './selected-capabilities.mjs'
60
76
  import { mergeQuickForgeTiming, wrapToolDefinition, wrapMcpToolDefinition, wrapPluginToolDefinition, sessionSkillsContext } from './tool-wiring.mjs'
61
77
  import {
62
78
  APPROVAL_TIMEOUT_MS,
@@ -68,6 +84,15 @@ import {
68
84
  commandToolPermissionError,
69
85
  createCommandToolPermissions,
70
86
  } from './approval-store.mjs'
87
+ import {
88
+ ASK_TIMEOUT_MS,
89
+ pendingAsks,
90
+ getPendingAskForSession,
91
+ normalizeAskQuestions,
92
+ formatAskResult,
93
+ } from './ask-store.mjs'
94
+
95
+ export { getPendingAskForSession, normalizeAskQuestions } from './ask-store.mjs'
71
96
 
72
97
  // ---------------------------------------------------------------------------
73
98
  // Tool definitions (server-side, no REST roundtrip)
@@ -88,8 +113,25 @@ function wrapSubagentToolDefinition(definition, parentSessionId) {
88
113
  }
89
114
  }
90
115
 
116
+ function wrapAskUserToolDefinition(definition, parentSessionId) {
117
+ return {
118
+ ...definition,
119
+ execute: async (toolCallId, params) => {
120
+ const session = agentSessions.get(parentSessionId)
121
+ if (!session) {
122
+ return {
123
+ content: [{ type: 'text', text: 'No active session for ask_user.' }],
124
+ details: { askId: null, skipped: true },
125
+ }
126
+ }
127
+ return createAskUserPromise(session, toolCallId, params || {})
128
+ },
129
+ }
130
+ }
131
+
91
132
  function wrapWorkspaceToolDefinition(definition, context, toolPermissions, options = {}) {
92
133
  if (definition.name === 'run_subagent') return wrapSubagentToolDefinition(definition, options.parentSessionId)
134
+ if (definition.name === 'ask_user') return wrapAskUserToolDefinition(definition, options.parentSessionId)
93
135
  return wrapToolDefinition(definition, context, toolPermissions)
94
136
  }
95
137
 
@@ -304,8 +346,83 @@ function createApprovalPromise(session, toolCallId, toolName, args, source) {
304
346
  })
305
347
  }
306
348
 
307
- function createAcpApprovalPromise(session, request) {
308
- return new Promise((resolve, reject) => {
349
+ /**
350
+ * Create a Promise that resolves when the user answers (or skips) an ask_user
351
+ * tool call. The tool's execute blocks on this promise, pausing the agent loop
352
+ * until the user responds.
353
+ */
354
+ function createAskUserPromise(session, toolCallId, params) {
355
+ const questions = normalizeAskQuestions(params)
356
+ if (!questions.length) {
357
+ return Promise.resolve({
358
+ content: [{ type: 'text', text: formatAskResult(questions, null, true, 'no-questions') }],
359
+ details: { askId: null, skipped: true },
360
+ })
361
+ }
362
+ const askId = randomUUID()
363
+ return new Promise((resolve) => {
364
+ let settled = false
365
+ const requestedAt = Date.now()
366
+ const expiresAt = requestedAt + ASK_TIMEOUT_MS
367
+
368
+ const timeout = setTimeout(() => {
369
+ if (settled) return
370
+ finish({ skipped: true, reason: 'timeout' })
371
+ }, ASK_TIMEOUT_MS)
372
+
373
+ let onAbort = null
374
+
375
+ const finish = (payload) => {
376
+ if (settled) return
377
+ settled = true
378
+ clearTimeout(timeout)
379
+ if (onAbort && session.agent.signal) session.agent.signal.removeEventListener('abort', onAbort)
380
+ pendingAsks.delete(askId)
381
+ const answers = payload.skipped ? null : payload.answers
382
+ resolve({
383
+ content: [{ type: 'text', text: formatAskResult(questions, answers, payload.skipped, payload.reason) }],
384
+ details: { askId, questions, answers, skipped: !!payload.skipped, ...(payload.reason ? { skipReason: payload.reason } : {}) },
385
+ })
386
+ emitSessionEvent(session, {
387
+ type: 'ask_user_answered',
388
+ sessionId: session.sessionId,
389
+ askId,
390
+ toolCallId,
391
+ skipped: !!payload.skipped,
392
+ answers: answers || [],
393
+ })
394
+ }
395
+
396
+ const signal = session.agent.signal
397
+ if (signal) {
398
+ if (signal.aborted) {
399
+ finish({ skipped: true, reason: 'aborted' })
400
+ return
401
+ }
402
+ onAbort = () => finish({ skipped: true, reason: 'aborted' })
403
+ signal.addEventListener('abort', onAbort, { once: true })
404
+ }
405
+
406
+ pendingAsks.set(askId, {
407
+ finish: (payload) => finish(payload),
408
+ sessionId: session.sessionId,
409
+ toolCallId,
410
+ questions,
411
+ requestedAt,
412
+ expiresAt,
413
+ })
414
+
415
+ emitSessionEvent(session, {
416
+ type: 'ask_user_required',
417
+ sessionId: session.sessionId,
418
+ askId,
419
+ toolCallId,
420
+ questions,
421
+ })
422
+ })
423
+ }
424
+
425
+ function createAcpApprovalPromise(session, request) { return new Promise((resolve, reject) => {
309
426
  let settled = false
310
427
  const requestedAt = Date.now()
311
428
  const expiresAt = requestedAt + APPROVAL_TIMEOUT_MS
@@ -1035,10 +1152,103 @@ function planCommandState(userMessage, args) {
1035
1152
  }
1036
1153
  }
1037
1154
 
1155
+ function parseSlashNameAndTask(args) {
1156
+ const trimmed = String(args || '').trim()
1157
+ if (!trimmed) return { name: '', task: '' }
1158
+ const match = trimmed.match(/^(\S+)(?:\s+([\s\S]*))?$/)
1159
+ return { name: match?.[1] || '', task: (match?.[2] || '').trim() }
1160
+ }
1161
+
1162
+ async function enabledSkillsForSession(session) {
1163
+ // Same sources as the activate_skill tool (loadSkillToolContext), resolved
1164
+ // from the session's captured skill selections and workspace.
1165
+ const skillContext = await loadSkillToolContext({
1166
+ ...sessionSkillsContext(session),
1167
+ workspaceRoot: session.projectContext?.workspaceRoot,
1168
+ })
1169
+ return mergeSkills(skillContext.globalSkills, skillContext.projectSkills)
1170
+ }
1171
+
1172
+ function enabledSkillsLine(enabledSkills) {
1173
+ if (enabledSkills.length === 0) return 'No skills are currently enabled.'
1174
+ return `Enabled skills: ${enabledSkills.map((skill) => skill.name).join(', ')}.`
1175
+ }
1176
+
1177
+ function availableSubagentsLine(profiles) {
1178
+ return `Available subagents: ${profiles.map((profile) => profile.name).join(', ')}.`
1179
+ }
1180
+
1181
+ async function skillCommandState(session, userMessage, args) {
1182
+ const { name, task } = parseSlashNameAndTask(args)
1183
+ const enabledSkills = await enabledSkillsForSession(session)
1184
+
1185
+ if (!name) {
1186
+ return { textResponse: ['Usage: /skill <name> [task]', '', enabledSkillsLine(enabledSkills)].join('\n') }
1187
+ }
1188
+
1189
+ const skillName = normalizeSkillNames([name])[0]
1190
+ const skill = skillName ? enabledSkills.find((item) => item.name === skillName) : null
1191
+ if (!skill) {
1192
+ return {
1193
+ textResponse: [
1194
+ `Unknown or disabled skill: ${name}`,
1195
+ '',
1196
+ 'Usage: /skill <name> [task]',
1197
+ '',
1198
+ enabledSkillsLine(enabledSkills),
1199
+ ].join('\n'),
1200
+ }
1201
+ }
1202
+
1203
+ return {
1204
+ userMessage,
1205
+ commandPrompt: formatSkillCommandPrompt(skill.name, task),
1206
+ commandName: 'skill',
1207
+ }
1208
+ }
1209
+
1210
+ async function agentCommandState(session, userMessage, args) {
1211
+ const { name, task } = parseSlashNameAndTask(args)
1212
+ const profileOptions = { workspaceRoot: session.projectContext?.workspaceRoot }
1213
+
1214
+ if (!name) {
1215
+ const profiles = await listSubagentProfiles(profileOptions)
1216
+ return { textResponse: ['Usage: /agent <name> <task>', '', availableSubagentsLine(profiles)].join('\n') }
1217
+ }
1218
+ if (!task) {
1219
+ return { textResponse: 'Usage: /agent <name> <task>' }
1220
+ }
1221
+
1222
+ const profile = await getAgentProfile(name, profileOptions)
1223
+ if (!profile || profile.enabledAsSubagent !== true) {
1224
+ const profiles = await listSubagentProfiles(profileOptions)
1225
+ return {
1226
+ textResponse: [
1227
+ `Unknown subagent: ${name}`,
1228
+ '',
1229
+ 'Usage: /agent <name> <task>',
1230
+ '',
1231
+ availableSubagentsLine(profiles),
1232
+ ].join('\n'),
1233
+ }
1234
+ }
1235
+
1236
+ return {
1237
+ userMessage,
1238
+ commandPrompt: formatAgentCommandPrompt(profile.name, task),
1239
+ commandName: 'agent',
1240
+ }
1241
+ }
1242
+
1038
1243
  async function resolveCommandState(session, userMessage, promptCommand = null) {
1039
1244
  const command = normalizedPromptCommand(promptCommand) || promptCommandFromMessage(userMessage)
1245
+ const internalInvocation = internalInvocationForPromptCommand(userMessage, command)
1246
+ // /skill and /agent need session context (enabled skills, workspace-rooted
1247
+ // agent profiles), so they are resolved here before handleInternalCommand.
1248
+ if (internalInvocation?.type === 'skill') return skillCommandState(session, userMessage, internalInvocation.args)
1249
+ if (internalInvocation?.type === 'agent') return agentCommandState(session, userMessage, internalInvocation.args)
1040
1250
  const internalResponse = await handleInternalCommand(
1041
- internalInvocationForPromptCommand(userMessage, command),
1251
+ internalInvocation,
1042
1252
  session.projectContext?.workspaceRoot,
1043
1253
  session.projectContext?.project?.commandDir,
1044
1254
  )
@@ -1066,6 +1276,14 @@ async function resolveCommandState(session, userMessage, promptCommand = null) {
1066
1276
  commandName: 'review',
1067
1277
  }
1068
1278
  }
1279
+ if (internalResponse?.commit) {
1280
+ return {
1281
+ userMessage,
1282
+ commandPrompt: formatCommitCommandPrompt(internalResponse.args),
1283
+ permissions: { allowEdit: false, allowCommands: true, allowSubagents: false },
1284
+ commandName: 'commit',
1285
+ }
1286
+ }
1069
1287
 
1070
1288
  if (!session.projectContext?.workspaceRoot) {
1071
1289
  // Even without a project, user-level custom commands (~/.quickforge/commands/) are available
@@ -1175,6 +1393,24 @@ ${taskText}
1175
1393
  </plan_command_invocation>`
1176
1394
  }
1177
1395
 
1396
+ function formatCommitCommandPrompt(message) {
1397
+ const messageText = String(message || '').trim() || '(none; generate a message from the diff and repository style)'
1398
+ return `<commit_command_invocation name="commit">
1399
+ This /commit command applies only to the current user request. Create at most one local commit for the current task.
1400
+
1401
+ Rules for this turn:
1402
+ - Inspect the current task's Git changes and commit only files related to this task; do not mix in unrelated changes.
1403
+ - Never use \`git add .\`, \`git add -A\`, or \`git add --all\`; stage only explicit task-related paths.
1404
+ - Run relevant validation before committing and stop if it fails. Do not modify code, bypass hooks, or alter unrelated changes.
1405
+ - Create at most one local commit. Do not push, tag, release, publish, or otherwise affect a remote.
1406
+ - Use the requested message below, or generate one from the diff and repository conventions when none is provided.
1407
+ - Report the commit hash and message, validations run, and any remaining working tree changes.
1408
+
1409
+ Requested commit message:
1410
+ ${messageText}
1411
+ </commit_command_invocation>`
1412
+ }
1413
+
1178
1414
  function formatReviewCommandPrompt(scope) {
1179
1415
  const scopeText = String(scope || '').trim() || '(none; review the repository changes that appear relevant for a pre-commit check)'
1180
1416
  return `<review_command_invocation name="review">
@@ -1541,30 +1777,6 @@ function textFromMessageContent(content) {
1541
1777
  return ''
1542
1778
  }
1543
1779
 
1544
- function selectedCapabilityPrompt(capabilities) {
1545
- if (!Array.isArray(capabilities) || capabilities.length === 0) return null
1546
- const normalized = capabilities
1547
- .filter((capability) => capability && typeof capability === 'object')
1548
- .map((capability) => ({
1549
- type: String(capability.type || '').slice(0, 32),
1550
- pluginName: String(capability.pluginName || '').slice(0, 120),
1551
- name: String(capability.name || '').slice(0, 120),
1552
- label: String(capability.label || capability.name || '').slice(0, 160),
1553
- description: String(capability.description || '').slice(0, 400),
1554
- }))
1555
- .filter((capability) => capability.type && capability.pluginName && capability.name)
1556
- .slice(0, 4)
1557
- if (normalized.length === 0) return null
1558
-
1559
- const lines = normalized.map((capability) => {
1560
- const toolHint = capability.type === 'tool' ? ` Tool name: plugin__${capability.pluginName}__${capability.name}.` : ''
1561
- const description = capability.description ? ` Description: ${capability.description}` : ''
1562
- return `- ${capability.label} (${capability.type}, plugin: ${capability.pluginName}, name: ${capability.name}).${toolHint}${description}`
1563
- }).join('\n')
1564
-
1565
- return `The user selected the following QuickForge plugin capability mentions for this turn. Treat them as an explicit preference for routing and context. Use the selected capability when relevant, but do not force it if it is unrelated to the actual request.\n\n${lines}`
1566
- }
1567
-
1568
1780
  function applyActiveCapabilityPrompt(messages, capabilityPrompt) {
1569
1781
  if (!capabilityPrompt) return messages
1570
1782
 
@@ -1880,6 +2092,8 @@ export async function createAgent(sessionId, config = {}) {
1880
2092
  if (commandPermissionError) return { block: true, reason: commandPermissionError }
1881
2093
  const isSkillTool = toolName === 'activate_skill' || toolName === 'read_skill_resource'
1882
2094
  if (isSkillTool) return undefined
2095
+ // ask_user only waits for the user's answer; it changes nothing and needs no approval.
2096
+ if (toolName === 'ask_user') return undefined
1883
2097
  if (profileToolNames && !profileToolNames.includes(toolName)) return { block: true, reason: `Agent profile ${agentProfile.name} is not allowed to use ${toolName}.` }
1884
2098
  if (toolName === 'manage_global_memory') return undefined
1885
2099
  if (toolName === 'run_subagent') {
@@ -2592,14 +2806,11 @@ async function refreshSessionModelBinding(session) {
2592
2806
  * Send a user message to the agent and start the agent loop.
2593
2807
  * Returns immediately; events are streamed via the event bus.
2594
2808
  */
2595
- export async function runPrompt(sessionId, message, selectedCapabilities = [], promptCommand = null, transientContextPrompt = null, modelAccessContext = null) {
2809
+ export async function runPrompt(sessionId, message, selectedCapabilities = [], promptCommand = null, transientContextPrompt = null, modelAccessContext = null, contextReferences = undefined) {
2596
2810
  let session = await syncSessionFromStorage(sessionId)
2597
2811
  if (!session) {
2598
2812
  throw Object.assign(new Error('Session not found'), { statusCode: 404 })
2599
2813
  }
2600
- if (modelAccessContext) session.modelAccessContext = modelAccessContext
2601
- if (session.harness === AGENT_HARNESS_QUICKFORGE) await refreshSessionModelBinding(session)
2602
-
2603
2814
  if (session.agent.state.isStreaming || session.abortPending) {
2604
2815
  throw Object.assign(new Error('Generation is still running. Stop it or wait until it finishes.'), {
2605
2816
  statusCode: 409,
@@ -2607,17 +2818,27 @@ export async function runPrompt(sessionId, message, selectedCapabilities = [], p
2607
2818
  })
2608
2819
  }
2609
2820
 
2610
- if (session.harness === AGENT_HARNESS_QUICKFORGE) await refreshMemoryState(session)
2611
- resetIdleTimer(session)
2612
-
2613
- // Build user message
2821
+ // Validate references before model refresh, memory refresh, idle timers, title,
2822
+ // message persistence, or agent_start side effects.
2614
2823
  const initialUserMessage = typeof message === 'string'
2615
2824
  ? { role: 'user', content: message, timestamp: new Date().toISOString() }
2616
2825
  : message
2826
+ const canonicalContextReferences = await validatePromptContextReferences(contextReferences, session)
2827
+ const canonicalSelectedCapabilities = normalizeSelectedCapabilities(selectedCapabilities)
2828
+ const canonicalInitialUserMessage = withCanonicalSelectedCapabilities(
2829
+ withCanonicalContextReferences(initialUserMessage, canonicalContextReferences),
2830
+ canonicalSelectedCapabilities,
2831
+ )
2832
+
2833
+ if (modelAccessContext) session.modelAccessContext = modelAccessContext
2834
+ if (session.harness === AGENT_HARNESS_QUICKFORGE) await refreshSessionModelBinding(session)
2835
+ if (session.harness === AGENT_HARNESS_QUICKFORGE) await refreshMemoryState(session)
2836
+ resetIdleTimer(session)
2837
+
2617
2838
  const commandState = session.harness === AGENT_HARNESS_QUICKFORGE
2618
- ? await resolveCommandState(session, initialUserMessage, promptCommand)
2619
- : { userMessage: initialUserMessage }
2620
- const resolvedUserMessage = commandState.userMessage ?? initialUserMessage
2839
+ ? await resolveCommandState(session, canonicalInitialUserMessage, promptCommand)
2840
+ : { userMessage: canonicalInitialUserMessage }
2841
+ const resolvedUserMessage = commandState.userMessage ?? canonicalInitialUserMessage
2621
2842
 
2622
2843
  if (session.harness === AGENT_HARNESS_OPENCODE) {
2623
2844
  session.agent.validatePrompt(resolvedUserMessage)
@@ -2626,7 +2847,7 @@ export async function runPrompt(sessionId, message, selectedCapabilities = [], p
2626
2847
  if (commandState.textResponse) {
2627
2848
  session.agent.state.messages = [
2628
2849
  ...session.agent.state.messages,
2629
- initialUserMessage,
2850
+ canonicalInitialUserMessage,
2630
2851
  assistantTextMessage(commandState.textResponse, session.model),
2631
2852
  ]
2632
2853
  await persistSession(session)
@@ -2641,11 +2862,11 @@ export async function runPrompt(sessionId, message, selectedCapabilities = [], p
2641
2862
  }
2642
2863
 
2643
2864
  if (commandState.summary) {
2644
- return summarySession(session, initialUserMessage, commandState.summary)
2865
+ return summarySession(session, canonicalInitialUserMessage, commandState.summary)
2645
2866
  }
2646
2867
 
2647
2868
  if (commandState.compact) {
2648
- return compactSession(session, initialUserMessage, commandState.compact)
2869
+ return compactSession(session, canonicalInitialUserMessage, commandState.compact)
2649
2870
  }
2650
2871
 
2651
2872
  const userMessage = session.harness === AGENT_HARNESS_QUICKFORGE
@@ -2665,10 +2886,12 @@ export async function runPrompt(sessionId, message, selectedCapabilities = [], p
2665
2886
  session.activeCommandName = commandState.commandName ?? null
2666
2887
  session.activeCommandPermissions = commandState.permissions ?? null
2667
2888
  session.activeCommandPrompt = commandState.commandPrompt ?? null
2668
- session.activeCapabilityPrompt = selectedCapabilityPrompt(selectedCapabilities)
2669
- session.activeTransientContextPrompt = typeof transientContextPrompt === 'string' && transientContextPrompt.trim()
2670
- ? transientContextPrompt
2671
- : null
2889
+ session.activeCapabilityPrompt = selectedCapabilityPrompt(canonicalSelectedCapabilities)
2890
+ const referencePrompt = contextReferencesPrompt(canonicalContextReferences)
2891
+ session.activeTransientContextPrompt = [
2892
+ typeof transientContextPrompt === 'string' && transientContextPrompt.trim() ? transientContextPrompt : null,
2893
+ referencePrompt,
2894
+ ].filter(Boolean).join('\n\n') || null
2672
2895
 
2673
2896
  // Fire and forget — events come through eventBus
2674
2897
  session.agent.prompt(userMessage).catch((err) => {
@@ -2744,8 +2967,14 @@ export async function continueSession(sessionId, modelAccessContext = null) {
2744
2967
  }
2745
2968
 
2746
2969
  const lastUserMessage = messages[lastUserIndex]
2747
- const commandState = await resolveCommandState(session, lastUserMessage)
2748
- const continuedUserMessage = prepareCloudUserMessage(session, commandState.userMessage ?? lastUserMessage)
2970
+ const canonicalContextReferences = await validateContextReferences(contextReferencesFromMessage(lastUserMessage), session)
2971
+ const canonicalSelectedCapabilities = normalizeSelectedCapabilities(selectedCapabilitiesFromMessage(lastUserMessage))
2972
+ const canonicalLastUserMessage = withCanonicalSelectedCapabilities(
2973
+ withCanonicalContextReferences(lastUserMessage, canonicalContextReferences),
2974
+ canonicalSelectedCapabilities,
2975
+ )
2976
+ const commandState = await resolveCommandState(session, canonicalLastUserMessage)
2977
+ const continuedUserMessage = prepareCloudUserMessage(session, commandState.userMessage ?? canonicalLastUserMessage)
2749
2978
  const trimmedMessages = messages.slice(0, lastUserIndex).concat(continuedUserMessage)
2750
2979
  updateSessionMessages(session, trimmedMessages)
2751
2980
  const compactedUpToIndex = Number(session.contextCompaction?.compactedUpToIndex) || 0
@@ -2764,7 +2993,8 @@ export async function continueSession(sessionId, modelAccessContext = null) {
2764
2993
  session.activeCommandName = commandState.commandName ?? null
2765
2994
  session.activeCommandPermissions = commandState.permissions ?? null
2766
2995
  session.activeCommandPrompt = commandState.commandPrompt ?? null
2767
- session.activeCapabilityPrompt = null
2996
+ session.activeCapabilityPrompt = selectedCapabilityPrompt(canonicalSelectedCapabilities)
2997
+ session.activeTransientContextPrompt = contextReferencesPrompt(canonicalContextReferences)
2768
2998
 
2769
2999
  session.agent.continue().catch((err) => {
2770
3000
  logger.error(`Agent continue error for session ${sessionId}:`, err, { sessionId })
@@ -2774,6 +3004,7 @@ export async function continueSession(sessionId, modelAccessContext = null) {
2774
3004
  session.activeCommandPermissions = null
2775
3005
  session.activeCommandPrompt = null
2776
3006
  session.activeCapabilityPrompt = null
3007
+ session.activeTransientContextPrompt = null
2777
3008
  })
2778
3009
 
2779
3010
  return { sessionId, status: 'running' }
@@ -2972,6 +3203,7 @@ export function getSessionState(sessionId) {
2972
3203
  contextUsage: getSessionContextUsage(session),
2973
3204
  pendingToolApproval: getPendingApprovalForSession(session.sessionId),
2974
3205
  pendingAutoCompactApproval: getPendingAutoCompactApprovalForSession(session.sessionId),
3206
+ pendingAsk: getPendingAskForSession(session.sessionId),
2975
3207
  acpSession: session.harness === AGENT_HARNESS_OPENCODE ? session.agent.state.acpSession : undefined,
2976
3208
  isStreaming: session.abortPending ? false : session.agent.state.isStreaming,
2977
3209
  errorMessage: session.agent.state.errorMessage,
@@ -3228,6 +3460,30 @@ export function rejectAutoCompact(sessionId, approvalId) {
3228
3460
  return { rejected: true, approvalId }
3229
3461
  }
3230
3462
 
3463
+ /**
3464
+ * Resolve a pending ask_user call with the user's answers (or a skip).
3465
+ * `answers` is an array aligned with the ask's questions:
3466
+ * `[{ choices: string[], custom?: string }]`.
3467
+ */
3468
+ export function answerAsk(sessionId, askId, { answers, skipped = false } = {}) {
3469
+ const ask = pendingAsks.get(askId)
3470
+ if (!ask || ask.sessionId !== sessionId) {
3471
+ throw Object.assign(new Error('No pending ask for this session'), { statusCode: 404 })
3472
+ }
3473
+ const normalizedAnswers = (Array.isArray(answers) ? answers : []).slice(0, ask.questions.length).map((answer) => ({
3474
+ choices: (Array.isArray(answer?.choices) ? answer.choices : [])
3475
+ .filter((choice) => typeof choice === 'string')
3476
+ .map((choice) => choice.slice(0, 500))
3477
+ .slice(0, 8),
3478
+ ...(typeof answer?.custom === 'string' && answer.custom.trim()
3479
+ ? { custom: answer.custom.slice(0, 4000) }
3480
+ : {}),
3481
+ }))
3482
+ if (skipped) ask.finish({ skipped: true })
3483
+ else ask.finish({ answers: normalizedAnswers })
3484
+ return { answered: true, askId, skipped: !!skipped }
3485
+ }
3486
+
3231
3487
  export function abortToolCall(sessionId, toolCallId) {
3232
3488
  const session = agentSessions.get(sessionId)
3233
3489
  if (!session) {
@@ -31,6 +31,7 @@ export const planAllowedTools = new Set([
31
31
  'activate_skill',
32
32
  'read_skill_resource',
33
33
  'run_subagent',
34
+ 'ask_user',
34
35
  ])
35
36
 
36
37
  export const safeReadTools = new Set([
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Ask-user store — shared pending-question state.
3
+ *
4
+ * The Promise-based ask flow (createAskUserPromise) lives in agent-manager.mjs
5
+ * because it depends on the agent event buses; this module only holds the
6
+ * pending queue, mirroring approval-store.mjs.
7
+ */
8
+
9
+ // Waiting for a user decision is not a security gate — allow generous time
10
+ // before resolving as skipped so the agent can continue on its own.
11
+ export const ASK_TIMEOUT_MS = 30 * 60 * 1000 // 30 minutes
12
+
13
+ /** askId → { finish, sessionId, toolCallId, questions, requestedAt, expiresAt } */
14
+ export const pendingAsks = new Map()
15
+
16
+ export function getPendingAskForSession(sessionId) {
17
+ for (const [askId, ask] of pendingAsks) {
18
+ if (ask.sessionId === sessionId) {
19
+ return {
20
+ askId,
21
+ toolCallId: ask.toolCallId,
22
+ questions: ask.questions,
23
+ requestedAt: ask.requestedAt,
24
+ expiresAt: ask.expiresAt,
25
+ }
26
+ }
27
+ }
28
+ return null
29
+ }
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Pure helpers (no dependencies — unit-tested directly)
33
+ // ---------------------------------------------------------------------------
34
+
35
+ /** Normalize ask_user params into a bounded questions list (robust against model shorthand). */
36
+ export function normalizeAskQuestions(params) {
37
+ const rawQuestions = Array.isArray(params?.questions)
38
+ ? params.questions
39
+ : (typeof params?.question === 'string' ? [{ ...params }] : [])
40
+ return rawQuestions
41
+ .filter((q) => q && typeof q.question === 'string' && q.question.trim())
42
+ .slice(0, 4)
43
+ .map((q) => ({
44
+ question: String(q.question),
45
+ multiSelect: q.multiSelect === true,
46
+ allowCustom: q.allowCustom === false ? false : true,
47
+ options: (Array.isArray(q.options) ? q.options : [])
48
+ .filter((option) => option && typeof option.label === 'string' && option.label.trim())
49
+ .slice(0, 4)
50
+ .map((option) => ({
51
+ label: String(option.label),
52
+ ...(typeof option.description === 'string' && option.description ? { description: option.description } : {}),
53
+ })),
54
+ }))
55
+ }
56
+
57
+ /** Render the tool-result text returned to the model. */
58
+ export function formatAskResult(questions, answers, skipped, reason) {
59
+ if (skipped) {
60
+ const note = reason === 'timeout'
61
+ ? '(等待超时)'
62
+ : reason === 'aborted'
63
+ ? '(运行被停止)'
64
+ : reason === 'no-questions'
65
+ ? '(问题列表无效)'
66
+ : '(用户跳过)'
67
+ return `用户没有回答这些问题${note}。请按你的默认方案继续,不要再重复追问。`
68
+ }
69
+ const lines = questions.map((q, index) => {
70
+ const answer = answers?.[index]
71
+ if (!answer) return `${index + 1}. ${q.question} → 用户未回答`
72
+ const choiceText = Array.isArray(answer.choices) && answer.choices.length ? answer.choices.join('、') : ''
73
+ const customText = typeof answer.custom === 'string' && answer.custom.trim() ? answer.custom.trim() : ''
74
+ const text = [choiceText, customText].filter(Boolean).join(' 补充:')
75
+ return `${index + 1}. ${q.question} → ${text || '用户未回答'}`
76
+ })
77
+ return `用户的回答:\n${lines.join('\n')}`
78
+ }