@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.
- package/LICENSE +674 -0
- package/README.md +445 -0
- package/package.json +46 -0
- package/src/agent/agent.mjs +170 -0
- package/src/agent/custom-agent-loader.mjs +158 -0
- package/src/agent/generator.mjs +115 -0
- package/src/agent/prompt/architect.txt +36 -0
- package/src/agent/prompt/build-fixer.txt +71 -0
- package/src/agent/prompt/build.txt +101 -0
- package/src/agent/prompt/compaction.txt +12 -0
- package/src/agent/prompt/explore.txt +29 -0
- package/src/agent/prompt/guide.txt +40 -0
- package/src/agent/prompt/longagent.txt +178 -0
- package/src/agent/prompt/plan.txt +50 -0
- package/src/agent/prompt/researcher.txt +23 -0
- package/src/agent/prompt/reviewer.txt +44 -0
- package/src/agent/prompt/security-reviewer.txt +62 -0
- package/src/agent/prompt/tdd-guide.txt +84 -0
- package/src/agent/prompt/title.txt +8 -0
- package/src/command/custom-commands.mjs +57 -0
- package/src/commands/agent.mjs +71 -0
- package/src/commands/audit.mjs +77 -0
- package/src/commands/background.mjs +86 -0
- package/src/commands/chat.mjs +114 -0
- package/src/commands/command.mjs +41 -0
- package/src/commands/config.mjs +44 -0
- package/src/commands/doctor.mjs +148 -0
- package/src/commands/hook.mjs +29 -0
- package/src/commands/init.mjs +141 -0
- package/src/commands/longagent.mjs +100 -0
- package/src/commands/mcp.mjs +89 -0
- package/src/commands/permission.mjs +36 -0
- package/src/commands/prompt.mjs +42 -0
- package/src/commands/review.mjs +266 -0
- package/src/commands/rule.mjs +34 -0
- package/src/commands/session.mjs +235 -0
- package/src/commands/theme.mjs +98 -0
- package/src/commands/usage.mjs +91 -0
- package/src/config/defaults.mjs +195 -0
- package/src/config/import-config.mjs +76 -0
- package/src/config/load-config.mjs +76 -0
- package/src/config/schema.mjs +509 -0
- package/src/context.mjs +40 -0
- package/src/core/constants.mjs +46 -0
- package/src/core/errors.mjs +57 -0
- package/src/core/events.mjs +29 -0
- package/src/core/types.mjs +57 -0
- package/src/github/api.mjs +78 -0
- package/src/github/auth.mjs +286 -0
- package/src/github/flow.mjs +298 -0
- package/src/github/workspace.mjs +212 -0
- package/src/index.mjs +82 -0
- package/src/knowledge/api-design.txt +9 -0
- package/src/knowledge/cpp.txt +10 -0
- package/src/knowledge/docker.txt +10 -0
- package/src/knowledge/dotnet.txt +9 -0
- package/src/knowledge/electron.txt +10 -0
- package/src/knowledge/flutter.txt +10 -0
- package/src/knowledge/go.txt +9 -0
- package/src/knowledge/graphql.txt +10 -0
- package/src/knowledge/java.txt +9 -0
- package/src/knowledge/kotlin.txt +10 -0
- package/src/knowledge/loader.mjs +125 -0
- package/src/knowledge/next.txt +8 -0
- package/src/knowledge/node.txt +8 -0
- package/src/knowledge/nuxt.txt +9 -0
- package/src/knowledge/php.txt +10 -0
- package/src/knowledge/python.txt +10 -0
- package/src/knowledge/react-native.txt +10 -0
- package/src/knowledge/react.txt +9 -0
- package/src/knowledge/ruby.txt +11 -0
- package/src/knowledge/rust.txt +9 -0
- package/src/knowledge/svelte.txt +9 -0
- package/src/knowledge/swift.txt +10 -0
- package/src/knowledge/tailwind.txt +10 -0
- package/src/knowledge/testing.txt +8 -0
- package/src/knowledge/typescript.txt +8 -0
- package/src/knowledge/vue.txt +9 -0
- package/src/mcp/client-http.mjs +157 -0
- package/src/mcp/client-sse.mjs +286 -0
- package/src/mcp/client-stdio.mjs +451 -0
- package/src/mcp/registry.mjs +394 -0
- package/src/mcp/stdio-framing.mjs +127 -0
- package/src/orchestration/background-manager.mjs +358 -0
- package/src/orchestration/background-worker.mjs +245 -0
- package/src/orchestration/longagent-manager.mjs +116 -0
- package/src/orchestration/stage-scheduler.mjs +489 -0
- package/src/orchestration/subagent-router.mjs +62 -0
- package/src/orchestration/task-scheduler.mjs +74 -0
- package/src/permission/engine.mjs +92 -0
- package/src/permission/exec-policy.mjs +372 -0
- package/src/permission/prompt.mjs +39 -0
- package/src/permission/rules.mjs +120 -0
- package/src/permission/workspace-trust.mjs +44 -0
- package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
- package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
- package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
- package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
- package/src/plugin/hook-bus.mjs +154 -0
- package/src/provider/anthropic.mjs +389 -0
- package/src/provider/ollama.mjs +236 -0
- package/src/provider/openai-compatible.mjs +1 -0
- package/src/provider/openai.mjs +339 -0
- package/src/provider/retry-policy.mjs +68 -0
- package/src/provider/router.mjs +228 -0
- package/src/provider/sse.mjs +91 -0
- package/src/repl.mjs +2929 -0
- package/src/review/diff-parser.mjs +36 -0
- package/src/review/rejection-queue.mjs +62 -0
- package/src/review/review-store.mjs +21 -0
- package/src/review/risk-score.mjs +61 -0
- package/src/rules/load-rules.mjs +64 -0
- package/src/runtime.mjs +1 -0
- package/src/session/checkpoint.mjs +239 -0
- package/src/session/compaction.mjs +276 -0
- package/src/session/engine.mjs +225 -0
- package/src/session/instinct-manager.mjs +172 -0
- package/src/session/instruction-loader.mjs +25 -0
- package/src/session/longagent-plan.mjs +329 -0
- package/src/session/longagent-scaffold.mjs +100 -0
- package/src/session/longagent.mjs +1462 -0
- package/src/session/loop.mjs +905 -0
- package/src/session/memory-loader.mjs +75 -0
- package/src/session/project-context.mjs +367 -0
- package/src/session/prompt/anthropic.txt +151 -0
- package/src/session/prompt/beast.txt +37 -0
- package/src/session/prompt/max-steps.txt +6 -0
- package/src/session/prompt/plan.txt +9 -0
- package/src/session/prompt/qwen.txt +46 -0
- package/src/session/prompt-loader.mjs +18 -0
- package/src/session/recovery.mjs +52 -0
- package/src/session/store.mjs +503 -0
- package/src/session/system-prompt.mjs +260 -0
- package/src/session/task-validator.mjs +266 -0
- package/src/session/usability-gates.mjs +379 -0
- package/src/skill/builtin/backend-patterns.mjs +123 -0
- package/src/skill/builtin/commit.mjs +64 -0
- package/src/skill/builtin/debug.mjs +45 -0
- package/src/skill/builtin/frontend-patterns.mjs +120 -0
- package/src/skill/builtin/frontend.mjs +188 -0
- package/src/skill/builtin/init.mjs +220 -0
- package/src/skill/builtin/review.mjs +49 -0
- package/src/skill/builtin/security-checklist.mjs +80 -0
- package/src/skill/builtin/tdd.mjs +54 -0
- package/src/skill/generator.mjs +113 -0
- package/src/skill/registry.mjs +336 -0
- package/src/storage/audit-store.mjs +83 -0
- package/src/storage/event-log.mjs +82 -0
- package/src/storage/ghost-commit-store.mjs +235 -0
- package/src/storage/json-store.mjs +53 -0
- package/src/storage/paths.mjs +148 -0
- package/src/theme/color.mjs +64 -0
- package/src/theme/default-theme.mjs +29 -0
- package/src/theme/load-theme.mjs +71 -0
- package/src/theme/markdown.mjs +135 -0
- package/src/theme/schema.mjs +45 -0
- package/src/theme/status-bar.mjs +158 -0
- package/src/tool/audit-wrapper.mjs +38 -0
- package/src/tool/edit-transaction.mjs +126 -0
- package/src/tool/executor.mjs +109 -0
- package/src/tool/file-lock-manager.mjs +85 -0
- package/src/tool/git-auto.mjs +545 -0
- package/src/tool/git-full-auto.mjs +478 -0
- package/src/tool/image-util.mjs +276 -0
- package/src/tool/prompt/background_cancel.txt +1 -0
- package/src/tool/prompt/background_output.txt +1 -0
- package/src/tool/prompt/bash.txt +71 -0
- package/src/tool/prompt/codesearch.txt +18 -0
- package/src/tool/prompt/edit.txt +27 -0
- package/src/tool/prompt/enter_plan.txt +74 -0
- package/src/tool/prompt/exit_plan.txt +62 -0
- package/src/tool/prompt/glob.txt +33 -0
- package/src/tool/prompt/grep.txt +43 -0
- package/src/tool/prompt/list.txt +8 -0
- package/src/tool/prompt/multiedit.txt +20 -0
- package/src/tool/prompt/notebookedit.txt +21 -0
- package/src/tool/prompt/patch.txt +24 -0
- package/src/tool/prompt/question.txt +44 -0
- package/src/tool/prompt/read.txt +40 -0
- package/src/tool/prompt/task.txt +83 -0
- package/src/tool/prompt/todowrite.txt +117 -0
- package/src/tool/prompt/webfetch.txt +38 -0
- package/src/tool/prompt/websearch.txt +43 -0
- package/src/tool/prompt/write.txt +38 -0
- package/src/tool/prompt-loader.mjs +18 -0
- package/src/tool/question-prompt.mjs +86 -0
- package/src/tool/registry.mjs +1309 -0
- package/src/tool/task-tool.mjs +28 -0
- package/src/ui/activity-renderer.mjs +410 -0
- package/src/ui/repl-dashboard.mjs +357 -0
- package/src/usage/pricing.mjs +121 -0
- package/src/usage/usage-meter.mjs +113 -0
- package/src/util/git.mjs +496 -0
- package/src/util/template.mjs +10 -0
- 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
|
+
}
|