@kkelly-offical/kkcode 0.1.2

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 (196) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +445 -0
  3. package/package.json +46 -0
  4. package/src/agent/agent.mjs +170 -0
  5. package/src/agent/custom-agent-loader.mjs +158 -0
  6. package/src/agent/generator.mjs +115 -0
  7. package/src/agent/prompt/architect.txt +36 -0
  8. package/src/agent/prompt/build-fixer.txt +71 -0
  9. package/src/agent/prompt/build.txt +101 -0
  10. package/src/agent/prompt/compaction.txt +12 -0
  11. package/src/agent/prompt/explore.txt +29 -0
  12. package/src/agent/prompt/guide.txt +40 -0
  13. package/src/agent/prompt/longagent.txt +178 -0
  14. package/src/agent/prompt/plan.txt +50 -0
  15. package/src/agent/prompt/researcher.txt +23 -0
  16. package/src/agent/prompt/reviewer.txt +44 -0
  17. package/src/agent/prompt/security-reviewer.txt +62 -0
  18. package/src/agent/prompt/tdd-guide.txt +84 -0
  19. package/src/agent/prompt/title.txt +8 -0
  20. package/src/command/custom-commands.mjs +57 -0
  21. package/src/commands/agent.mjs +71 -0
  22. package/src/commands/audit.mjs +77 -0
  23. package/src/commands/background.mjs +86 -0
  24. package/src/commands/chat.mjs +114 -0
  25. package/src/commands/command.mjs +41 -0
  26. package/src/commands/config.mjs +44 -0
  27. package/src/commands/doctor.mjs +148 -0
  28. package/src/commands/hook.mjs +29 -0
  29. package/src/commands/init.mjs +141 -0
  30. package/src/commands/longagent.mjs +100 -0
  31. package/src/commands/mcp.mjs +89 -0
  32. package/src/commands/permission.mjs +36 -0
  33. package/src/commands/prompt.mjs +42 -0
  34. package/src/commands/review.mjs +266 -0
  35. package/src/commands/rule.mjs +34 -0
  36. package/src/commands/session.mjs +235 -0
  37. package/src/commands/theme.mjs +98 -0
  38. package/src/commands/usage.mjs +91 -0
  39. package/src/config/defaults.mjs +195 -0
  40. package/src/config/import-config.mjs +76 -0
  41. package/src/config/load-config.mjs +76 -0
  42. package/src/config/schema.mjs +509 -0
  43. package/src/context.mjs +40 -0
  44. package/src/core/constants.mjs +46 -0
  45. package/src/core/errors.mjs +57 -0
  46. package/src/core/events.mjs +29 -0
  47. package/src/core/types.mjs +57 -0
  48. package/src/github/api.mjs +78 -0
  49. package/src/github/auth.mjs +286 -0
  50. package/src/github/flow.mjs +298 -0
  51. package/src/github/workspace.mjs +212 -0
  52. package/src/index.mjs +82 -0
  53. package/src/knowledge/api-design.txt +9 -0
  54. package/src/knowledge/cpp.txt +10 -0
  55. package/src/knowledge/docker.txt +10 -0
  56. package/src/knowledge/dotnet.txt +9 -0
  57. package/src/knowledge/electron.txt +10 -0
  58. package/src/knowledge/flutter.txt +10 -0
  59. package/src/knowledge/go.txt +9 -0
  60. package/src/knowledge/graphql.txt +10 -0
  61. package/src/knowledge/java.txt +9 -0
  62. package/src/knowledge/kotlin.txt +10 -0
  63. package/src/knowledge/loader.mjs +125 -0
  64. package/src/knowledge/next.txt +8 -0
  65. package/src/knowledge/node.txt +8 -0
  66. package/src/knowledge/nuxt.txt +9 -0
  67. package/src/knowledge/php.txt +10 -0
  68. package/src/knowledge/python.txt +10 -0
  69. package/src/knowledge/react-native.txt +10 -0
  70. package/src/knowledge/react.txt +9 -0
  71. package/src/knowledge/ruby.txt +11 -0
  72. package/src/knowledge/rust.txt +9 -0
  73. package/src/knowledge/svelte.txt +9 -0
  74. package/src/knowledge/swift.txt +10 -0
  75. package/src/knowledge/tailwind.txt +10 -0
  76. package/src/knowledge/testing.txt +8 -0
  77. package/src/knowledge/typescript.txt +8 -0
  78. package/src/knowledge/vue.txt +9 -0
  79. package/src/mcp/client-http.mjs +157 -0
  80. package/src/mcp/client-sse.mjs +286 -0
  81. package/src/mcp/client-stdio.mjs +451 -0
  82. package/src/mcp/registry.mjs +394 -0
  83. package/src/mcp/stdio-framing.mjs +127 -0
  84. package/src/orchestration/background-manager.mjs +358 -0
  85. package/src/orchestration/background-worker.mjs +245 -0
  86. package/src/orchestration/longagent-manager.mjs +116 -0
  87. package/src/orchestration/stage-scheduler.mjs +489 -0
  88. package/src/orchestration/subagent-router.mjs +62 -0
  89. package/src/orchestration/task-scheduler.mjs +74 -0
  90. package/src/permission/engine.mjs +92 -0
  91. package/src/permission/exec-policy.mjs +372 -0
  92. package/src/permission/prompt.mjs +39 -0
  93. package/src/permission/rules.mjs +120 -0
  94. package/src/permission/workspace-trust.mjs +44 -0
  95. package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
  96. package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
  97. package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
  98. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
  99. package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
  100. package/src/plugin/hook-bus.mjs +154 -0
  101. package/src/provider/anthropic.mjs +389 -0
  102. package/src/provider/ollama.mjs +236 -0
  103. package/src/provider/openai-compatible.mjs +1 -0
  104. package/src/provider/openai.mjs +339 -0
  105. package/src/provider/retry-policy.mjs +68 -0
  106. package/src/provider/router.mjs +228 -0
  107. package/src/provider/sse.mjs +91 -0
  108. package/src/repl.mjs +2929 -0
  109. package/src/review/diff-parser.mjs +36 -0
  110. package/src/review/rejection-queue.mjs +62 -0
  111. package/src/review/review-store.mjs +21 -0
  112. package/src/review/risk-score.mjs +61 -0
  113. package/src/rules/load-rules.mjs +64 -0
  114. package/src/runtime.mjs +1 -0
  115. package/src/session/checkpoint.mjs +239 -0
  116. package/src/session/compaction.mjs +276 -0
  117. package/src/session/engine.mjs +225 -0
  118. package/src/session/instinct-manager.mjs +172 -0
  119. package/src/session/instruction-loader.mjs +25 -0
  120. package/src/session/longagent-plan.mjs +329 -0
  121. package/src/session/longagent-scaffold.mjs +100 -0
  122. package/src/session/longagent.mjs +1462 -0
  123. package/src/session/loop.mjs +905 -0
  124. package/src/session/memory-loader.mjs +75 -0
  125. package/src/session/project-context.mjs +367 -0
  126. package/src/session/prompt/anthropic.txt +151 -0
  127. package/src/session/prompt/beast.txt +37 -0
  128. package/src/session/prompt/max-steps.txt +6 -0
  129. package/src/session/prompt/plan.txt +9 -0
  130. package/src/session/prompt/qwen.txt +46 -0
  131. package/src/session/prompt-loader.mjs +18 -0
  132. package/src/session/recovery.mjs +52 -0
  133. package/src/session/store.mjs +503 -0
  134. package/src/session/system-prompt.mjs +260 -0
  135. package/src/session/task-validator.mjs +266 -0
  136. package/src/session/usability-gates.mjs +379 -0
  137. package/src/skill/builtin/backend-patterns.mjs +123 -0
  138. package/src/skill/builtin/commit.mjs +64 -0
  139. package/src/skill/builtin/debug.mjs +45 -0
  140. package/src/skill/builtin/frontend-patterns.mjs +120 -0
  141. package/src/skill/builtin/frontend.mjs +188 -0
  142. package/src/skill/builtin/init.mjs +220 -0
  143. package/src/skill/builtin/review.mjs +49 -0
  144. package/src/skill/builtin/security-checklist.mjs +80 -0
  145. package/src/skill/builtin/tdd.mjs +54 -0
  146. package/src/skill/generator.mjs +113 -0
  147. package/src/skill/registry.mjs +336 -0
  148. package/src/storage/audit-store.mjs +83 -0
  149. package/src/storage/event-log.mjs +82 -0
  150. package/src/storage/ghost-commit-store.mjs +235 -0
  151. package/src/storage/json-store.mjs +53 -0
  152. package/src/storage/paths.mjs +148 -0
  153. package/src/theme/color.mjs +64 -0
  154. package/src/theme/default-theme.mjs +29 -0
  155. package/src/theme/load-theme.mjs +71 -0
  156. package/src/theme/markdown.mjs +135 -0
  157. package/src/theme/schema.mjs +45 -0
  158. package/src/theme/status-bar.mjs +158 -0
  159. package/src/tool/audit-wrapper.mjs +38 -0
  160. package/src/tool/edit-transaction.mjs +126 -0
  161. package/src/tool/executor.mjs +109 -0
  162. package/src/tool/file-lock-manager.mjs +85 -0
  163. package/src/tool/git-auto.mjs +545 -0
  164. package/src/tool/git-full-auto.mjs +478 -0
  165. package/src/tool/image-util.mjs +276 -0
  166. package/src/tool/prompt/background_cancel.txt +1 -0
  167. package/src/tool/prompt/background_output.txt +1 -0
  168. package/src/tool/prompt/bash.txt +71 -0
  169. package/src/tool/prompt/codesearch.txt +18 -0
  170. package/src/tool/prompt/edit.txt +27 -0
  171. package/src/tool/prompt/enter_plan.txt +74 -0
  172. package/src/tool/prompt/exit_plan.txt +62 -0
  173. package/src/tool/prompt/glob.txt +33 -0
  174. package/src/tool/prompt/grep.txt +43 -0
  175. package/src/tool/prompt/list.txt +8 -0
  176. package/src/tool/prompt/multiedit.txt +20 -0
  177. package/src/tool/prompt/notebookedit.txt +21 -0
  178. package/src/tool/prompt/patch.txt +24 -0
  179. package/src/tool/prompt/question.txt +44 -0
  180. package/src/tool/prompt/read.txt +40 -0
  181. package/src/tool/prompt/task.txt +83 -0
  182. package/src/tool/prompt/todowrite.txt +117 -0
  183. package/src/tool/prompt/webfetch.txt +38 -0
  184. package/src/tool/prompt/websearch.txt +43 -0
  185. package/src/tool/prompt/write.txt +38 -0
  186. package/src/tool/prompt-loader.mjs +18 -0
  187. package/src/tool/question-prompt.mjs +86 -0
  188. package/src/tool/registry.mjs +1309 -0
  189. package/src/tool/task-tool.mjs +28 -0
  190. package/src/ui/activity-renderer.mjs +410 -0
  191. package/src/ui/repl-dashboard.mjs +357 -0
  192. package/src/usage/pricing.mjs +121 -0
  193. package/src/usage/usage-meter.mjs +113 -0
  194. package/src/util/git.mjs +496 -0
  195. package/src/util/template.mjs +10 -0
  196. package/src/util/yaml.mjs +100 -0
