@shawnstack/quickforge 1.6.7 → 1.6.9

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 (168) hide show
  1. package/README.md +10 -10
  2. package/dist/assets/AgentProfilesPage-5eqU0Cjk.js +1 -0
  3. package/dist/assets/ChatPanelHost-BNFeNRy-.js +244 -0
  4. package/dist/assets/{PluginsPage-DDxjlhiK.js → PluginsPage-C5OClNdT.js} +1 -1
  5. package/dist/assets/ScheduledTasksPage-yT10oaaM.js +2 -0
  6. package/dist/assets/{SettingsWorkspacePage-B_7o3noo.js → SettingsWorkspacePage-DuXwIeLd.js} +346 -335
  7. package/dist/assets/SharedConversationPage-ClpUSa0F.js +1 -0
  8. package/dist/assets/TerminalDock-BmL-FeKv.js +2 -0
  9. package/dist/assets/WorkspaceInspector-yH23pXpj.js +13 -0
  10. package/dist/assets/abnfDiagram-VRR7QNED-Dl_Fvlqg.js +1 -0
  11. package/dist/assets/arc-Bhg4c0ht.js +1 -0
  12. package/dist/assets/architecture-TIHT7OUA-BgP-IMeo.js +1 -0
  13. package/dist/assets/architectureDiagram-ZJ3FMSHR-BuojN27B.js +36 -0
  14. package/dist/assets/array-CYkMkqnU.js +1 -0
  15. package/dist/assets/blockDiagram-677ZJIJ3-alDLbrR1.js +132 -0
  16. package/dist/assets/c4Diagram-LMCZKHZV-D1lKa4VT.js +10 -0
  17. package/dist/assets/channel-Rz20dKTK.js +1 -0
  18. package/dist/assets/chunk-2Q5K7J3B-Cb14ZF_k.js +1 -0
  19. package/dist/assets/chunk-32BRIVSS-CwWLNjMZ.js +1 -0
  20. package/dist/assets/chunk-52WLFC77-Be569m7d.js +10 -0
  21. package/dist/assets/chunk-5FCAYU7R-BSvoiWV1.js +1 -0
  22. package/dist/assets/chunk-5HE753X5-CRgsy8WS.js +1 -0
  23. package/dist/assets/chunk-5JV3BV7I-CdW477UR.js +1 -0
  24. package/dist/assets/chunk-5TONJI2A-CdtKzSmp.js +2 -0
  25. package/dist/assets/chunk-5VM5RSS4-BKjjssLZ.js +15 -0
  26. package/dist/assets/chunk-7BUUIJ7U-ByAXiXdn.js +1 -0
  27. package/dist/assets/chunk-BIQX33UG-DeG8SCT1.js +1 -0
  28. package/dist/assets/chunk-C7G6YPKG-D_zHT68Q.js +1 -0
  29. package/dist/assets/chunk-CQNSW5MT-CHpHoUSu.js +1 -0
  30. package/dist/assets/chunk-CYSBUYHQ-CRP50Vov.js +1 -0
  31. package/dist/assets/chunk-EMLP6XTP-DX6uQQ2v.js +1 -0
  32. package/dist/assets/chunk-EX3LRPZG-DAJRNkHR.js +231 -0
  33. package/dist/assets/chunk-FWX5IMBZ-XbM5yomC.js +2 -0
  34. package/dist/assets/chunk-HOUHSVGY-Ci0ljnb1.js +1 -0
  35. package/dist/assets/chunk-ICXQ74PX-Dy2WJ71z.js +2 -0
  36. package/dist/assets/chunk-JG7HCLWE-2PrkzHkt.js +2 -0
  37. package/dist/assets/chunk-JWPE2WC7-D32uacSM.js +1 -0
  38. package/dist/assets/chunk-KEIR6QF5-77hsebd7.js +161 -0
  39. package/dist/assets/chunk-MOJQB5TN-DIAFLtiS.js +88 -0
  40. package/dist/assets/chunk-MOZMSUNE-BbscOaEz.js +1 -0
  41. package/dist/assets/chunk-OGEWGWER-ivBVxzKe.js +1 -0
  42. package/dist/assets/chunk-OSBZ3O6U-frD0hkdx.js +1 -0
  43. package/dist/assets/chunk-PUDLZKDR-B-37Rxae.js +156 -0
  44. package/dist/assets/chunk-Q4XR5HBZ-Y90m2RTe.js +11 -0
  45. package/dist/assets/chunk-QBLGF6JB-DixBqslB.js +1 -0
  46. package/dist/assets/chunk-R7FJI6CG-DXS1UXn_.js +1 -0
  47. package/dist/assets/chunk-RYQCIY6F-n4JxhNd0.js +1 -0
  48. package/dist/assets/chunk-U6XO7XAA-yHlgxMmn.js +2 -0
  49. package/dist/assets/chunk-V7JOEXUC-Z6kuZfbl.js +206 -0
  50. package/dist/assets/chunk-VAUOI2AC-BOZvd2Sn.js +1 -0
  51. package/dist/assets/chunk-VR4S4FIN-Cs_SIQma.js +1 -0
  52. package/dist/assets/chunk-WYO6CB5R-CGExYbV-.js +128 -0
  53. package/dist/assets/chunk-XXDRQBXY-CD_EGCtT.js +1 -0
  54. package/dist/assets/chunk-Y2CYZVJY-CQltPwYk.js +1 -0
  55. package/dist/assets/chunk-YOTPTUD7-DopPXWSl.js +1 -0
  56. package/dist/assets/chunk-ZGVPDNZ5-CJHOeqMb.js +62 -0
  57. package/dist/assets/chunk-ZIRB5QZD-BBVc5mf6.js +32 -0
  58. package/dist/assets/classDiagram-OUVF2IWQ-C_ozTKCt.js +1 -0
  59. package/dist/assets/classDiagram-v2-EOCWNBFH-FOfFbbZF.js +1 -0
  60. package/dist/assets/cose-bilkent-JH36ORCC-DLjV-dTm.js +1 -0
  61. package/dist/assets/cynefin-VYW2F7L2-BzPhLtd5.js +1 -0
  62. package/dist/assets/cynefinDiagram-TSTJHNR4-BfH3zVS-.js +62 -0
  63. package/dist/assets/cytoscape.esm-DwdZnGOm.js +321 -0
  64. package/dist/assets/dagre-CY3D84Cl.js +1 -0
  65. package/dist/assets/dagre-VKFMJZFB-BIhwZCIZ.js +4 -0
  66. package/dist/assets/defaultLocale-Beh6XjaL.js +1 -0
  67. package/dist/assets/diagram-FQU43EPY-BHnzNM2w.js +3 -0
  68. package/dist/assets/diagram-G47NLZAW-C7-ZlmFT.js +24 -0
  69. package/dist/assets/diagram-NH7WQ7WH-C-P9Y7IR.js +24 -0
  70. package/dist/assets/diagram-OA4YK3LP-Ci5lZ_T5.js +30 -0
  71. package/dist/assets/diagram-WEI45ONY-C1xuK2bs.js +41 -0
  72. package/dist/assets/dist-DelZBZpK.js +1 -0
  73. package/dist/assets/ebnfDiagram-CCIWWBDH-DKQ5NWMD.js +1 -0
  74. package/dist/assets/erDiagram-Q63AITRT-HAWySadP.js +85 -0
  75. package/dist/assets/eventmodeling-45OFAUF4-BcgGbVJ-.js +1 -0
  76. package/dist/assets/flowDiagram-23GEKE2U-dtkzcC8E.js +1 -0
  77. package/dist/assets/ganttDiagram-NO4QXBWP-C1hwIcOA.js +292 -0
  78. package/dist/assets/gitGraph-TEB2WS4Q-VByWboBU.js +1 -0
  79. package/dist/assets/gitGraphDiagram-IHSO6WYX-DYUYxs1R.js +106 -0
  80. package/dist/assets/graphlib-CAISHZh4.js +1 -0
  81. package/dist/assets/icons-C7j5jdKo.js +1 -0
  82. package/dist/assets/index-Qof73j0o.css +3 -0
  83. package/dist/assets/index-_ut7awrd.js +63 -0
  84. package/dist/assets/info-DKCQHKI2-CZeDLZxx.js +1 -0
  85. package/dist/assets/infoDiagram-FWYZ7A6U-CnIL6tv4.js +2 -0
  86. package/dist/assets/init-Rr1s_RiX.js +1 -0
  87. package/dist/assets/ishikawaDiagram-FXEZZL3T-B7WXNycQ.js +70 -0
  88. package/dist/assets/journeyDiagram-5HDEW3XC-D4HvioyY.js +139 -0
  89. package/dist/assets/kanban-definition-HUTT4EX6-CDs763eS.js +89 -0
  90. package/dist/assets/line-ikKkQiFp.js +1 -0
  91. package/dist/assets/linear-GqPyxQkN.js +1 -0
  92. package/dist/assets/map-CJwjCvwr.js +1 -0
  93. package/dist/assets/{mcp-servers-dialog-CJ4HKWVZ.js → mcp-servers-dialog-DC2Zfojn.js} +2 -2
  94. package/dist/assets/mermaid-parser.core-CoE8cccr.js +4 -0
  95. package/dist/assets/mermaid-renderer-BmbSAmpG.js +2 -0
  96. package/dist/assets/mermaid.core-DlFSL90j.js +11 -0
  97. package/dist/assets/mindmap-definition-LN4V7U3C-DLdq0K9C.js +96 -0
  98. package/dist/assets/{monaco-FCb94ino.js → monaco-CdOyGlWD.js} +1 -1
  99. package/dist/assets/ordinal-CibZU3PM.js +1 -0
  100. package/dist/assets/otel-BNGM50C-.js +3 -0
  101. package/dist/assets/packet-7NZHBO7P-decrrxyX.js +1 -0
  102. package/dist/assets/path-BAQ3hXlG.js +1 -0
  103. package/dist/assets/pegDiagram-2B236MQR-Cm1kPRC2.js +1 -0
  104. package/dist/assets/pi-ai-Cr3rpCXT.js +131 -0
  105. package/dist/assets/pi-web-ui-DoRGtREc.js +2803 -0
  106. package/dist/assets/pie-RZYD4A2V-J1CasQZl.js +1 -0
  107. package/dist/assets/pieDiagram-ENE6RG2P-Df05wjjo.js +39 -0
  108. package/dist/assets/quadrantDiagram-ABIIQ3AL-BjCwGxJC.js +7 -0
  109. package/dist/assets/radar-I7S5WNFK-B-sZytUA.js +1 -0
  110. package/dist/assets/railroad-3IZDKUUU-BSaqNPm8.js +1 -0
  111. package/dist/assets/railroad-abnf-AHOZXSZD-lzZR7PxT.js +1 -0
  112. package/dist/assets/railroad-ebnf-EBAXGLYW-DQtz1Fh9.js +1 -0
  113. package/dist/assets/railroad-peg-LSFZ7HO6-CpMsuGY3.js +1 -0
  114. package/dist/assets/railroadDiagram-RFXS5EU6-CTUvpKkv.js +1 -0
  115. package/dist/assets/{react-vendor-BR9MG2D-.js → react-vendor-VqdnQHHS.js} +1 -1
  116. package/dist/assets/requirementDiagram-TGXJPOKE-BU1kvEku.js +84 -0
  117. package/dist/assets/rough.esm-Djo4Abte.js +1 -0
  118. package/dist/assets/sankeyDiagram-HTMAVEWB-BHmvxVqH.js +40 -0
  119. package/dist/assets/sequenceDiagram-DBY2YBRQ-CTpwICgg.js +162 -0
  120. package/dist/assets/sizeCapture-X5ZJPWSS-BbLhGaa4.js +1 -0
  121. package/dist/assets/{skills-dialog-CmCLl7K_.js → skills-dialog-Mnj8fZwq.js} +1 -1
  122. package/dist/assets/src-B4Tvg3gT.js +1 -0
  123. package/dist/assets/stateDiagram-2N3HPSRC-BxuDwIgT.js +1 -0
  124. package/dist/assets/stateDiagram-v2-6OUMAXLB-BGLshxcY.js +1 -0
  125. package/dist/assets/swimlanes-5IMT3BWC-EkZsiylV.js +2 -0
  126. package/dist/assets/swimlanesDiagram-G3AALYLV-CqdqxA-b.js +8 -0
  127. package/dist/assets/timeline-definition-FHXFAJF6-C9ewOCTa.js +120 -0
  128. package/dist/assets/treeView-QDETBFTQ-Byb71heW.js +1 -0
  129. package/dist/assets/treemap-6X3UGDF4-CMV1SJsq.js +1 -0
  130. package/dist/assets/vennDiagram-L72KCM5P-CR49Y4Lp.js +34 -0
  131. package/dist/assets/wardley-OPB4EBWU-DtCHtr2V.js +1 -0
  132. package/dist/assets/wardleyDiagram-EHGQE667-Cn0GgwWi.js +78 -0
  133. package/dist/assets/xychartDiagram-FW5EYKEG-DlmPNAvd.js +7 -0
  134. package/dist/index.html +8 -8
  135. package/package.json +4 -3
  136. package/server/agent-manager.mjs +174 -9
  137. package/server/agent-profile-files.mjs +86 -6
  138. package/server/agent-profile-schema.mjs +197 -0
  139. package/server/agent-profiles.mjs +172 -41
  140. package/server/ai-http-logger.mjs +1 -1
  141. package/server/auto-compaction.mjs +2 -2
  142. package/server/project-route-context.mjs +25 -0
  143. package/server/routes/agent-profiles.mjs +10 -4
  144. package/server/routes/models.mjs +1 -1
  145. package/server/routes/project.mjs +6 -10
  146. package/server/routes/scheduled-tasks.mjs +1 -1
  147. package/server/routes/workspace.mjs +185 -12
  148. package/server/session-utils.mjs +1 -1
  149. package/server/storage.mjs +34 -0
  150. package/server/subagents.mjs +64 -0
  151. package/server/tools/definitions.mjs +17 -1
  152. package/server/utils/platform.mjs +25 -2
  153. package/dist/assets/AgentProfilesPage-XJdWmkQb.js +0 -1
  154. package/dist/assets/ChatPanelHost-BrUHIzBw.js +0 -242
  155. package/dist/assets/ScheduledTasksPage-Bw-pSpbv.js +0 -2
  156. package/dist/assets/SharedConversationPage-B5DPQla3.js +0 -1
  157. package/dist/assets/TerminalDock-CVObjSM6.js +0 -2
  158. package/dist/assets/WorkspaceInspector-Dk5887WR.js +0 -3
  159. package/dist/assets/WorkspaceReaderDialog-D-_BBvud.js +0 -1
  160. package/dist/assets/diff-line-counts-DagL0jQr.js +0 -10
  161. package/dist/assets/icons-DHoaB5uq.js +0 -1
  162. package/dist/assets/index-C77QCHE9.css +0 -3
  163. package/dist/assets/index-DeUvsbzW.js +0 -63
  164. package/dist/assets/pi-ai-Cx633yhb.js +0 -134
  165. package/dist/assets/pi-web-ui-DFNE2m5b.js +0 -2770
  166. package/dist/assets/useAppTheme-BwZ5OEf3.js +0 -1
  167. /package/dist/assets/{plugin-api-UKg_cgSG.js → plugin-api-BbUkYm3Y.js} +0 -0
  168. /package/dist/assets/{xterm-BtSXYfUR.js → xterm-CGiQzDiR.js} +0 -0
