@kkelly-offical/kkcode 0.1.3 → 0.1.7
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/README.md +110 -172
- package/package.json +46 -46
- package/src/agent/agent.mjs +220 -170
- package/src/agent/prompt/bug-hunter.txt +90 -0
- package/src/agent/prompt/frontend-designer.txt +58 -0
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -0
- package/src/agent/prompt/longagent-coding-agent.txt +37 -0
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -0
- package/src/agent/prompt/longagent-preview-agent.txt +63 -0
- package/src/config/defaults.mjs +260 -195
- package/src/config/schema.mjs +71 -6
- package/src/core/constants.mjs +91 -46
- package/src/index.mjs +1 -1
- package/src/knowledge/frontend-aesthetics.txt +39 -0
- package/src/knowledge/loader.mjs +2 -1
- package/src/knowledge/tailwind.txt +12 -3
- package/src/mcp/client-http.mjs +141 -157
- package/src/mcp/client-sse.mjs +288 -286
- package/src/mcp/client-stdio.mjs +533 -451
- package/src/mcp/constants.mjs +2 -0
- package/src/mcp/registry.mjs +479 -394
- package/src/mcp/stdio-framing.mjs +133 -127
- package/src/mcp/tool-result.mjs +24 -0
- package/src/observability/index.mjs +42 -0
- package/src/observability/metrics.mjs +137 -0
- package/src/observability/tracer.mjs +137 -0
- package/src/orchestration/background-manager.mjs +372 -358
- package/src/orchestration/background-worker.mjs +305 -245
- package/src/orchestration/longagent-manager.mjs +171 -116
- package/src/orchestration/stage-scheduler.mjs +728 -489
- package/src/permission/exec-policy.mjs +9 -11
- package/src/provider/anthropic.mjs +1 -0
- package/src/provider/openai.mjs +340 -339
- package/src/provider/retry-policy.mjs +68 -68
- package/src/provider/router.mjs +241 -228
- package/src/provider/sse.mjs +104 -91
- package/src/repl.mjs +59 -7
- package/src/session/checkpoint.mjs +66 -3
- package/src/session/compaction.mjs +298 -276
- package/src/session/engine.mjs +232 -225
- package/src/session/longagent-4stage.mjs +460 -0
- package/src/session/longagent-hybrid.mjs +1097 -0
- package/src/session/longagent-plan.mjs +365 -329
- package/src/session/longagent-project-memory.mjs +53 -0
- package/src/session/longagent-scaffold.mjs +291 -100
- package/src/session/longagent-task-bus.mjs +54 -0
- package/src/session/longagent-utils.mjs +472 -0
- package/src/session/longagent.mjs +900 -1462
- package/src/session/loop.mjs +65 -40
- package/src/session/project-context.mjs +30 -0
- package/src/session/prompt/agent.txt +25 -0
- package/src/session/prompt/plan.txt +31 -9
- package/src/session/rollback.mjs +196 -0
- package/src/session/store.mjs +519 -503
- package/src/session/system-prompt.mjs +273 -260
- package/src/session/task-validator.mjs +4 -3
- package/src/skill/builtin/design.mjs +76 -0
- package/src/skill/builtin/frontend.mjs +8 -0
- package/src/skill/registry.mjs +390 -336
- package/src/storage/ghost-commit-store.mjs +18 -8
- package/src/tool/executor.mjs +11 -0
- package/src/tool/git-auto.mjs +0 -19
- package/src/tool/question-prompt.mjs +93 -86
- package/src/tool/registry.mjs +71 -37
- package/src/ui/activity-renderer.mjs +664 -410
- package/src/util/git.mjs +23 -0
|
@@ -1,116 +1,171 @@
|
|
|
1
|
-
import path from "node:path"
|
|
2
|
-
import { mkdir, writeFile, unlink, stat } from "node:fs/promises"
|
|
3
|
-
import { readJson, writeJson } from "../storage/json-store.mjs"
|
|
4
|
-
import { projectRootDir } from "../storage/paths.mjs"
|
|
5
|
-
|
|
6
|
-
function statePath(cwd = process.cwd()) {
|
|
7
|
-
return path.join(projectRootDir(cwd), "longagent-state.json")
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function lockPath(cwd = process.cwd()) {
|
|
11
|
-
return statePath(cwd) + ".lock"
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async function ensure(cwd = process.cwd()) {
|
|
15
|
-
await mkdir(projectRootDir(cwd), { recursive: true })
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const LOCK_TIMEOUT_MS = 5000
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function
|
|
56
|
-
await ensure(cwd)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
async
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { mkdir, writeFile, readFile, unlink, stat } from "node:fs/promises"
|
|
3
|
+
import { readJson, writeJson } from "../storage/json-store.mjs"
|
|
4
|
+
import { projectRootDir } from "../storage/paths.mjs"
|
|
5
|
+
|
|
6
|
+
function statePath(cwd = process.cwd()) {
|
|
7
|
+
return path.join(projectRootDir(cwd), "longagent-state.json")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function lockPath(cwd = process.cwd()) {
|
|
11
|
+
return statePath(cwd) + ".lock"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function ensure(cwd = process.cwd()) {
|
|
15
|
+
await mkdir(projectRootDir(cwd), { recursive: true })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const LOCK_TIMEOUT_MS = 5000
|
|
19
|
+
const LOCK_STALE_MS = LOCK_TIMEOUT_MS * 0.8 // 4000ms — detect stale before timeout
|
|
20
|
+
const LOCK_RETRY_INIT_MS = 50
|
|
21
|
+
const LOCK_RETRY_MAX_MS = 500
|
|
22
|
+
|
|
23
|
+
function isProcessAlive(pid) {
|
|
24
|
+
try {
|
|
25
|
+
process.kill(pid, 0)
|
|
26
|
+
return true
|
|
27
|
+
} catch {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function tryRemoveStaleLock(file, staleMs = LOCK_STALE_MS) {
|
|
33
|
+
try {
|
|
34
|
+
const content = await readFile(file, "utf-8")
|
|
35
|
+
const [pidStr] = content.split(":")
|
|
36
|
+
const pid = Number(pidStr)
|
|
37
|
+
// If PID is valid and process is dead, remove immediately
|
|
38
|
+
if (pid > 0 && !isProcessAlive(pid)) {
|
|
39
|
+
await unlink(file).catch(() => {})
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
// Otherwise check mtime-based staleness
|
|
43
|
+
const info = await stat(file)
|
|
44
|
+
if (Date.now() - info.mtimeMs > staleMs) {
|
|
45
|
+
await unlink(file).catch(() => {})
|
|
46
|
+
return true
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// lock disappeared or unreadable — retry
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function acquireLock(cwd, lockTimeoutMs = LOCK_TIMEOUT_MS) {
|
|
56
|
+
await ensure(cwd)
|
|
57
|
+
const file = lockPath(cwd)
|
|
58
|
+
const staleMs = lockTimeoutMs * 0.8
|
|
59
|
+
const deadline = Date.now() + lockTimeoutMs
|
|
60
|
+
let retryMs = LOCK_RETRY_INIT_MS
|
|
61
|
+
|
|
62
|
+
while (Date.now() < deadline) {
|
|
63
|
+
try {
|
|
64
|
+
await writeFile(file, `${process.pid}:${Date.now()}`, { flag: "wx" })
|
|
65
|
+
return true
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err.code !== "EEXIST") throw err
|
|
68
|
+
const removed = await tryRemoveStaleLock(file, staleMs)
|
|
69
|
+
if (removed) continue
|
|
70
|
+
// Exponential backoff: 50 → 100 → 200 → 400 → 500 (capped)
|
|
71
|
+
await new Promise((r) => setTimeout(r, retryMs))
|
|
72
|
+
retryMs = Math.min(retryMs * 2, LOCK_RETRY_MAX_MS)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Final attempt after timeout
|
|
77
|
+
const removed = await tryRemoveStaleLock(file, staleMs)
|
|
78
|
+
if (removed) {
|
|
79
|
+
try {
|
|
80
|
+
await writeFile(file, `${process.pid}:${Date.now()}`, { flag: "wx" })
|
|
81
|
+
return true
|
|
82
|
+
} catch { /* another process grabbed it */ }
|
|
83
|
+
}
|
|
84
|
+
throw new Error(`Failed to acquire lock after ${lockTimeoutMs}ms: ${file}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function releaseLock(cwd) {
|
|
88
|
+
await unlink(lockPath(cwd)).catch(() => {})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function read(cwd = process.cwd()) {
|
|
92
|
+
await ensure(cwd)
|
|
93
|
+
return readJson(statePath(cwd), { sessions: {} })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function write(data, cwd = process.cwd()) {
|
|
97
|
+
await ensure(cwd)
|
|
98
|
+
await writeJson(statePath(cwd), data)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const LongAgentManager = {
|
|
102
|
+
async update(sessionId, patch, cwd = process.cwd(), config = null) {
|
|
103
|
+
const lockMs = Number(config?.agent?.longagent?.lock_timeout_ms || LOCK_TIMEOUT_MS)
|
|
104
|
+
await acquireLock(cwd, lockMs)
|
|
105
|
+
try {
|
|
106
|
+
const state = await read(cwd)
|
|
107
|
+
const current = state.sessions[sessionId] || {
|
|
108
|
+
sessionId,
|
|
109
|
+
status: "idle",
|
|
110
|
+
phase: "L0",
|
|
111
|
+
gateStatus: {},
|
|
112
|
+
currentGate: "execution",
|
|
113
|
+
recoveryCount: 0,
|
|
114
|
+
planFrozen: false,
|
|
115
|
+
currentStageId: null,
|
|
116
|
+
stageIndex: 0,
|
|
117
|
+
stageCount: 0,
|
|
118
|
+
stageStatus: null,
|
|
119
|
+
taskProgress: {},
|
|
120
|
+
remainingFiles: [],
|
|
121
|
+
remainingFilesCount: 0,
|
|
122
|
+
lastGateFailures: [],
|
|
123
|
+
createdAt: Date.now(),
|
|
124
|
+
updatedAt: Date.now(),
|
|
125
|
+
heartbeatAt: null,
|
|
126
|
+
iterations: 0,
|
|
127
|
+
lastMessage: ""
|
|
128
|
+
}
|
|
129
|
+
state.sessions[sessionId] = {
|
|
130
|
+
...current,
|
|
131
|
+
...patch,
|
|
132
|
+
updatedAt: Date.now()
|
|
133
|
+
}
|
|
134
|
+
await write(state, cwd)
|
|
135
|
+
return state.sessions[sessionId]
|
|
136
|
+
} finally {
|
|
137
|
+
await releaseLock(cwd)
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
async get(sessionId, cwd = process.cwd()) {
|
|
141
|
+
const state = await read(cwd)
|
|
142
|
+
return state.sessions[sessionId] || null
|
|
143
|
+
},
|
|
144
|
+
async list(cwd = process.cwd()) {
|
|
145
|
+
const state = await read(cwd)
|
|
146
|
+
return Object.values(state.sessions).sort((a, b) => b.updatedAt - a.updatedAt)
|
|
147
|
+
},
|
|
148
|
+
async stop(sessionId, cwd = process.cwd()) {
|
|
149
|
+
const existing = await this.get(sessionId, cwd)
|
|
150
|
+
if (!existing) return null
|
|
151
|
+
return this.update(sessionId, { stopRequested: true }, cwd)
|
|
152
|
+
},
|
|
153
|
+
async clearStop(sessionId, cwd = process.cwd()) {
|
|
154
|
+
const existing = await this.get(sessionId, cwd)
|
|
155
|
+
if (!existing) return null
|
|
156
|
+
return this.update(sessionId, { stopRequested: false }, cwd)
|
|
157
|
+
},
|
|
158
|
+
/**
|
|
159
|
+
* Execute `fn` while holding the state lock.
|
|
160
|
+
* Prevents TOCTOU races (e.g. read-status → git-merge).
|
|
161
|
+
*/
|
|
162
|
+
async withLock(fn, cwd = process.cwd(), config = null) {
|
|
163
|
+
const lockMs = Number(config?.agent?.longagent?.lock_timeout_ms || LOCK_TIMEOUT_MS)
|
|
164
|
+
await acquireLock(cwd, lockMs)
|
|
165
|
+
try {
|
|
166
|
+
return await fn()
|
|
167
|
+
} finally {
|
|
168
|
+
await releaseLock(cwd)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|