@@ -0,0 +1,489 @@
1
+ import { BackgroundManager } from "./background-manager.mjs"
2
+ import { EventBus } from "../core/events.mjs"
3
+ import { EVENT_TYPES } from "../core/constants.mjs"
4
+ import { getAgent } from "../agent/agent.mjs"
5
+
6
+ const AGENT_HINTS = [
7
+ { pattern: /\b(test|spec|jest|mocha|vitest|coverage)\b/i, agent: "tdd-guide" },
8
+ { pattern: /\b(review|audit|lint|quality)\b/i, agent: "reviewer" },
9
+ { pattern: /\b(secur|vuln|owasp|xss|inject|auth)\b/i, agent: "security-reviewer" },
10
+ { pattern: /\b(architect|design|blueprint|interface|api.*design)\b/i, agent: "architect" },
11
+ { pattern: /\b(build.*fix|compile.*error|type.*error|syntax.*error)\b/i, agent: "build-fixer" }
12
+ ]
13
+
14
+ function inferSubagentType(taskPrompt, taskId) {
15
+ const text = `${taskPrompt} ${taskId}`
16
+ for (const { pattern, agent } of AGENT_HINTS) {
17
+ if (pattern.test(text) && getAgent(agent)) return agent
18
+ }
19
+ return null
20
+ }
21
+
22
+ function sleep(ms) {
23
+ return new Promise((resolve) => setTimeout(resolve, ms))
24
+ }
25
+
26
+ function normalizeFiles(list) {
27
+ if (!Array.isArray(list)) return []
28
+ return [...new Set(list.map((item) => String(item || "").trim()).filter(Boolean))]
29
+ }
30
+
31
+ function mergeUnique(...lists) {
32
+ const merged = []
33
+ for (const list of lists) {
34
+ if (!Array.isArray(list)) continue
35
+ merged.push(...list)
36
+ }
37
+ return [...new Set(merged)]
38
+ }
39
+
40
+ function normalizeFileChanges(list) {
41
+ if (!Array.isArray(list)) return []
42
+ return list
43
+ .map((item) => ({
44
+ path: String(item?.path || "").trim(),
45
+ addedLines: Math.max(0, Number(item?.addedLines || 0)),
46
+ removedLines: Math.max(0, Number(item?.removedLines || 0)),
47
+ stageId: item?.stageId ? String(item.stageId) : "",
48
+ taskId: item?.taskId ? String(item.taskId) : ""
49
+ }))
50
+ .filter((item) => item.path)
51
+ }
52
+
53
+ function mergeFileChanges(...lists) {
54
+ const map = new Map()
55
+ for (const list of lists) {
56
+ for (const item of normalizeFileChanges(list)) {
57
+ const key = `${item.path}::${item.stageId}::${item.taskId}`
58
+ const prev = map.get(key) || { ...item, addedLines: 0, removedLines: 0 }
59
+ prev.addedLines += item.addedLines
60
+ prev.removedLines += item.removedLines
61
+ map.set(key, prev)
62
+ }
63
+ }
64
+ return [...map.values()]
65
+ }
66
+
67
+ function computeRemaining(planned = [], completed = []) {
68
+ const done = new Set(normalizeFiles(completed))
69
+ return normalizeFiles(planned).filter((file) => !done.has(file))
70
+ }
71
+
72
+ function stageConfig(config = {}) {
73
+ const parallel = config.agent?.longagent?.parallel || {}
74
+ return {
75
+ maxConcurrency: Math.max(1, Number(parallel.max_concurrency || 3)),
76
+ taskTimeoutMs: Math.max(1000, Number(parallel.task_timeout_ms || 600000)),
77
+ taskMaxRetries: Math.max(0, Number(parallel.task_max_retries ?? 2)),
78
+ budgetLimitUsd: Number(parallel.budget_limit_usd || 0),
79
+ passRule: "all_success"
80
+ }
81
+ }
82
+
83
+ function retryPrompt(taskPrompt, remainingFiles = [], attempt = 1, lastError = "") {
84
+ const parts = [
85
+ taskPrompt,
86
+ "",
87
+ `Retry attempt: ${attempt}`,
88
+ "Continue from previous progress. Focus ONLY on remaining files."
89
+ ]
90
+ if (remainingFiles.length) {
91
+ parts.push(`Remaining files: ${remainingFiles.join(", ")}`)
92
+ }
93
+ if (lastError) {
94
+ parts.push(`Previous failure: ${lastError}`)
95
+ }
96
+ return parts.join("\n")
97
+ }
98
+
99
+ function buildEnrichedPrompt({ stage, task, logicalTask, objective, stageIndex, stageCount, allTasks, priorContext }) {
100
+ const parts = []
101
+
102
+ parts.push("## Global Objective")
103
+ parts.push(objective || "(not specified)")
104
+ parts.push("")
105
+
106
+ if (priorContext) {
107
+ parts.push("## Prior Stage Results")
108
+ parts.push(priorContext)
109
+ parts.push("")
110
+ }
111
+
112
+ parts.push("## Current Stage")
113
+ parts.push(`Stage ${stageIndex + 1}/${stageCount}: ${stage.name || stage.stageId}`)
114
+ parts.push("")
115
+
116
+ parts.push("## Your Task")
117
+ parts.push(logicalTask.prompt)
118
+ parts.push("")
119
+
120
+ if (logicalTask.plannedFiles.length > 0) {
121
+ parts.push("## Files You Own (ONLY modify these)")
122
+ for (const file of logicalTask.plannedFiles) {
123
+ parts.push(`- ${file}`)
124
+ }
125
+ parts.push("")
126
+ }
127
+
128
+ const siblings = (allTasks || []).filter((t) => t.taskId !== task.taskId)
129
+ if (siblings.length > 0) {
130
+ parts.push("## Other Tasks in This Stage (DO NOT touch their files)")
131
+ for (const sibling of siblings) {
132
+ const files = normalizeFiles(sibling.plannedFiles)
133
+ parts.push(`- ${sibling.taskId}: ${files.length > 0 ? files.join(", ") : "(no files)"}`)
134
+ }
135
+ parts.push("")
136
+ }
137
+
138
+ if (logicalTask.acceptance.length > 0) {
139
+ parts.push("## Acceptance Criteria")
140
+ for (const criterion of logicalTask.acceptance) {
141
+ parts.push(`- ${criterion}`)
142
+ }
143
+ parts.push("")
144
+ }
145
+
146
+ return parts.join("\n")
147
+ }
148
+
149
+ function checkFileIsolation(tasks) {
150
+ const ownership = new Map()
151
+ const overlaps = []
152
+ for (const task of tasks) {
153
+ for (const file of normalizeFiles(task.plannedFiles)) {
154
+ if (ownership.has(file)) {
155
+ overlaps.push({ file, tasks: [ownership.get(file), task.taskId] })
156
+ } else {
157
+ ownership.set(file, task.taskId)
158
+ }
159
+ }
160
+ }
161
+ return overlaps
162
+ }
163
+
164
+ async function launchTask({
165
+ stage,
166
+ task,
167
+ logicalTask,
168
+ config,
169
+ sessionId,
170
+ model,
171
+ providerType,
172
+ objective,
173
+ stageIndex,
174
+ stageCount,
175
+ allTasks,
176
+ priorContext
177
+ }) {
178
+ const enrichedPrompt = buildEnrichedPrompt({
179
+ stage,
180
+ task,
181
+ logicalTask,
182
+ objective,
183
+ stageIndex: stageIndex || 0,
184
+ stageCount: stageCount || 1,
185
+ allTasks,
186
+ priorContext
187
+ })
188
+
189
+ const autoAgent = !task.subagentType ? inferSubagentType(logicalTask.prompt, task.taskId) : null
190
+
191
+ const payload = {
192
+ parentSessionId: sessionId,
193
+ subSessionId: logicalTask.subSessionId,
194
+ prompt: enrichedPrompt,
195
+ cwd: process.cwd(),
196
+ model,
197
+ providerType,
198
+ subagent: task.subagentType || autoAgent || null,
199
+ category: task.category || null,
200
+ subagentType: task.subagentType || autoAgent || null,
201
+ stageId: stage.stageId,
202
+ logicalTaskId: task.taskId,
203
+ plannedFiles: logicalTask.plannedFiles,
204
+ remainingFiles: logicalTask.remainingFiles,
205
+ attempt: logicalTask.attempt,
206
+ workerTimeoutMs: logicalTask.timeoutMs
207
+ }
208
+
209
+ const taskDescription = `${stage.stageId}:${task.taskId}#${logicalTask.attempt}`
210
+ const bg = await BackgroundManager.launchDelegateTask({
211
+ description: taskDescription,
212
+ payload,
213
+ config: {
214
+ ...config,
215
+ background: {
216
+ ...(config.background || {}),
217
+ max_parallel: Math.max(
218
+ Number(config.background?.max_parallel || 1),
219
+ Number(config.agent?.longagent?.parallel?.max_concurrency || 3)
220
+ )
221
+ }
222
+ }
223
+ })
224
+
225
+ await EventBus.emit({
226
+ type: EVENT_TYPES.LONGAGENT_STAGE_TASK_DISPATCHED,
227
+ sessionId,
228
+ payload: {
229
+ stageId: stage.stageId,
230
+ taskId: task.taskId,
231
+ backgroundTaskId: bg.id,
232
+ attempt: logicalTask.attempt
233
+ }
234
+ })
235
+
236
+ return bg.id
237
+ }
238
+
239
+ export async function runStageBarrier({
240
+ stage,
241
+ sessionId,
242
+ config,
243
+ model,
244
+ providerType,
245
+ seedTaskProgress = {},
246
+ objective = "",
247
+ stageIndex = 0,
248
+ stageCount = 1,
249
+ priorContext = ""
250
+ }) {
251
+ const cfg = stageConfig(config)
252
+ const logical = new Map()
253
+
254
+ // File isolation check: overlapping files = plan bug, fail-fast
255
+ const overlaps = checkFileIsolation(stage.tasks || [])
256
+ if (overlaps.length > 0) {
257
+ const details = overlaps.map((o) => `"${o.file}" claimed by [${o.tasks.join(", ")}]`).join("; ")
258
+ await EventBus.emit({
259
+ type: EVENT_TYPES.LONGAGENT_STAGE_STARTED,
260
+ sessionId,
261
+ payload: { error: `File isolation violation in stage ${stage.stageId}: ${details}`, stageId: stage.stageId }
262
+ })
263
+ throw new Error(`Stage ${stage.stageId}: file isolation violation — ${details}. Fix the plan to avoid overlapping file ownership.`)
264
+ }
265
+
266
+ for (const task of stage.tasks || []) {
267
+ const seeded = seedTaskProgress[task.taskId] || {}
268
+ const planned = normalizeFiles(task.plannedFiles)
269
+ const completed = normalizeFiles(seeded.completedFiles || [])
270
+ const remaining = normalizeFiles(seeded.remainingFiles || computeRemaining(planned, completed))
271
+ logical.set(task.taskId, {
272
+ stageId: stage.stageId,
273
+ taskId: task.taskId,
274
+ subSessionId: seeded.subSessionId || `sub_${sessionId}_${task.taskId}`,
275
+ plannedFiles: planned,
276
+ completedFiles: completed,
277
+ remainingFiles: remaining,
278
+ acceptance: Array.isArray(task.acceptance) ? task.acceptance : [],
279
+ prompt: seeded.prompt || task.prompt,
280
+ status: seeded.status || "pending",
281
+ attempt: Number(seeded.attempt || 0),
282
+ maxRetries: Number(task.maxRetries ?? cfg.taskMaxRetries),
283
+ timeoutMs: Number(task.timeoutMs || cfg.taskTimeoutMs),
284
+ backgroundTaskId: null,
285
+ lastError: seeded.lastError || "",
286
+ fileChanges: normalizeFileChanges(seeded.fileChanges || [])
287
+ })
288
+ }
289
+
290
+ await EventBus.emit({
291
+ type: EVENT_TYPES.LONGAGENT_STAGE_STARTED,
292
+ sessionId,
293
+ payload: {
294
+ stageId: stage.stageId,
295
+ taskCount: logical.size,
296
+ passRule: cfg.passRule
297
+ }
298
+ })
299
+
300
+ while (true) {
301
+ await BackgroundManager.tick({
302
+ ...config,
303
+ background: {
304
+ ...(config.background || {}),
305
+ max_parallel: Math.max(
306
+ Number(config.background?.max_parallel || 1),
307
+ cfg.maxConcurrency
308
+ )
309
+ }
310
+ })
311
+
312
+ let activeCount = [...logical.values()].filter((item) => item.status === "running").length
313
+ if (activeCount < cfg.maxConcurrency) {
314
+ const toLaunch = []
315
+ for (const task of stage.tasks || []) {
316
+ const item = logical.get(task.taskId)
317
+ if (!item || item.backgroundTaskId) continue
318
+ if (!["pending", "retrying"].includes(item.status)) continue
319
+ if (activeCount + toLaunch.length >= cfg.maxConcurrency) break
320
+ item.attempt += 1
321
+ item.status = "running"
322
+ if (item.attempt > 1) {
323
+ item.prompt = retryPrompt(task.prompt, item.remainingFiles, item.attempt, item.lastError)
324
+ }
325
+ toLaunch.push({ task, item })
326
+ }
327
+ if (toLaunch.length > 0) {
328
+ const bgIds = await Promise.all(toLaunch.map(({ task, item }) =>
329
+ launchTask({ stage, task, logicalTask: item, config, sessionId, model, providerType, objective, stageIndex, stageCount, allTasks: stage.tasks || [], priorContext })
330
+ ))
331
+ for (let i = 0; i < toLaunch.length; i++) {
332
+ toLaunch[i].item.backgroundTaskId = bgIds[i]
333
+ }
334
+ }
335
+ }
336
+
337
+ let pending = 0
338
+ for (const item of logical.values()) {
339
+ if (!item.backgroundTaskId) {
340
+ if (["pending", "retrying", "running"].includes(item.status)) pending += 1
341
+ continue
342
+ }
343
+ const bg = await BackgroundManager.get(item.backgroundTaskId)
344
+ if (!bg) {
345
+ item.status = "error"
346
+ item.lastError = "background worker disappeared"
347
+ item.backgroundTaskId = null
348
+ continue
349
+ }
350
+ if (!["completed", "error", "interrupted", "cancelled"].includes(bg.status)) {
351
+ pending += 1
352
+ continue
353
+ }
354
+
355
+ const result = bg.result || {}
356
+ const completedFromResult = mergeUnique(
357
+ item.completedFiles,
358
+ normalizeFiles(result.completed_files || result.completedFiles || [])
359
+ )
360
+ const remainingFromResult = normalizeFiles(
361
+ result.remaining_files || result.remainingFiles || computeRemaining(item.plannedFiles, completedFromResult)
362
+ )
363
+ item.completedFiles = completedFromResult
364
+ item.remainingFiles = remainingFromResult
365
+ item.fileChanges = mergeFileChanges(
366
+ item.fileChanges,
367
+ result.file_changes || result.fileChanges || []
368
+ )
369
+ item.backgroundTaskId = null
370
+
371
+ // Runtime file ownership check: warn if task touched files outside its plan
372
+ const plannedSet = new Set(item.plannedFiles)
373
+ const outOfScope = item.fileChanges
374
+ .map(fc => fc.path)
375
+ .filter(p => p && !plannedSet.has(p))
376
+ if (outOfScope.length > 0) {
377
+ await EventBus.emit({
378
+ type: EVENT_TYPES.LONGAGENT_ALERT,
379
+ sessionId,
380
+ payload: {
381
+ kind: "file_ownership_violation",
382
+ message: `Task ${item.taskId} modified ${outOfScope.length} file(s) outside its plan: ${outOfScope.slice(0, 5).join(", ")}`,
383
+ taskId: item.taskId,
384
+ stageId: stage.stageId,
385
+ outOfScopeFiles: outOfScope
386
+ }
387
+ })
388
+ }
389
+
390
+ if (bg.status === "completed" && remainingFromResult.length === 0) {
391
+ item.status = "completed"
392
+ item.lastError = ""
393
+ } else if (bg.status === "completed" && remainingFromResult.length > 0) {
394
+ item.status = item.attempt <= item.maxRetries ? "retrying" : "error"
395
+ item.lastError = "task completed but remaining files still pending"
396
+ } else {
397
+ item.lastError = bg.error || "task failed"
398
+ item.status = item.attempt <= item.maxRetries ? "retrying" : (bg.status === "cancelled" ? "cancelled" : "error")
399
+ }
400
+ item.lastReply = String(result.reply || "")
401
+ item.lastCost = Number(result.cost || 0)
402
+
403
+ await EventBus.emit({
404
+ type: EVENT_TYPES.LONGAGENT_STAGE_TASK_FINISHED,
405
+ sessionId,
406
+ payload: {
407
+ stageId: stage.stageId,
408
+ taskId: item.taskId,
409
+ status: item.status,
410
+ attempt: item.attempt,
411
+ remainingFiles: item.remainingFiles
412
+ }
413
+ })
414
+
415
+ if (["pending", "retrying", "running"].includes(item.status)) pending += 1
416
+ }
417
+
418
+ if (pending <= 0) break
419
+
420
+ // Budget circuit breaker: abort remaining tasks if cost exceeds limit
421
+ if (cfg.budgetLimitUsd > 0) {
422
+ const spent = [...logical.values()].reduce((s, i) => s + (Number.isFinite(i.lastCost) ? i.lastCost : 0), 0)
423
+ if (spent >= cfg.budgetLimitUsd) {
424
+ for (const item of logical.values()) {
425
+ if (["pending", "retrying"].includes(item.status)) {
426
+ item.status = "error"
427
+ item.lastError = `budget limit exceeded ($${spent.toFixed(2)} >= $${cfg.budgetLimitUsd})`
428
+ }
429
+ if (item.backgroundTaskId && item.status === "running") {
430
+ await BackgroundManager.cancel(item.backgroundTaskId).catch(() => {})
431
+ }
432
+ }
433
+ await EventBus.emit({
434
+ type: EVENT_TYPES.LONGAGENT_ALERT,
435
+ sessionId,
436
+ payload: { kind: "budget_breaker", spent, limit: cfg.budgetLimitUsd, stageId: stage.stageId }
437
+ })
438
+ break
439
+ }
440
+ }
441
+
442
+ await sleep(300)
443
+ }
444
+
445
+ const items = [...logical.values()]
446
+ const successCount = items.filter((item) => item.status === "completed").length
447
+ const failItems = items.filter((item) => item.status !== "completed")
448
+ const retryCount = items.reduce((sum, item) => sum + Math.max(0, item.attempt - 1), 0)
449
+ const remainingFiles = mergeUnique(...items.map((item) => item.remainingFiles))
450
+ const completionMarkerSeen = items.some((item) => String(item.lastReply || "").toLowerCase().includes("[task_complete]"))
451
+ const totalCost = items.reduce((sum, item) => sum + (Number.isFinite(item.lastCost) ? item.lastCost : 0), 0)
452
+ const fileChanges = mergeFileChanges(...items.map((item) => item.fileChanges))
453
+
454
+ const summary = {
455
+ stageId: stage.stageId,
456
+ successCount,
457
+ failCount: failItems.length,
458
+ retryCount,
459
+ remainingFiles,
460
+ completionMarkerSeen,
461
+ totalCost,
462
+ fileChanges,
463
+ allSuccess: failItems.length === 0,
464
+ taskProgress: Object.fromEntries(
465
+ items.map((item) => [
466
+ item.taskId,
467
+ {
468
+ taskId: item.taskId,
469
+ attempt: item.attempt,
470
+ status: item.status,
471
+ plannedFiles: item.plannedFiles,
472
+ completedFiles: item.completedFiles,
473
+ remainingFiles: item.remainingFiles,
474
+ fileChanges: item.fileChanges,
475
+ lastError: item.lastError || "",
476
+ lastReply: item.lastReply || ""
477
+ }
478
+ ])
479
+ )
480
+ }
481
+
482
+ await EventBus.emit({
483
+ type: EVENT_TYPES.LONGAGENT_STAGE_FINISHED,
484
+ sessionId,
485
+ payload: summary
486
+ })
487
+
488
+ return summary
489
+ }
@@ -0,0 +1,62 @@
1
+ import { getAgent } from "../agent/agent.mjs"
2
+
3
+ export function resolveSubagent({ config, subagentType = null, category = null }) {
4
+ if (subagentType && category) {
5
+ throw new Error("category and subagent_type are mutually exclusive")
6
+ }
7
+
8
+ if (subagentType) {
9
+ const agent = config.agent?.subagents?.[subagentType]
10
+ if (agent) {
11
+ return {
12
+ name: subagentType,
13
+ ...agent
14
+ }
15
+ }
16
+ // Fallback: check the agent registry for custom agents
17
+ const registeredAgent = getAgent(subagentType)
18
+ if (registeredAgent) {
19
+ return {
20
+ name: subagentType,
21
+ mode: registeredAgent.mode || "agent",
22
+ permission: registeredAgent.permission,
23
+ tools: registeredAgent.tools,
24
+ model: registeredAgent.model,
25
+ temperature: registeredAgent.temperature
26
+ }
27
+ }
28
+ // If the requested type isn't configured, fall through to default resolution
29
+ // instead of throwing — this handles "default-subagent" and other synthetic names
30
+ if (Object.keys(config.agent?.subagents || {}).length === 0) {
31
+ return {
32
+ name: subagentType,
33
+ mode: "agent"
34
+ }
35
+ }
36
+ throw new Error(`unknown subagent_type: ${subagentType}`)
37
+ }
38
+
39
+ if (category) {
40
+ const route = config.agent?.routing?.categories?.[category]
41
+ if (!route) throw new Error(`no subagent routing for category: ${category}`)
42
+ const agent = config.agent?.subagents?.[route]
43
+ if (!agent) throw new Error(`routed subagent not found: ${route}`)
44
+ return {
45
+ name: route,
46
+ ...agent
47
+ }
48
+ }
49
+
50
+ const first = Object.entries(config.agent?.subagents || {})[0]
51
+ if (!first) {
52
+ return {
53
+ name: "default-subagent",
54
+ mode: "agent"
55
+ }
56
+ }
57
+
58
+ return {
59
+ name: first[0],
60
+ ...first[1]
61
+ }
62
+ }
@@ -0,0 +1,74 @@
1
+ import { BackgroundManager } from "./background-manager.mjs"
2
+ import { resolveSubagent } from "./subagent-router.mjs"
3
+
4
+ export function createTaskDelegate({ config, parentSessionId, model, providerType, runSubtask }) {
5
+ return async function delegateTask(args = {}) {
6
+ const subagent = resolveSubagent({
7
+ config,
8
+ subagentType: args.subagent_type || null,
9
+ category: args.category || null
10
+ })
11
+
12
+ const subSessionId = String(args.session_id || `sub_${parentSessionId}_${Date.now()}`)
13
+ const prompt = String(args.prompt || "").trim() || (args.session_id ? "Continue from existing sub-session context." : "")
14
+
15
+ if (!prompt) {
16
+ return { error: "task.prompt is required when session_id is not provided" }
17
+ }
18
+
19
+ const subModel = subagent.model || model
20
+ const subProvider = subagent.providerType || providerType
21
+
22
+ const run = async ({ isCancelled, log }) => {
23
+ await log(`task started (${subagent.name})`)
24
+ const out = await runSubtask({
25
+ prompt,
26
+ sessionId: subSessionId,
27
+ model: subModel,
28
+ providerType: subProvider,
29
+ subagent,
30
+ allowQuestion: args.allow_question === true
31
+ })
32
+ await log(out.reply)
33
+ if (isCancelled()) return { cancelled: true }
34
+ return {
35
+ session_id: subSessionId,
36
+ subagent: subagent.name,
37
+ reply: out.reply,
38
+ tool_events: out.toolEvents?.length || 0
39
+ }
40
+ }
41
+
42
+ if (args.run_in_background) {
43
+ const task = await BackgroundManager.launchDelegateTask({
44
+ description: String(args.description || `background task (${subagent.name})`),
45
+ payload: {
46
+ parentSessionId,
47
+ subSessionId,
48
+ prompt,
49
+ cwd: process.cwd(),
50
+ model: subModel,
51
+ providerType: subProvider,
52
+ subagent: subagent.name,
53
+ category: args.category || null,
54
+ subagentType: subagent.name,
55
+ stageId: args.stage_id || null,
56
+ logicalTaskId: args.task_id || null,
57
+ plannedFiles: Array.isArray(args.planned_files) ? args.planned_files : [],
58
+ allowQuestion: args.allow_question === true
59
+ },
60
+ config
61
+ })
62
+ return {
63
+ background_task_id: task.id,
64
+ status: task.status,
65
+ session_id: subSessionId
66
+ }
67
+ }
68
+
69
+ return run({
70
+ isCancelled: () => false,
71
+ log: async () => {}
72
+ })
73
+ }
74
+ }