@@ -1,17 +1,36 @@
1
+ import { existsSync, promises as fs } from 'node:fs'
2
+ import path from 'node:path'
1
3
  import { randomUUID } from 'node:crypto'
2
- import { readStore, atomicUpdate } from './storage.mjs'
3
- import { subagentDefinitions } from './subagents.mjs'
4
+ import { readStore, userAgentsDir, writeStore } from './storage.mjs'
5
+ import { builtinSubagentProfilePath, ensureBuiltinSubagentMarkdownFiles, subagentDefinitions } from './subagents.mjs'
4
6
  import { workspaceTools } from './tools/definitions.mjs'
5
7
  import { defaultGlobalWorkspaceContext, projectContextFromId } from './project-config.mjs'
6
- import { loadFileAgentProfiles } from './agent-profile-files.mjs'
8
+ import {
9
+ deleteUserAgentProfileMarkdown,
10
+ loadFileAgentProfiles,
11
+ userAgentProfileFilePath,
12
+ writeUserAgentProfileMarkdown,
13
+ } from './agent-profile-files.mjs'
14
+ import {
15
+ AGENT_PROFILE_TOOL_NAMES,
16
+ inferCapabilityPolicy,
17
+ modelReferenceSnapshot,
18
+ normalizeAgentProfileThinkingLevel,
19
+ normalizeCapabilityPolicy,
20
+ normalizeModelReference,
21
+ validateAgentProfileTools,
22
+ validateModelReference,
23
+ } from './agent-profile-schema.mjs'
7
24
 
