@kkelly-offical/kkcode 0.1.2 → 0.1.6

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 (58) hide show
  1. package/README.md +120 -178
  2. package/package.json +46 -46
  3. package/src/agent/agent.mjs +41 -0
  4. package/src/agent/prompt/frontend-designer.txt +58 -0
  5. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -0
  6. package/src/agent/prompt/longagent-coding-agent.txt +37 -0
  7. package/src/agent/prompt/longagent-debugging-agent.txt +46 -0
  8. package/src/agent/prompt/longagent-preview-agent.txt +63 -0
  9. package/src/config/defaults.mjs +260 -195
  10. package/src/config/schema.mjs +71 -6
  11. package/src/core/constants.mjs +91 -46
  12. package/src/index.mjs +1 -1
  13. package/src/knowledge/frontend-aesthetics.txt +39 -0
  14. package/src/knowledge/loader.mjs +2 -1
  15. package/src/knowledge/tailwind.txt +12 -3
  16. package/src/mcp/client-http.mjs +141 -157
  17. package/src/mcp/client-sse.mjs +288 -286
  18. package/src/mcp/client-stdio.mjs +533 -451
  19. package/src/mcp/constants.mjs +2 -0
  20. package/src/mcp/registry.mjs +479 -394
  21. package/src/mcp/stdio-framing.mjs +133 -127
  22. package/src/mcp/tool-result.mjs +24 -0
  23. package/src/observability/index.mjs +42 -0
  24. package/src/observability/metrics.mjs +137 -0
  25. package/src/observability/tracer.mjs +137 -0
  26. package/src/orchestration/background-manager.mjs +372 -358
  27. package/src/orchestration/background-worker.mjs +305 -245
  28. package/src/orchestration/longagent-manager.mjs +171 -116
  29. package/src/orchestration/stage-scheduler.mjs +728 -489
  30. package/src/permission/exec-policy.mjs +9 -11
  31. package/src/provider/anthropic.mjs +1 -0
  32. package/src/provider/openai.mjs +340 -339
  33. package/src/provider/retry-policy.mjs +68 -68
  34. package/src/provider/router.mjs +241 -228
  35. package/src/provider/sse.mjs +104 -91
  36. package/src/repl.mjs +1 -1
  37. package/src/session/checkpoint.mjs +66 -3
  38. package/src/session/engine.mjs +227 -225
  39. package/src/session/longagent-4stage.mjs +460 -0
  40. package/src/session/longagent-hybrid.mjs +1081 -0
  41. package/src/session/longagent-plan.mjs +365 -329
  42. package/src/session/longagent-project-memory.mjs +53 -0
  43. package/src/session/longagent-scaffold.mjs +291 -100
  44. package/src/session/longagent-task-bus.mjs +54 -0
  45. package/src/session/longagent-utils.mjs +472 -0
  46. package/src/session/longagent.mjs +884 -1462
  47. package/src/session/project-context.mjs +30 -0
  48. package/src/session/store.mjs +510 -503
  49. package/src/session/task-validator.mjs +4 -3
  50. package/src/skill/builtin/design.mjs +76 -0
  51. package/src/skill/builtin/frontend.mjs +8 -0
  52. package/src/skill/registry.mjs +390 -336
  53. package/src/storage/ghost-commit-store.mjs +18 -8
  54. package/src/tool/executor.mjs +11 -0
  55. package/src/tool/git-auto.mjs +0 -19
  56. package/src/tool/registry.mjs +71 -37
  57. package/src/ui/activity-renderer.mjs +664 -410
  58. package/src/util/git.mjs +23 -0
