@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.
Files changed (66) hide show
  1. package/README.md +110 -172
  2. package/package.json +46 -46
  3. package/src/agent/agent.mjs +220 -170
  4. package/src/agent/prompt/bug-hunter.txt +90 -0
  5. package/src/agent/prompt/frontend-designer.txt +58 -0
  6. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -0
  7. package/src/agent/prompt/longagent-coding-agent.txt +37 -0
  8. package/src/agent/prompt/longagent-debugging-agent.txt +46 -0
  9. package/src/agent/prompt/longagent-preview-agent.txt +63 -0
  10. package/src/config/defaults.mjs +260 -195
  11. package/src/config/schema.mjs +71 -6
  12. package/src/core/constants.mjs +91 -46
  13. package/src/index.mjs +1 -1
  14. package/src/knowledge/frontend-aesthetics.txt +39 -0
  15. package/src/knowledge/loader.mjs +2 -1
  16. package/src/knowledge/tailwind.txt +12 -3
  17. package/src/mcp/client-http.mjs +141 -157
  18. package/src/mcp/client-sse.mjs +288 -286
  19. package/src/mcp/client-stdio.mjs +533 -451
  20. package/src/mcp/constants.mjs +2 -0
  21. package/src/mcp/registry.mjs +479 -394
  22. package/src/mcp/stdio-framing.mjs +133 -127
  23. package/src/mcp/tool-result.mjs +24 -0
  24. package/src/observability/index.mjs +42 -0
  25. package/src/observability/metrics.mjs +137 -0
  26. package/src/observability/tracer.mjs +137 -0
  27. package/src/orchestration/background-manager.mjs +372 -358
  28. package/src/orchestration/background-worker.mjs +305 -245
  29. package/src/orchestration/longagent-manager.mjs +171 -116
  30. package/src/orchestration/stage-scheduler.mjs +728 -489
  31. package/src/permission/exec-policy.mjs +9 -11
  32. package/src/provider/anthropic.mjs +1 -0
  33. package/src/provider/openai.mjs +340 -339
  34. package/src/provider/retry-policy.mjs +68 -68
  35. package/src/provider/router.mjs +241 -228
  36. package/src/provider/sse.mjs +104 -91
  37. package/src/repl.mjs +59 -7
  38. package/src/session/checkpoint.mjs +66 -3
  39. package/src/session/compaction.mjs +298 -276
  40. package/src/session/engine.mjs +232 -225
  41. package/src/session/longagent-4stage.mjs +460 -0
  42. package/src/session/longagent-hybrid.mjs +1097 -0
  43. package/src/session/longagent-plan.mjs +365 -329
  44. package/src/session/longagent-project-memory.mjs +53 -0
  45. package/src/session/longagent-scaffold.mjs +291 -100
  46. package/src/session/longagent-task-bus.mjs +54 -0
  47. package/src/session/longagent-utils.mjs +472 -0
  48. package/src/session/longagent.mjs +900 -1462
  49. package/src/session/loop.mjs +65 -40
  50. package/src/session/project-context.mjs +30 -0
  51. package/src/session/prompt/agent.txt +25 -0
  52. package/src/session/prompt/plan.txt +31 -9
  53. package/src/session/rollback.mjs +196 -0
  54. package/src/session/store.mjs +519 -503
  55. package/src/session/system-prompt.mjs +273 -260
  56. package/src/session/task-validator.mjs +4 -3
  57. package/src/skill/builtin/design.mjs +76 -0
  58. package/src/skill/builtin/frontend.mjs +8 -0
  59. package/src/skill/registry.mjs +390 -336
  60. package/src/storage/ghost-commit-store.mjs +18 -8
  61. package/src/tool/executor.mjs +11 -0
  62. package/src/tool/git-auto.mjs +0 -19
  63. package/src/tool/question-prompt.mjs +93 -86
  64. package/src/tool/registry.mjs +71 -37
  65. package/src/ui/activity-renderer.mjs +664 -410
  66. 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 LOCK_RETRY_MS = 50
20
-
21
- async function acquireLock(cwd) {
22
- const file = lockPath(cwd)
23
- const deadline = Date.now() + LOCK_TIMEOUT_MS
24
- while (Date.now() < deadline) {
25
- try {
26
- await writeFile(file, String(process.pid), { flag: "wx" })
27
- return true
28
- } catch (err) {
29
- if (err.code !== "EEXIST") throw err
30
- // Stale lock detection: if lock file is older than timeout, remove it
31
- try {
32
- const info = await stat(file)
33
- if (Date.now() - info.mtimeMs > LOCK_TIMEOUT_MS) {
34
- await unlink(file).catch(() => {})
35
- continue
36
- }
37
- } catch { /* lock disappeared, retry */ continue }
38
- await new Promise((r) => setTimeout(r, LOCK_RETRY_MS))
39
- }
40
- }
41
- // Timeout: force-remove stale lock and proceed
42
- await unlink(file).catch(() => {})
43
- return false
44
- }
45
-
46
- async function releaseLock(cwd) {
47
- await unlink(lockPath(cwd)).catch(() => {})
48
- }
49
-
50
- async function read(cwd = process.cwd()) {
51
- await ensure(cwd)
52
- return readJson(statePath(cwd), { sessions: {} })
53
- }
54
-
55
- async function write(data, cwd = process.cwd()) {
56
- await ensure(cwd)
57
- await writeJson(statePath(cwd), data)
58
- }
59
-
60
- export const LongAgentManager = {
61
- async update(sessionId, patch, cwd = process.cwd()) {
62
- await acquireLock(cwd)
63
- try {
64
- const state = await read(cwd)
65
- const current = state.sessions[sessionId] || {
66
- sessionId,
67
- status: "idle",
68
- phase: "L0",
69
- gateStatus: {},
70
- currentGate: "execution",
71
- recoveryCount: 0,
72
- planFrozen: false,
73
- currentStageId: null,
74
- stageIndex: 0,
75
- stageCount: 0,
76
- stageStatus: null,
77
- taskProgress: {},
78
- remainingFiles: [],
79
- remainingFilesCount: 0,
80
- lastGateFailures: [],
81
- createdAt: Date.now(),
82
- updatedAt: Date.now(),
83
- heartbeatAt: null,
84
- iterations: 0,
85
- lastMessage: ""
86
- }
87
- state.sessions[sessionId] = {
88
- ...current,
89
- ...patch,
90
- updatedAt: Date.now()
91
- }
92
- await write(state, cwd)
93
- return state.sessions[sessionId]
94
- } finally {
95
- await releaseLock(cwd)
96
- }
97
- },
98
- async get(sessionId, cwd = process.cwd()) {
99
- const state = await read(cwd)
100
- return state.sessions[sessionId] || null
101
- },
102
- async list(cwd = process.cwd()) {
103
- const state = await read(cwd)
104
- return Object.values(state.sessions).sort((a, b) => b.updatedAt - a.updatedAt)
105
- },
106
- async stop(sessionId, cwd = process.cwd()) {
107
- const existing = await this.get(sessionId, cwd)
108
- if (!existing) return null
109
- return this.update(sessionId, { stopRequested: true }, cwd)
110
- },
111
- async clearStop(sessionId, cwd = process.cwd()) {
112
- const existing = await this.get(sessionId, cwd)
113
- if (!existing) return null
114
- return this.update(sessionId, { stopRequested: false }, cwd)
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
+ }