8
25
  const STORE = 'custom-agents'
26
+ const BUILTIN_OVERRIDES_STORE = 'agent-profile-overrides'
9
27
  const RESERVED_NAMES = new Set(subagentDefinitions.map((definition) => definition.name))
10
- export const AGENT_PROFILE_TOOL_NAMES = ['read_file', 'grep_files', 'write_file', 'edit_file', 'run_command']
11
28
  const allowedToolNames = new Set(AGENT_PROFILE_TOOL_NAMES)
12
29
  const nameRegex = /^[a-z][a-z0-9_-]{1,39}$/
13
30
  const DEFAULT_MAX_RUNTIME_MS = 30 * 60 * 1000
14
31
  const DEFAULT_MAX_TOOL_CALLS = 300
32
+ const MIGRATION_MARKER_FILE = path.join(userAgentsDir, '.custom-agents-migrated.json')
33
+ let customAgentMigrationPromise = null
15
34
 
16
35
  function requestError(message, statusCode = 400) {
17
36
  const error = new Error(message)
@@ -46,7 +65,7 @@ function normalizeOptionalRuntime(value, fallback) {
46
65
  return Math.min(Math.max(Math.round(parsed), 1000), DEFAULT_MAX_RUNTIME_MS)
47
66
  }
48
67
 
49
- function builtinProfileFromSubagent(definition) {
68
+ function builtinProfileFromSubagent(definition, override = null) {
50
69
  return {
51
70
  id: definition.name,
52
71
  name: definition.name,
@@ -54,20 +73,28 @@ function builtinProfileFromSubagent(definition) {
54
73
  description: definition.description || '',
55
74
  systemPrompt: definition.systemPrompt || '',
56
75
  allowedTools: [...definition.allowedTools],
76
+ capabilityPolicy: definition.capabilityPolicy || inferCapabilityPolicy(definition.allowedTools),
77
+ model: modelReferenceSnapshot(override?.model || definition.model),
78
+ thinkingLevel: 'inherit',
79
+ lifecycle: 'builtin',
80
+ managed: true,
57
81
  maxRuntimeMs: definition.maxRuntimeMs || DEFAULT_MAX_RUNTIME_MS,
58
82
  maxToolCalls: definition.maxToolCalls || DEFAULT_MAX_TOOL_CALLS,
59
83
  enabledAsSubagent: true,
60
84
  builtin: true,
61
85
  source: 'builtin',
62
86
  readonly: true,
87
+ filePath: builtinSubagentProfilePath(definition.name),
88
+ relativePath: `~/.quickforge/agents/builtin/${definition.name}.md`,
63
89
  allowFileMutations: definition.allowFileMutations === true,
64
90
  createdAt: 'builtin',
65
91
  updatedAt: 'builtin',
66
92
  }
67
93
  }
68
94
 
69
- export function listBuiltinAgentProfiles() {
70
- return subagentDefinitions.map(builtinProfileFromSubagent)
95
+ export async function listBuiltinAgentProfiles() {
96
+ const overrides = await readStore(BUILTIN_OVERRIDES_STORE).catch(() => ({}))
97
+ return subagentDefinitions.map((definition) => builtinProfileFromSubagent(definition, overrides?.[definition.name]))
71
98
  }
72
99
 
73
100
  function normalizeProfileInput(input, existing = null, { creating = false } = {}) {
@@ -85,6 +112,13 @@ function normalizeProfileInput(input, existing = null, { creating = false } = {}
85
112
  for (const toolName of allowedTools) {
86
113
  if (!allowedToolNames.has(toolName)) throw requestError(`Unsupported tool for custom agent: ${toolName}`)
87
114
  }
115
+ const capabilityPolicy = normalizeCapabilityPolicy(
116
+ input?.capabilityPolicy ?? existing?.capabilityPolicy,
117
+ allowedTools,
118
+ )
119
+ validateAgentProfileTools(allowedTools, capabilityPolicy)
120
+ const model = validateModelReference(normalizeModelReference(input?.model ?? existing?.model))
121
+ const thinkingLevel = normalizeAgentProfileThinkingLevel(input?.thinkingLevel ?? existing?.thinkingLevel)
88
122
 
89
123
  return {
90
124
  id: existing?.id || `agent-${randomUUID()}`,
@@ -93,12 +127,18 @@ function normalizeProfileInput(input, existing = null, { creating = false } = {}
93
127
  description: String(input?.description ?? existing?.description ?? '').trim().slice(0, 500),
94
128
  systemPrompt: String(input?.systemPrompt ?? existing?.systemPrompt ?? '').trim(),
95
129
  allowedTools,
130
+ capabilityPolicy,
131
+ model,
132
+ thinkingLevel,
133
+ lifecycle: existing?.lifecycle || 'persistent',
96
134
  maxRuntimeMs: normalizeOptionalRuntime(input?.maxRuntimeMs ?? existing?.maxRuntimeMs, DEFAULT_MAX_RUNTIME_MS),
97
135
  maxToolCalls: normalizeOptionalPositiveInteger(input?.maxToolCalls ?? existing?.maxToolCalls, DEFAULT_MAX_TOOL_CALLS, 300),
98
136
  enabledAsSubagent: input?.enabledAsSubagent === undefined ? Boolean(existing?.enabledAsSubagent ?? true) : input.enabledAsSubagent === true,
99
137
  builtin: false,
100
- source: 'store',
138
+ source: 'user',
101
139
  readonly: false,
140
+ managed: true,
141
+ managedBy: 'quickforge',
102
142
  allowFileMutations: allowedTools.some((toolName) => toolName === 'write_file' || toolName === 'edit_file'),
103
143
  createdAt: existing?.createdAt || now,
104
144
  updatedAt: now,
@@ -110,6 +150,74 @@ async function readCustomAgentMap() {
110
150
  return data && typeof data === 'object' ? data : {}
111
151
  }
112
152
 
153
+ function migrationReportEntry(profile, file, extra = {}) {
154
+ return {
155
+ id: profile.id,
156
+ fromName: profile.name,
157
+ toName: path.basename(file, '.md'),
158
+ file,
159
+ ...extra,
160
+ }
161
+ }
162
+
163
+ function uniqueMigratedName(name, usedNames) {
164
+ const base = String(name || '').trim().toLowerCase()
165
+ const candidates = [base, `${base}-store`, `${base}-custom`]
166
+ for (const candidate of candidates) {
167
+ if (nameRegex.test(candidate) && !usedNames.has(candidate) && !RESERVED_NAMES.has(candidate) && !existsSync(userAgentProfileFilePath(candidate))) return candidate
168
+ }
169
+ for (let index = 2; index < 100; index += 1) {
170
+ const candidate = `${base.slice(0, 34)}-${index}`
171
+ if (nameRegex.test(candidate) && !usedNames.has(candidate) && !RESERVED_NAMES.has(candidate) && !existsSync(userAgentProfileFilePath(candidate))) return candidate
172
+ }
173
+ return null
174
+ }
175
+
176
+ async function migrateCustomAgentStoreToFiles() {
177
+ if (existsSync(MIGRATION_MARKER_FILE)) return
178
+ await fs.mkdir(userAgentsDir, { recursive: true })
179
+ const custom = Object.values(await readCustomAgentMap())
180
+ const report = { migratedAt: new Date().toISOString(), sourceStore: STORE, migrated: [], conflicts: [], skipped: [] }
181
+ if (custom.length === 0) {
182
+ await fs.writeFile(MIGRATION_MARKER_FILE, JSON.stringify(report, null, 2), 'utf8')
183
+ return
184
+ }
185
+
186
+ const usedNames = new Set()
187
+ for (const item of custom) {
188
+ try {
189
+ const profile = normalizeProfileInput({ ...item, id: item.id }, item)
190
+ let targetName = profile.name
191
+ let conflictReason = ''
192
+ if (RESERVED_NAMES.has(targetName) || existsSync(userAgentProfileFilePath(targetName)) || usedNames.has(targetName)) {
193
+ conflictReason = RESERVED_NAMES.has(targetName) ? 'reserved builtin name or duplicate' : 'target file already exists or duplicate'
194
+ targetName = uniqueMigratedName(targetName, usedNames)
195
+ }
196
+ if (!targetName) {
197
+ report.skipped.push({ id: item?.id, fromName: item?.name, reason: 'unable to allocate unique markdown file name' })
198
+ continue
199
+ }
200
+ const migrated = { ...profile, name: targetName, source: 'user', readonly: false, managed: true, managedBy: 'quickforge' }
201
+ const file = await writeUserAgentProfileMarkdown(migrated)
202
+ usedNames.add(targetName)
203
+ const entry = migrationReportEntry(item, file, conflictReason ? { reason: conflictReason } : {})
204
+ if (conflictReason) report.conflicts.push(entry)
205
+ else report.migrated.push(entry)
206
+ } catch (error) {
207
+ report.skipped.push({ id: item?.id, fromName: item?.name, reason: error?.message || String(error) })
208
+ }
209
+ }
210
+ await fs.writeFile(MIGRATION_MARKER_FILE, JSON.stringify(report, null, 2), 'utf8')
211
+ }
212
+
213
+ async function ensureCustomAgentStoreMigrated() {
214
+ if (existsSync(MIGRATION_MARKER_FILE)) return
215
+ customAgentMigrationPromise ||= migrateCustomAgentStoreToFiles().finally(() => {
216
+ customAgentMigrationPromise = null
217
+ })
218
+ await customAgentMigrationPromise
219
+ }
220
+
113
221
  async function resolveWorkspaceRoot(options = {}) {
114
222
  if (options.workspaceRoot) return options.workspaceRoot
115
223
  if (options.projectId) {
@@ -146,10 +254,11 @@ function mergeProfiles({ builtin = [], file = [], custom = [] }) {
146
254
  }
147
255
 
148
256
  export async function listAgentProfiles(options = {}) {
149
- const custom = Object.values(await readCustomAgentMap())
257
+ await ensureBuiltinSubagentMarkdownFiles()
258
+ await ensureCustomAgentStoreMigrated()
150
259
  const workspaceRoot = await resolveWorkspaceRoot(options)
151
260
  const file = await loadFileAgentProfiles(workspaceRoot, { reservedNames: RESERVED_NAMES })
152
- const profiles = mergeProfiles({ builtin: listBuiltinAgentProfiles(), file, custom })
261
+ const profiles = mergeProfiles({ builtin: await listBuiltinAgentProfiles(), file, custom: [] })
153
262
  return options.includeDisabled ? profiles : profiles.filter((profile) => profile.enabledAsSubagent || profile.builtin || profile.enabledAsSubagent === false)
154
263
  }
155
264
 
@@ -163,46 +272,60 @@ export async function getAgentProfile(idOrName, options = {}) {
163
272
  const profiles = await listAgentProfiles({ ...options, includeDisabled: true })
164
273
  const byName = profiles.find((profile) => profile.name === key)
165
274
  if (byName) return byName
166
- const custom = Object.values(await readCustomAgentMap())
167
- return custom.find((profile) => profile?.id === key) || profiles.find((profile) => profile.id === key) || null
275
+ return profiles.find((profile) => String(profile.id || '').toLowerCase() === key) || null
276
+ }
277
+
278
+ function assertEditableUserProfile(profile) {
279
+ if (!profile) throw requestError('Agent not found', 404)
280
+ if (profile.builtin) throw requestError('Built-in agents cannot be modified', 403)
281
+ if (profile.readonly) throw requestError('Read-only agents cannot be modified from the API', 403)
282
+ if (profile.source !== 'user' || profile.managedBy !== 'quickforge') throw requestError('Only QuickForge-managed user agents can be modified from the API', 403)
283
+ }
284
+
285
+ async function assertNameAvailable(name, currentId = null) {
286
+ if (RESERVED_NAMES.has(name)) throw requestError(`Agent name is reserved: ${name}`, 409)
287
+ const existing = await listAgentProfiles({ includeDisabled: true })
288
+ const conflict = existing.find((profile) => profile.name === name && profile.id !== currentId)
289
+ if (conflict) throw requestError(`Agent name already exists: ${name}`, 409)
168
290
  }
169
291
 
170
292
  export async function createCustomAgentProfile(input) {
171
- let created = null
172
- await atomicUpdate(STORE, (data) => {
173
- const map = data && typeof data === 'object' ? data : {}
174
- const profile = normalizeProfileInput(input, null, { creating: true })
175
- if (Object.values(map).some((item) => item?.name === profile.name)) throw requestError(`Agent name already exists: ${profile.name}`, 409)
176
- created = profile
177
- map[profile.id] = profile
178
- return map
179
- })
180
- return created
293
+ await ensureCustomAgentStoreMigrated()
294
+ const profile = normalizeProfileInput(input, null, { creating: true })
295
+ await assertNameAvailable(profile.name)
296
+ const file = await writeUserAgentProfileMarkdown(profile)
297
+ return { ...profile, filePath: file, relativePath: `~/.quickforge/agents/${profile.name}.md` }
181
298
  }
182
299
 
183
300
  export async function updateCustomAgentProfile(id, patch) {
184
- let updated = null
185
- await atomicUpdate(STORE, (data) => {
186
- const map = data && typeof data === 'object' ? data : {}
187
- const current = map[id]
188
- if (!current) throw requestError('Agent not found', 404)
189
- const next = normalizeProfileInput(patch, current)
190
- if (RESERVED_NAMES.has(next.name)) throw requestError(`Agent name is reserved: ${next.name}`, 409)
191
- if (Object.values(map).some((item) => item?.id !== id && item?.name === next.name)) throw requestError(`Agent name already exists: ${next.name}`, 409)
192
- updated = next
193
- map[id] = next
194
- return map
195
- })
196
- return updated
301
+ await ensureCustomAgentStoreMigrated()
302
+ const current = await getAgentProfile(id)
303
+ assertEditableUserProfile(current)
304
+ const next = normalizeProfileInput(patch, current)
305
+ await assertNameAvailable(next.name, current.id)
306
+ const file = await writeUserAgentProfileMarkdown(next)
307
+ if (next.name !== current.name) await deleteUserAgentProfileMarkdown(current)
308
+ return { ...next, filePath: file, relativePath: `~/.quickforge/agents/${next.name}.md` }
309
+ }
310
+
311
+ export async function updateBuiltinAgentModelOverride(id, modelInput) {
312
+ const current = await getAgentProfile(id)
313
+ if (!current) throw requestError('Agent not found', 404)
314
+ if (!current.builtin) throw requestError('Only built-in agents support model overrides', 400)
315
+ const model = validateModelReference(normalizeModelReference(modelInput))
316
+ const overrides = await readStore(BUILTIN_OVERRIDES_STORE).catch(() => ({}))
317
+ const next = { ...(overrides && typeof overrides === 'object' ? overrides : {}) }
318
+ if (model.mode === 'fixed') next[current.name] = { model }
319
+ else delete next[current.name]
320
+ await writeStore(BUILTIN_OVERRIDES_STORE, next)
321
+ return { ...current, model: modelReferenceSnapshot(model) }
197
322
  }
198
323
 
199
324
  export async function deleteCustomAgentProfile(id) {
200
- await atomicUpdate(STORE, (data) => {
201
- const map = data && typeof data === 'object' ? data : {}
202
- if (!map[id]) throw requestError('Agent not found', 404)
203
- delete map[id]
204
- return map
205
- })
325
+ await ensureCustomAgentStoreMigrated()
326
+ const current = await getAgentProfile(id)
327
+ assertEditableUserProfile(current)
328
+ await deleteUserAgentProfileMarkdown(current)
206
329
  }
207
330
 
208
331
  export function agentProfileSnapshot(profile) {
@@ -214,6 +337,11 @@ export function agentProfileSnapshot(profile) {
214
337
  description: profile.description,
215
338
  systemPrompt: profile.systemPrompt,
216
339
  allowedTools: [...profile.allowedTools],
340
+ capabilityPolicy: profile.capabilityPolicy || inferCapabilityPolicy(profile.allowedTools || []),
341
+ model: modelReferenceSnapshot(profile.model),
342
+ thinkingLevel: normalizeAgentProfileThinkingLevel(profile.thinkingLevel),
343
+ lifecycle: profile.lifecycle || (profile.builtin ? 'builtin' : 'persistent'),
344
+ managed: profile.managed === true,
217
345
  maxRuntimeMs: profile.maxRuntimeMs,
218
346
  maxToolCalls: profile.maxToolCalls,
219
347
  enabledAsSubagent: profile.enabledAsSubagent === true,
@@ -222,9 +350,12 @@ export function agentProfileSnapshot(profile) {
222
350
  readonly: profile.readonly === true || profile.builtin === true,
223
351
  filePath: profile.filePath,
224
352
  relativePath: profile.relativePath,
353
+ warnings: Array.isArray(profile.warnings) ? [...profile.warnings] : undefined,
225
354
  }
226
355
  }
227
356
 
357
+ export { AGENT_PROFILE_TOOL_NAMES } from './agent-profile-schema.mjs'
358
+
228
359
  export function listAvailableAgentTools() {
229
360
  const labels = {
230
361
  read_file: 'Read file',
@@ -2,7 +2,7 @@ import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { AsyncLocalStorage } from 'node:async_hooks'
4
4
  import { randomUUID } from 'node:crypto'
5
- import { streamSimple } from '@earendil-works/pi-ai'
5
+ import { streamSimple } from '@earendil-works/pi-ai/compat'
6
6
  import { logsDir } from './storage.mjs'
7
7
 
8
8
  const PATCH_MARKER = Symbol.for('quickforge.aiHttpLogger.fetchPatched')
@@ -5,7 +5,7 @@ import { estimateContextUsage, shouldCompactContextByPercent } from './context-u
5
5
  export const AUTO_COMPACT_SETTINGS_KEY = 'auto-compact-settings'
6
6
 
7
7
  export const DEFAULT_AUTO_COMPACT_SETTINGS = {
8
- enabled: false,
8
+ enabled: true,
9
9
  thresholdPercent: 80,
10
10
  keepRecentTurns: 2,
11
11
  minSourceChars: 1600,
@@ -24,7 +24,7 @@ function clampNumber(value, fallback, min, max) {
24
24
  export function normalizeAutoCompactSettings(value) {
25
25
  if (!value || typeof value !== 'object') return { ...DEFAULT_AUTO_COMPACT_SETTINGS }
26
26
  return {
27
- enabled: value.enabled === true,
27
+ enabled: value.enabled !== false,
28
28
  thresholdPercent: clampNumber(value.thresholdPercent, DEFAULT_AUTO_COMPACT_SETTINGS.thresholdPercent, 50, 95),
29
29
  keepRecentTurns: clampNumber(value.keepRecentTurns, DEFAULT_AUTO_COMPACT_SETTINGS.keepRecentTurns, 1, 20),
30
30
  minSourceChars: clampNumber(value.minSourceChars, DEFAULT_AUTO_COMPACT_SETTINGS.minSourceChars, 0, 200000),
@@ -0,0 +1,25 @@
1
+ import path from 'node:path'
2
+
3
+ export function resolveRequestedProject(config, projectId, defaultWorkspaceRoot) {
4
+ if (!projectId) return config.projects.find((project) => project.id === config.activeProjectId) ?? config.projects[0]
5
+ if (projectId === 'default') {
6
+ return {
7
+ id: 'default',
8
+ name: 'workspace',
9
+ path: defaultWorkspaceRoot,
10
+ lastOpenedAt: '',
11
+ sortOrder: 0,
12
+ skills: [],
13
+ commandDir: '',
14
+ }
15
+ }
16
+ const project = config.projects.find((item) => item.id === projectId)
17
+ if (project) return project
18
+ const error = new Error('Unknown project')
19
+ error.statusCode = 404
20
+ throw error
21
+ }
22
+
23
+ export function workspaceRootAfterProjectDeletion(activeProject, defaultWorkspaceRoot) {
24
+ return path.resolve(activeProject?.path || defaultWorkspaceRoot)
25
+ }
@@ -1,4 +1,4 @@
1
- import { streamSimple } from '@earendil-works/pi-ai'
1
+ import { streamSimple } from '@earendil-works/pi-ai/compat'
2
2
  import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
3
3
  import { readStore } from '../storage.mjs'
4
4
  import { logger } from '../utils/logger.mjs'
@@ -9,6 +9,7 @@ import {
9
9
  getAgentProfile,
10
10
  listAgentProfiles,
11
11
  listAvailableAgentTools,
12
+ updateBuiltinAgentModelOverride,
12
13
  updateCustomAgentProfile,
13
14
  } from '../agent-profiles.mjs'
14
15
 
@@ -155,9 +156,14 @@ export async function handleAgentProfilesApi(req, res, url) {
155
156
 
156
157
  if (req.method === 'PATCH' || req.method === 'PUT') {
157
158
  const current = await getAgentProfile(id)
158
- if (current?.builtin) throw requestError('Built-in agents cannot be modified', 403)
159
- if (current?.readonly) throw requestError('File-based agents cannot be modified from the API', 403)
160
159
  const body = await readJsonBody(req)
160
+ if (current?.builtin) {
161
+ const keys = Object.keys(body || {})
162
+ if (keys.length !== 1 || keys[0] !== 'model') throw requestError('Built-in agents only allow model updates', 403)
163
+ sendJson(res, 200, { agent: agentProfileSnapshot(await updateBuiltinAgentModelOverride(id, body.model)) })
164
+ return
165
+ }
166
+ if (current?.readonly) throw requestError('Read-only agents cannot be modified from the API', 403)
161
167
  sendJson(res, 200, { agent: await updateCustomAgentProfile(id, body || {}) })
162
168
  return
163
169
  }
@@ -165,7 +171,7 @@ export async function handleAgentProfilesApi(req, res, url) {
165
171
  if (req.method === 'DELETE') {
166
172
  const current = await getAgentProfile(id)
167
173
  if (current?.builtin) throw requestError('Built-in agents cannot be deleted', 403)
168
- if (current?.readonly) throw requestError('File-based agents cannot be deleted from the API', 403)
174
+ if (current?.readonly) throw requestError('Read-only agents cannot be deleted from the API', 403)
169
175
  await deleteCustomAgentProfile(id)
170
176
  sendJson(res, 200, { ok: true })
171
177
  return
@@ -1,4 +1,4 @@
1
- import { streamSimple } from '@earendil-works/pi-ai'
1
+ import { streamSimple } from '@earendil-works/pi-ai/compat'
2
2
  import { sendJson, readJsonBody } from '../utils/response.mjs'
3
3
  import { readStore } from '../storage.mjs'
4
4
  import { logger } from '../utils/logger.mjs'
@@ -1,6 +1,7 @@
1
1
  import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
2
2
  import { logger } from '../utils/logger.mjs'
3
3
  import { getActiveProject, setActiveProjectPath, readProjectConfig, getDefaultWorkspaceRoot } from '../project-config.mjs'
4
+ import { resolveRequestedProject, workspaceRootAfterProjectDeletion } from '../project-route-context.mjs'
4
5
  import { listProjectCommands, createCommandFile } from '../custom-commands.mjs'
5
6
  import { atomicProjectConfigUpdate } from '../storage.mjs'
6
7
  import { getWorkspaceRoot, setWorkspaceRoot } from '../utils/workspace.mjs'
@@ -137,13 +138,12 @@ export async function handleProjectApi(req, res, url) {
137
138
 
138
139
  if (req.method === 'POST' && url.pathname === '/api/project/open-path') {
139
140
  const body = await readJsonBody(req)
140
- const active = body?.projectId
141
- ? config.projects.find((project) => project.id === body.projectId) ?? getActiveProject(config)
142
- : getActiveProject(config)
141
+ const active = resolveRequestedProject(config, body?.projectId, getDefaultWorkspaceRoot())
143
142
  const target = String(body?.path || '')
143
+ const root = active?.path || getWorkspaceRoot() || getDefaultWorkspaceRoot()
144
144
  const resolved = target && path.isAbsolute(target)
145
145
  ? path.resolve(target)
146
- : path.resolve(active?.path || '', target)
146
+ : path.resolve(root, target)
147
147
  await openPathInFileManager(resolved)
148
148
  sendJson(res, 200, { ok: true })
149
149
  return
@@ -151,9 +151,7 @@ export async function handleProjectApi(req, res, url) {
151
151
 
152
152
  if (req.method === 'POST' && url.pathname === '/api/project/command') {
153
153
  const body = await readJsonBody(req)
154
- const active = body?.projectId
155
- ? config.projects.find((project) => project.id === body.projectId) ?? getActiveProject(config)
156
- : getActiveProject(config)
154
+ const active = resolveRequestedProject(config, body?.projectId, getDefaultWorkspaceRoot())
157
155
  if (!active?.path) {
158
156
  const error = new Error('No active project')
159
157
  error.statusCode = 400
@@ -178,9 +176,7 @@ export async function handleProjectApi(req, res, url) {
178
176
  return cfg
179
177
  })
180
178
  const active = getActiveProject(updated)
181
- if (active?.path) {
182
- setWorkspaceRoot(path.resolve(active.path))
183
- }
179
+ setWorkspaceRoot(workspaceRootAfterProjectDeletion(active, getDefaultWorkspaceRoot()))
184
180
  sendJson(res, 200, { project: active ?? null, projects: updated.projects, workspaceRoot: getWorkspaceRoot() })
185
181
  return
186
182
  }
@@ -1,4 +1,4 @@
1
- import { streamSimple } from '@earendil-works/pi-ai'
1
+ import { streamSimple } from '@earendil-works/pi-ai/compat'
2
2
  import { readJsonBody, sendJson, decodeSegment } from '../utils/response.mjs'
3
3
  import { readStore, atomicUpdate } from '../storage.mjs'
4
4
  import { createAgent, getSessionEventBus, agentEvents, persistSessionState } from '../agent-manager.mjs'