@@ -1,245 +1,305 @@
1
- import { appendFile } from "node:fs/promises"
2
- import { readJson, writeJson } from "../storage/json-store.mjs"
3
- import { ensureBackgroundTaskRuntimeDir, backgroundTaskCheckpointPath, backgroundTaskLogPath } from "../storage/paths.mjs"
4
- import { buildContext } from "../context.mjs"
5
- import { ToolRegistry } from "../tool/registry.mjs"
6
- import { executeTurn } from "../session/engine.mjs"
7
-
8
- function now() {
9
- return Date.now()
10
- }
11
-
12
- function argValue(flag) {
13
- const idx = process.argv.indexOf(flag)
14
- if (idx < 0) return null
15
- return process.argv[idx + 1] || null
16
- }
17
-
18
- function makeAbortError(reason = "aborted") {
19
- const err = new Error(reason)
20
- err.code = "ABORT_ERR"
21
- return err
22
- }
23
-
24
- function isAbortError(error) {
25
- return error?.code === "ABORT_ERR" || error?.name === "AbortError"
26
- }
27
-
28
- async function readTask(taskId) {
29
- return readJson(backgroundTaskCheckpointPath(taskId), null)
30
- }
31
-
32
- async function patchTask(taskId, updater) {
33
- const current = await readTask(taskId)
34
- if (!current) return null
35
- const next = {
36
- ...current,
37
- ...updater(current),
38
- updatedAt: now()
39
- }
40
- await writeJson(backgroundTaskCheckpointPath(taskId), next)
41
- return next
42
- }
43
-
44
- async function appendTaskLog(taskId, line) {
45
- await appendFile(backgroundTaskLogPath(taskId), `${line}\n`, "utf8")
46
- await patchTask(taskId, (current) => ({
47
- logs: [...(current.logs || []), String(line)].slice(-300),
48
- lastHeartbeatAt: now()
49
- }))
50
- }
51
-
52
- async function runDelegateTask(task, signal) {
53
- const payload = task.payload || {}
54
- const cwd = payload.cwd || process.cwd()
55
- process.chdir(cwd)
56
-
57
- const ctx = await buildContext({ cwd })
58
- await ToolRegistry.initialize({
59
- config: ctx.configState.config,
60
- cwd
61
- })
62
- const { CustomAgentRegistry } = await import("../agent/custom-agent-loader.mjs")
63
- await CustomAgentRegistry.initialize(cwd)
64
-
65
- const providerType = payload.providerType || ctx.configState.config.provider.default
66
- const providerDefault = ctx.configState.config.provider[providerType]
67
- const model = payload.model || providerDefault?.default_model
68
-
69
- const out = await executeTurn({
70
- prompt: String(payload.prompt || ""),
71
- mode: "agent",
72
- model,
73
- providerType,
74
- sessionId: payload.subSessionId,
75
- configState: ctx.configState,
76
- signal,
77
- allowQuestion: payload.allowQuestion !== true ? false : true,
78
- toolContext: {
79
- taskId: task.id,
80
- stageId: payload.stageId || null,
81
- logicalTaskId: payload.logicalTaskId || null
82
- }
83
- })
84
-
85
- const plannedFiles = Array.isArray(payload.plannedFiles)
86
- ? payload.plannedFiles.map((item) => String(item || "").trim()).filter(Boolean)
87
- : []
88
- const completedFilesFromTools = out.toolEvents
89
- .filter((event) => ["write", "edit"].includes(event.name) && event.status === "completed")
90
- .map((event) => {
91
- const p = event.args?.path
92
- return p ? String(p).trim() : ""
93
- })
94
- .filter(Boolean)
95
-
96
- const fileChanges = out.toolEvents
97
- .flatMap((event) => Array.isArray(event?.metadata?.fileChanges) ? event.metadata.fileChanges : [])
98
- .map((item) => ({
99
- path: String(item?.path || "").trim(),
100
- addedLines: Math.max(0, Number(item?.addedLines || 0)),
101
- removedLines: Math.max(0, Number(item?.removedLines || 0)),
102
- stageId: item?.stageId ? String(item.stageId) : (payload.stageId || ""),
103
- taskId: item?.taskId ? String(item.taskId) : (payload.logicalTaskId || "")
104
- }))
105
- .filter((item) => item.path)
106
-
107
- const completedFileSet = new Set(
108
- completedFilesFromTools.filter((file) => plannedFiles.length === 0 || plannedFiles.includes(file))
109
- )
110
- const completedFiles = [...completedFileSet]
111
- const remainingFiles = plannedFiles.filter((file) => !completedFileSet.has(file))
112
-
113
- return {
114
- session_id: payload.subSessionId,
115
- parent_session_id: payload.parentSessionId || null,
116
- subagent: payload.subagent || null,
117
- reply: out.reply,
118
- tool_events: out.toolEvents?.length || 0,
119
- completed_files: completedFiles,
120
- remaining_files: remainingFiles,
121
- file_changes: fileChanges,
122
- cost: out.cost,
123
- budget_warnings: out.budgetWarnings || []
124
- }
125
- }
126
-
127
- async function main() {
128
- const taskId = argValue("--task-id") || process.env.KKCODE_BACKGROUND_TASK_ID || null
129
- if (!taskId) {
130
- process.exit(1)
131
- return
132
- }
133
-
134
- await ensureBackgroundTaskRuntimeDir()
135
- const task = await readTask(taskId)
136
- if (!task) {
137
- process.exit(1)
138
- return
139
- }
140
-
141
- if (task.cancelled) {
142
- await patchTask(taskId, () => ({
143
- status: "cancelled",
144
- endedAt: now()
145
- }))
146
- process.exit(0)
147
- return
148
- }
149
-
150
- await patchTask(taskId, () => ({
151
- status: "running",
152
- workerPid: process.pid,
153
- startedAt: now(),
154
- lastHeartbeatAt: now()
155
- }))
156
-
157
- const abortController = new AbortController()
158
- const parentPid = process.ppid
159
- const heartbeatTimer = setInterval(() => {
160
- patchTask(taskId, () => ({ lastHeartbeatAt: now() })).catch(() => {})
161
- }, 2000)
162
-
163
- const cancelPoll = setInterval(() => {
164
- // Orphan detection: if parent process died, self-terminate
165
- try { process.kill(parentPid, 0) } catch {
166
- if (!abortController.signal.aborted) {
167
- abortController.abort(makeAbortError("parent process exited, worker orphaned"))
168
- }
169
- return
170
- }
171
- readTask(taskId).then((latest) => {
172
- if (latest?.cancelled && !abortController.signal.aborted) {
173
- abortController.abort(makeAbortError("cancelled by user"))
174
- }
175
- }).catch(() => {})
176
- }, 1500)
177
-
178
- const timeoutMs = Math.max(1000, Number(task.payload?.workerTimeoutMs || 900000))
179
- const timeoutTimer = setTimeout(() => {
180
- if (!abortController.signal.aborted) {
181
- abortController.abort(makeAbortError(`worker timeout after ${timeoutMs}ms`))
182
- }
183
- }, timeoutMs)
184
-
185
- try {
186
- await appendTaskLog(taskId, `task started (worker pid=${process.pid})`)
187
-
188
- const latest = await readTask(taskId)
189
- if (!latest?.payload?.workerType || latest.payload.workerType !== "delegate_task") {
190
- throw new Error(`unsupported workerType: ${latest?.payload?.workerType || "unknown"}`)
191
- }
192
-
193
- const result = await runDelegateTask(latest, abortController.signal)
194
- await appendTaskLog(taskId, "task completed")
195
- await patchTask(taskId, () => ({
196
- status: "completed",
197
- result,
198
- error: null,
199
- endedAt: now(),
200
- lastHeartbeatAt: now()
201
- }))
202
- process.exit(0)
203
- } catch (error) {
204
- const latest = await readTask(taskId)
205
- const cancelled = latest?.cancelled
206
- const aborted = isAbortError(error)
207
- if (cancelled) {
208
- await appendTaskLog(taskId, "task cancelled")
209
- await patchTask(taskId, () => ({
210
- status: "cancelled",
211
- endedAt: now(),
212
- error: null
213
- }))
214
- process.exit(0)
215
- return
216
- }
217
-
218
- if (aborted) {
219
- await appendTaskLog(taskId, `task interrupted: ${error.message}`)
220
- await patchTask(taskId, () => ({
221
- status: "interrupted",
222
- error: error.message,
223
- endedAt: now()
224
- }))
225
- process.exit(2)
226
- return
227
- }
228
-
229
- await appendTaskLog(taskId, `task error: ${error.message}`)
230
- await patchTask(taskId, () => ({
231
- status: "error",
232
- error: error.message,
233
- endedAt: now()
234
- }))
235
- process.exit(1)
236
- } finally {
237
- clearInterval(heartbeatTimer)
238
- clearInterval(cancelPoll)
239
- clearTimeout(timeoutTimer)
240
- }
241
- }
242
-
243
- main().catch(() => {
244
- process.exit(1)
245
- })
1
+ import { appendFile } from "node:fs/promises"
2
+ import { readJson, writeJson } from "../storage/json-store.mjs"
3
+ import { ensureBackgroundTaskRuntimeDir, backgroundTaskCheckpointPath, backgroundTaskLogPath } from "../storage/paths.mjs"
4
+ import { buildContext } from "../context.mjs"
5
+ import { ToolRegistry } from "../tool/registry.mjs"
6
+ import { executeTurn } from "../session/engine.mjs"
7
+
8
+ function now() {
9
+ return Date.now()
10
+ }
11
+
12
+ function argValue(flag) {
13
+ const idx = process.argv.indexOf(flag)
14
+ if (idx < 0) return null
15
+ return process.argv[idx + 1] || null
16
+ }
17
+
18
+ function makeAbortError(reason = "aborted") {
19
+ const err = new Error(reason)
20
+ err.code = "ABORT_ERR"
21
+ return err
22
+ }
23
+
24
+ function isAbortError(error) {
25
+ return error?.code === "ABORT_ERR" || error?.name === "AbortError"
26
+ }
27
+
28
+ async function readTask(taskId) {
29
+ return readJson(backgroundTaskCheckpointPath(taskId), null)
30
+ }
31
+
32
+ async function patchTask(taskId, updater) {
33
+ const current = await readTask(taskId)
34
+ if (!current) return null
35
+ const next = {
36
+ ...current,
37
+ ...updater(current),
38
+ updatedAt: now()
39
+ }
40
+ await writeJson(backgroundTaskCheckpointPath(taskId), next)
41
+ return next
42
+ }
43
+
44
+ let _maxLogLines = 300
45
+
46
+ async function appendTaskLog(taskId, line) {
47
+ await appendFile(backgroundTaskLogPath(taskId), `${line}\n`, "utf8")
48
+ await patchTask(taskId, (current) => ({
49
+ logs: [...(current.logs || []), String(line)].slice(-_maxLogLines),
50
+ lastHeartbeatAt: now()
51
+ }))
52
+ }
53
+
54
+ async function runDelegateTask(task, signal) {
55
+ const payload = task.payload || {}
56
+ const cwd = payload.cwd || process.cwd()
57
+ process.chdir(cwd)
58
+
59
+ const ctx = await buildContext({ cwd })
60
+ _maxLogLines = Number(ctx.configState.config?.background?.max_log_lines || 300)
61
+ await ToolRegistry.initialize({
62
+ config: ctx.configState.config,
63
+ cwd
64
+ })
65
+ const { CustomAgentRegistry } = await import("../agent/custom-agent-loader.mjs")
66
+ await CustomAgentRegistry.initialize(cwd)
67
+
68
+ const providerType = payload.providerType || ctx.configState.config.provider.default
69
+ const providerDefault = ctx.configState.config.provider[providerType]
70
+ const model = payload.model || providerDefault?.default_model
71
+
72
+ const out = await executeTurn({
73
+ prompt: String(payload.prompt || ""),
74
+ mode: "agent",
75
+ model,
76
+ providerType,
77
+ sessionId: payload.subSessionId,
78
+ configState: ctx.configState,
79
+ signal,
80
+ allowQuestion: payload.allowQuestion !== true ? false : true,
81
+ toolContext: {
82
+ taskId: task.id,
83
+ stageId: payload.stageId || null,
84
+ logicalTaskId: payload.logicalTaskId || null
85
+ }
86
+ })
87
+
88
+ const plannedFiles = Array.isArray(payload.plannedFiles)
89
+ ? payload.plannedFiles.map((item) => String(item || "").trim()).filter(Boolean)
90
+ : []
91
+ const completedFilesFromTools = out.toolEvents
92
+ .filter((event) => ["write", "edit"].includes(event.name) && event.status === "completed")
93
+ .map((event) => {
94
+ const p = event.args?.path
95
+ return p ? String(p).trim() : ""
96
+ })
97
+ .filter(Boolean)
98
+
99
+ const fileChanges = out.toolEvents
100
+ .flatMap((event) => Array.isArray(event?.metadata?.fileChanges) ? event.metadata.fileChanges : [])
101
+ .map((item) => ({
102
+ path: String(item?.path || "").trim(),
103
+ addedLines: Math.max(0, Number(item?.addedLines || 0)),
104
+ removedLines: Math.max(0, Number(item?.removedLines || 0)),
105
+ stageId: item?.stageId ? String(item.stageId) : (payload.stageId || ""),
106
+ taskId: item?.taskId ? String(item.taskId) : (payload.logicalTaskId || "")
107
+ }))
108
+ .filter((item) => item.path)
109
+
110
+ const completedFileSet = new Set(
111
+ completedFilesFromTools.filter((file) => plannedFiles.length === 0 || plannedFiles.includes(file))
112
+ )
113
+ const completedFiles = [...completedFileSet]
114
+ const remainingFiles = plannedFiles.filter((file) => !completedFileSet.has(file))
115
+
116
+ return {
117
+ session_id: payload.subSessionId,
118
+ parent_session_id: payload.parentSessionId || null,
119
+ subagent: payload.subagent || null,
120
+ reply: out.reply,
121
+ tool_events: out.toolEvents?.length || 0,
122
+ completed_files: completedFiles,
123
+ remaining_files: remainingFiles,
124
+ file_changes: fileChanges,
125
+ cost: out.cost,
126
+ budget_warnings: out.budgetWarnings || []
127
+ }
128
+ }
129
+
130
+ const SILENT_ERROR_PATTERNS = [
131
+ /provider[\s._-]*error/i,
132
+ /api[\s._-]*timeout/i,
133
+ /rate[\s._-]?limit/i,
134
+ /\b(429|503|502|500)\b/,
135
+ /missing api key/i,
136
+ /stream idle timeout/i,
137
+ /\b(econnreset|econnrefused|etimedout)\b/i,
138
+ /budget exceeded/i
139
+ ]
140
+
141
+ function detectSilentError(result, payload) {
142
+ const reply = String(result?.reply || "")
143
+ const toolEvents = Number(result?.tool_events || 0)
144
+ const plannedFiles = Array.isArray(payload?.plannedFiles) ? payload.plannedFiles : []
145
+ const completedFiles = Array.isArray(result?.completed_files) ? result.completed_files : []
146
+ const remainingFiles = Array.isArray(result?.remaining_files) ? result.remaining_files : []
147
+
148
+ // Guard: tasks without plannedFiles (review/analysis) skip all detection
149
+ if (plannedFiles.length === 0) return { hasError: false, errorMessage: "" }
150
+
151
+ // Guard: [TASK_COMPLETE] marker present — trust the agent's self-report
152
+ if (reply.toLowerCase().includes("[task_complete]")) return { hasError: false, errorMessage: "" }
153
+
154
+ // Guard: has tool activity and substantial reply — likely real work done
155
+ if (toolEvents > 0 && reply.length >= 200) return { hasError: false, errorMessage: "" }
156
+
157
+ // Pattern matching: known provider error signatures in reply
158
+ for (const pattern of SILENT_ERROR_PATTERNS) {
159
+ if (pattern.test(reply)) {
160
+ return { hasError: true, errorMessage: `silent provider error detected: ${reply.slice(0, 200)}` }
161
+ }
162
+ }
163
+
164
+ // Heuristic: planned files exist but none completed, low activity
165
+ if (completedFiles.length === 0
166
+ && remainingFiles.length === plannedFiles.length
167
+ && (reply.length < 200 || toolEvents === 0)) {
168
+ return { hasError: true, errorMessage: `heuristic: no files completed, no tool activity (reply ${reply.length} chars, ${toolEvents} tool events)` }
169
+ }
170
+
171
+ return { hasError: false, errorMessage: "" }
172
+ }
173
+
174
+ async function main() {
175
+ const taskId = argValue("--task-id") || process.env.KKCODE_BACKGROUND_TASK_ID || null
176
+ if (!taskId) {
177
+ process.exit(1)
178
+ return
179
+ }
180
+
181
+ await ensureBackgroundTaskRuntimeDir()
182
+ const task = await readTask(taskId)
183
+ if (!task) {
184
+ process.exit(1)
185
+ return
186
+ }
187
+
188
+ if (task.cancelled) {
189
+ await patchTask(taskId, () => ({
190
+ status: "cancelled",
191
+ endedAt: now()
192
+ }))
193
+ process.exit(0)
194
+ return
195
+ }
196
+
197
+ await patchTask(taskId, () => ({
198
+ status: "running",
199
+ workerPid: process.pid,
200
+ startedAt: now(),
201
+ lastHeartbeatAt: now()
202
+ }))
203
+
204
+ const abortController = new AbortController()
205
+ const parentPid = process.ppid
206
+ const heartbeatTimer = setInterval(() => {
207
+ patchTask(taskId, () => ({ lastHeartbeatAt: now() })).catch(() => {})
208
+ }, 2000)
209
+
210
+ const cancelPoll = setInterval(() => {
211
+ // Orphan detection: if parent process died, self-terminate
212
+ try { process.kill(parentPid, 0) } catch {
213
+ if (!abortController.signal.aborted) {
214
+ abortController.abort(makeAbortError("parent process exited, worker orphaned"))
215
+ }
216
+ return
217
+ }
218
+ readTask(taskId).then((latest) => {
219
+ if (latest?.cancelled && !abortController.signal.aborted) {
220
+ abortController.abort(makeAbortError("cancelled by user"))
221
+ }
222
+ }).catch(() => {})
223
+ }, 1500)
224
+
225
+ const timeoutMs = Math.max(1000, Number(task.payload?.workerTimeoutMs || 900000))
226
+ const timeoutTimer = setTimeout(() => {
227
+ if (!abortController.signal.aborted) {
228
+ abortController.abort(makeAbortError(`worker timeout after ${timeoutMs}ms`))
229
+ }
230
+ }, timeoutMs)
231
+
232
+ try {
233
+ await appendTaskLog(taskId, `task started (worker pid=${process.pid})`)
234
+
235
+ const latest = await readTask(taskId)
236
+ if (!latest?.payload?.workerType || latest.payload.workerType !== "delegate_task") {
237
+ throw new Error(`unsupported workerType: ${latest?.payload?.workerType || "unknown"}`)
238
+ }
239
+
240
+ const result = await runDelegateTask(latest, abortController.signal)
241
+ const silentCheck = detectSilentError(result, latest.payload)
242
+ if (silentCheck.hasError) {
243
+ await appendTaskLog(taskId, `silent error detected: ${silentCheck.errorMessage}`)
244
+ await patchTask(taskId, () => ({
245
+ status: "error",
246
+ result,
247
+ error: silentCheck.errorMessage,
248
+ endedAt: now(),
249
+ lastHeartbeatAt: now()
250
+ }))
251
+ process.exit(1)
252
+ } else {
253
+ await appendTaskLog(taskId, "task completed")
254
+ await patchTask(taskId, () => ({
255
+ status: "completed",
256
+ result,
257
+ error: null,
258
+ endedAt: now(),
259
+ lastHeartbeatAt: now()
260
+ }))
261
+ process.exit(0)
262
+ }
263
+ } catch (error) {
264
+ const latest = await readTask(taskId)
265
+ const cancelled = latest?.cancelled
266
+ const aborted = isAbortError(error)
267
+ if (cancelled) {
268
+ await appendTaskLog(taskId, "task cancelled")
269
+ await patchTask(taskId, () => ({
270
+ status: "cancelled",
271
+ endedAt: now(),
272
+ error: null
273
+ }))
274
+ process.exit(0)
275
+ return
276
+ }
277
+
278
+ if (aborted) {
279
+ await appendTaskLog(taskId, `task interrupted: ${error.message}`)
280
+ await patchTask(taskId, () => ({
281
+ status: "interrupted",
282
+ error: error.message,
283
+ endedAt: now()
284
+ }))
285
+ process.exit(2)
286
+ return
287
+ }
288
+
289
+ await appendTaskLog(taskId, `task error: ${error.message}`)
290
+ await patchTask(taskId, () => ({
291
+ status: "error",
292
+ error: error.message,
293
+ endedAt: now()
294
+ }))
295
+ process.exit(1)
296
+ } finally {
297
+ clearInterval(heartbeatTimer)
298
+ clearInterval(cancelPoll)
299
+ clearTimeout(timeoutTimer)
300
+ }
301
+ }
302
+
303
+ main().catch(() => {
304
+ process.exit(1)
305
+ })