@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,276 @@
1
+ import { requestProvider } from "../provider/router.mjs"
2
+ import { getConversationHistory, replaceMessages } from "./store.mjs"
3
+ import { HookBus } from "../plugin/hook-bus.mjs"
4
+ import { saveCheckpoint } from "./checkpoint.mjs"
5
+ import { recordTurn } from "../usage/usage-meter.mjs"
6
+ import { loadPricing, calculateCost } from "../usage/pricing.mjs"
7
+
8
+ const COMPACTION_SYSTEM = `You are a conversation summarizer. Create a structured summary preserving all critical information for continued work.
9
+
10
+ ## Output Format
11
+
12
+ <summary>
13
+ <goal>The user's overall goal or current task</goal>
14
+ <completed>
15
+ - Completed task with specific details (file paths, function names, line numbers)
16
+ </completed>
17
+ <in_progress>Current work being done, if any</in_progress>
18
+ <files_modified>
19
+ - path/to/file: specific change description
20
+ </files_modified>
21
+ <key_decisions>
22
+ - Decision and reasoning
23
+ - User preferences or constraints
24
+ </key_decisions>
25
+ <errors_resolved>
26
+ - Error description → fix applied
27
+ </errors_resolved>
28
+ <next_steps>
29
+ - Specific next action items
30
+ </next_steps>
31
+ </summary>
32
+
33
+ Rules:
34
+ - Use the SAME LANGUAGE as the conversation
35
+ - Preserve ALL file paths, function names, variable names, and technical identifiers exactly
36
+ - Include specific code changes, not just "modified file X"
37
+ - Omit tool call metadata and message formatting details
38
+ - Be concise but never drop actionable information`
39
+
40
+ const DEFAULT_THRESHOLD_MESSAGES = 50
41
+ const DEFAULT_THRESHOLD_RATIO = 0.7
42
+ const DEFAULT_KEEP_RECENT = 6
43
+ const TOOL_RESULT_PREVIEW_LIMIT = 200
44
+
45
+ // Estimate tokens from a string, accounting for CJK characters (~1.5 chars/token vs ~4 for Latin)
46
+ export function estimateStringTokens(str) {
47
+ if (!str) return 0
48
+ let cjk = 0
49
+ for (let i = 0; i < str.length; i++) {
50
+ const code = str.charCodeAt(i)
51
+ if ((code >= 0x4E00 && code <= 0x9FFF) || (code >= 0x3000 && code <= 0x30FF) ||
52
+ (code >= 0xAC00 && code <= 0xD7AF)) cjk++
53
+ }
54
+ const latin = str.length - cjk
55
+ return Math.ceil(latin / 4 + cjk / 1.5)
56
+ }
57
+
58
+ const MSG_OVERHEAD = 4 // ~4 tokens per message for role/metadata
59
+
60
+ export function estimateTokenCount(messages) {
61
+ let tokens = 0
62
+ for (const msg of messages) {
63
+ tokens += MSG_OVERHEAD
64
+ const content = msg.content
65
+ if (Array.isArray(content)) {
66
+ for (const block of content) {
67
+ if (block.type === "image") {
68
+ tokens += 1600 // conservative estimate for a typical image
69
+ } else if (block.type === "tool_use") {
70
+ tokens += estimateStringTokens(block.name || "")
71
+ tokens += estimateStringTokens(JSON.stringify(block.input || {}))
72
+ } else if (block.type === "tool_result") {
73
+ tokens += estimateStringTokens(String(block.content || ""))
74
+ } else {
75
+ tokens += estimateStringTokens(block.text || block.content || "")
76
+ }
77
+ }
78
+ } else {
79
+ tokens += estimateStringTokens(content || "")
80
+ }
81
+ }
82
+ return tokens
83
+ }
84
+
85
+ /**
86
+ * Pre-prune messages before LLM summarization.
87
+ * - Truncate large tool_result content to a short preview
88
+ * - Keep tool_use blocks intact (they show model intent)
89
+ * - Truncate very long plain-text assistant/user messages
90
+ */
91
+ export function pruneForSummary(messages, previewLimit = TOOL_RESULT_PREVIEW_LIMIT) {
92
+ return messages.map((msg) => {
93
+ const content = msg.content
94
+ if (Array.isArray(content)) {
95
+ const pruned = content.map((block) => {
96
+ if (block.type === "tool_result") {
97
+ const raw = String(block.content || "")
98
+ if (raw.length > previewLimit) {
99
+ return {
100
+ ...block,
101
+ content: `${raw.slice(0, previewLimit)}... [truncated ${raw.length} chars]`
102
+ }
103
+ }
104
+ }
105
+ return block
106
+ })
107
+ return { ...msg, content: pruned }
108
+ }
109
+ // Truncate very long plain-text messages (e.g. large tool output pasted as text)
110
+ if (typeof content === "string" && content.length > 2000) {
111
+ return { ...msg, content: `${content.slice(0, 2000)}... [truncated ${content.length} chars]` }
112
+ }
113
+ return msg
114
+ })
115
+ }
116
+
117
+ const BUILTIN_CONTEXT = {
118
+ "gpt-5": 272000, "o3": 200000, "o1": 200000,
119
+ "claude-opus-4": 200000, "claude-3-5": 200000, "claude-3.5": 200000, "claude": 200000,
120
+ "gemini-2": 1048576, "gemini-1.5": 1048576, "gemini": 128000,
121
+ "gpt-4o": 128000, "gpt-4": 128000, "gpt-3.5": 16000,
122
+ "deepseek": 64000, "qwen": 128000
123
+ }
124
+
125
+ export function modelContextLimit(model, configState = null) {
126
+ const m = String(model || "").toLowerCase()
127
+ // 1) Check provider-level context_limit for the active provider
128
+ const providerCfg = configState?.config?.provider
129
+ if (providerCfg) {
130
+ // Per-model override from provider.model_context map
131
+ const mc = providerCfg.model_context
132
+ if (mc) {
133
+ if (mc[model]) return mc[model]
134
+ for (const key of Object.keys(mc)) {
135
+ if (m.startsWith(key.toLowerCase())) return mc[key]
136
+ }
137
+ }
138
+ // Provider-level context_limit
139
+ const active = providerCfg[providerCfg.default]
140
+ if (active?.context_limit > 0) return active.context_limit
141
+ }
142
+ // 2) Builtin prefix match
143
+ for (const [prefix, limit] of Object.entries(BUILTIN_CONTEXT)) {
144
+ if (m.includes(prefix)) return limit
145
+ }
146
+ return 128000
147
+ }
148
+
149
+ export function contextUtilization(messages, model, configState = null) {
150
+ const tokens = estimateTokenCount(messages)
151
+ const limit = modelContextLimit(model, configState)
152
+ const ratio = limit > 0 ? Math.min(1, tokens / limit) : 0
153
+ return {
154
+ tokens,
155
+ limit,
156
+ ratio,
157
+ percent: Math.round(ratio * 100)
158
+ }
159
+ }
160
+
161
+ export function supportsNativeCompaction(providerType, model) {
162
+ if (providerType !== "anthropic") return false
163
+ const m = String(model || "").toLowerCase()
164
+ return m.includes("claude") && (m.includes("opus") || m.includes("sonnet"))
165
+ }
166
+
167
+ export function shouldCompact({ messages, model, thresholdMessages = DEFAULT_THRESHOLD_MESSAGES, thresholdRatio = DEFAULT_THRESHOLD_RATIO, configState = null, realTokenCount = null }) {
168
+ if (messages.length >= thresholdMessages) return true
169
+ const limit = modelContextLimit(model, configState)
170
+ const tokens = realTokenCount != null ? realTokenCount : estimateTokenCount(messages)
171
+ return tokens >= limit * thresholdRatio
172
+ }
173
+
174
+ export async function compactSession({
175
+ sessionId,
176
+ model,
177
+ providerType,
178
+ configState,
179
+ keepRecent = DEFAULT_KEEP_RECENT,
180
+ baseUrl = null,
181
+ apiKeyEnv = null
182
+ }) {
183
+ const history = await getConversationHistory(sessionId, 9999)
184
+ if (history.length <= keepRecent + 2) return { compacted: false, reason: "too few messages" }
185
+
186
+ // Find split point that doesn't break tool_use/tool_result pairs
187
+ let splitIdx = history.length - keepRecent
188
+ while (splitIdx > 0 && splitIdx < history.length) {
189
+ const msg = history[splitIdx]
190
+ const content = msg.content
191
+ if (Array.isArray(content) && content.some(b => b.type === "tool_result")) {
192
+ splitIdx-- // include the paired assistant tool_use message
193
+ continue
194
+ }
195
+ break
196
+ }
197
+ const toSummarize = history.slice(0, splitIdx)
198
+ const kept = history.slice(splitIdx)
199
+
200
+ // Layer 1: prune large tool outputs before sending to LLM
201
+ const pruned = pruneForSummary(toSummarize)
202
+ const summaryPrompt = pruned.map((m) => {
203
+ const content = m.content
204
+ if (Array.isArray(content)) {
205
+ return `[${m.role}]: ${content.map((b) => {
206
+ if (b.type === "text") return b.text || ""
207
+ if (b.type === "tool_use") return `[tool_use:${b.name}(${JSON.stringify(b.input || {}).slice(0, 120)})]`
208
+ if (b.type === "tool_result") return `[tool_result:${b.is_error ? "ERROR " : ""}${b.content || ""}]`
209
+ return ""
210
+ }).filter(Boolean).join("\n")}`
211
+ }
212
+ return `[${m.role}]: ${content}`
213
+ }).join("\n\n")
214
+
215
+ const hookPayload = await HookBus.sessionCompacting({
216
+ sessionId,
217
+ messageCount: history.length,
218
+ summarizeCount: toSummarize.length,
219
+ keepCount: kept.length
220
+ })
221
+ if (hookPayload?.skip) return { compacted: false, reason: "skipped by hook" }
222
+
223
+ let summaryText
224
+ let compactionUsage = null
225
+ try {
226
+ const response = await requestProvider({
227
+ configState,
228
+ providerType,
229
+ model,
230
+ system: COMPACTION_SYSTEM,
231
+ messages: [{ role: "user", content: summaryPrompt }],
232
+ tools: [],
233
+ baseUrl,
234
+ apiKeyEnv
235
+ })
236
+ summaryText = (response.text || "").trim()
237
+ compactionUsage = response.usage || null
238
+ } catch (error) {
239
+ return { compacted: false, reason: `compaction LLM call failed: ${error.message}` }
240
+ }
241
+
242
+ if (!summaryText) return { compacted: false, reason: "empty summary from LLM" }
243
+
244
+ // Replace all messages with: [summary] + [kept recent messages]
245
+ const summaryMessage = {
246
+ role: "user",
247
+ content: `<compaction-summary>\n${summaryText}\n</compaction-summary>`
248
+ }
249
+ await replaceMessages(sessionId, [summaryMessage, ...kept])
250
+
251
+ // Record compaction LLM usage so it's not "invisible"
252
+ if (compactionUsage) {
253
+ try {
254
+ const { pricing } = await loadPricing(configState)
255
+ const { amount } = calculateCost(pricing, model, compactionUsage)
256
+ await recordTurn({ sessionId, usage: compactionUsage, cost: amount })
257
+ } catch { /* best-effort */ }
258
+ }
259
+
260
+ await saveCheckpoint(sessionId, {
261
+ kind: "compaction",
262
+ iteration: 0,
263
+ compactedAt: Date.now(),
264
+ summarizeCount: toSummarize.length,
265
+ keepCount: kept.length,
266
+ summaryVersion: 1,
267
+ summaryLength: summaryText.length
268
+ })
269
+
270
+ return {
271
+ compacted: true,
272
+ summarizedCount: toSummarize.length,
273
+ keptCount: kept.length,
274
+ summaryLength: summaryText.length
275
+ }
276
+ }
@@ -0,0 +1,225 @@
1
+ import { randomUUID } from "node:crypto"
2
+ import { loadPricing, calculateCost } from "../usage/pricing.mjs"
3
+ import { recordTurn } from "../usage/usage-meter.mjs"
4
+ import { processTurnLoop } from "./loop.mjs"
5
+ import { runLongAgent } from "./longagent.mjs"
6
+ import { touchSession, setBudgetState } from "./store.mjs"
7
+ import { appendEventLog } from "../storage/event-log.mjs"
8
+ import { EventBus } from "../core/events.mjs"
9
+ import { ToolRegistry } from "../tool/registry.mjs"
10
+ import { resolveAgentForMode } from "../agent/agent.mjs"
11
+ import { estimateStringTokens } from "./compaction.mjs"
12
+
13
+ let sinkReady = false
14
+
15
+ function estimateTokens(text) {
16
+ return Math.max(1, estimateStringTokens(text || ""))
17
+ }
18
+
19
+ export function resolveMode(inputMode = "agent") {
20
+ const mode = String(inputMode || "agent").toLowerCase()
21
+ if (["ask", "plan", "agent", "longagent"].includes(mode)) return mode
22
+ return "agent"
23
+ }
24
+
25
+ export function newSessionId() {
26
+ return `ses_${randomUUID().slice(0, 12)}`
27
+ }
28
+
29
+ function maybeRegisterSink() {
30
+ if (sinkReady) return
31
+ EventBus.registerSink(async (event) => {
32
+ await appendEventLog(event)
33
+ })
34
+ sinkReady = true
35
+ }
36
+
37
+ function evaluateBudget(config, meter) {
38
+ const budget = config.usage?.budget || {}
39
+ const warnings = []
40
+ const strategy = budget.strategy || "warn"
41
+ const warnAt = Number(budget.warn_at_percent || 80)
42
+ let exceeded = false
43
+
44
+ if (budget.session_usd && meter.session.cost > 0) {
45
+ const ratio = (meter.session.cost / budget.session_usd) * 100
46
+ if (ratio >= 100) exceeded = true
47
+ if (ratio >= warnAt) warnings.push(`session budget ${ratio.toFixed(1)}% (${meter.session.cost.toFixed(4)}/${budget.session_usd})`)
48
+ }
49
+ if (budget.global_usd && meter.global.cost > 0) {
50
+ const ratio = (meter.global.cost / budget.global_usd) * 100
51
+ if (ratio >= 100) exceeded = true
52
+ if (ratio >= warnAt) warnings.push(`global budget ${ratio.toFixed(1)}% (${meter.global.cost.toFixed(4)}/${budget.global_usd})`)
53
+ }
54
+ return { warnings, exceeded, strategy }
55
+ }
56
+
57
+ export async function executeTurn({
58
+ prompt,
59
+ contentBlocks = null,
60
+ mode,
61
+ model,
62
+ sessionId,
63
+ configState,
64
+ providerType = null,
65
+ baseUrl = null,
66
+ apiKeyEnv = null,
67
+ maxIterations = null,
68
+ signal = null,
69
+ output = null,
70
+ allowQuestion = true,
71
+ toolContext = {}
72
+ }) {
73
+ maybeRegisterSink()
74
+
75
+ const resolvedProviderType = providerType || configState.config.provider.default
76
+ const agent = resolveAgentForMode(mode)
77
+ await ToolRegistry.initialize({
78
+ config: configState.config,
79
+ cwd: process.cwd()
80
+ })
81
+ await touchSession({
82
+ sessionId,
83
+ mode,
84
+ model,
85
+ providerType: resolvedProviderType,
86
+ cwd: process.cwd(),
87
+ status: mode === "longagent" ? "running-longagent" : "active"
88
+ })
89
+
90
+ const turn =
91
+ mode === "longagent"
92
+ ? await runLongAgent({
93
+ prompt,
94
+ model,
95
+ providerType: resolvedProviderType,
96
+ sessionId,
97
+ configState,
98
+ baseUrl,
99
+ apiKeyEnv,
100
+ agent,
101
+ maxIterations:
102
+ maxIterations === null
103
+ ? Number(configState.config.agent.longagent.max_iterations || 0)
104
+ : Number(maxIterations),
105
+ signal,
106
+ output,
107
+ allowQuestion,
108
+ toolContext
109
+ })
110
+ : await processTurnLoop({
111
+ prompt,
112
+ contentBlocks,
113
+ mode,
114
+ model,
115
+ providerType: resolvedProviderType,
116
+ sessionId,
117
+ configState,
118
+ baseUrl,
119
+ apiKeyEnv,
120
+ agent,
121
+ output,
122
+ signal,
123
+ allowQuestion,
124
+ toolContext
125
+ })
126
+
127
+ const usage = { ...turn.usage }
128
+ let estimated = false
129
+ if ((usage.input || 0) === 0 && (usage.output || 0) === 0) {
130
+ usage.input = estimateTokens(prompt)
131
+ usage.output = estimateTokens(turn.reply)
132
+ estimated = true
133
+ }
134
+
135
+ const pricingInfo = await loadPricing(configState)
136
+ const costInfo = calculateCost(pricingInfo.pricing, model, usage)
137
+ const meter = await recordTurn({ sessionId, usage, cost: costInfo.amount })
138
+ const budgetResult = evaluateBudget(configState.config, meter)
139
+
140
+ await setBudgetState(sessionId, {
141
+ lastTurnCost: costInfo.amount,
142
+ warnings: budgetResult.warnings,
143
+ exceeded: budgetResult.exceeded,
144
+ updatedAt: Date.now()
145
+ })
146
+
147
+ if (budgetResult.exceeded && budgetResult.strategy === "block") {
148
+ const msg = `budget exceeded — ${budgetResult.warnings.join("; ")}. strategy=block, stopping execution.`
149
+ return {
150
+ reply: msg,
151
+ mode,
152
+ model,
153
+ sessionId,
154
+ turnId: turn.turnId,
155
+ emittedText: turn.emittedText,
156
+ context: turn.context,
157
+ tokenMeter: { ...meter, estimated: estimated || costInfo.unknown },
158
+ cost: costInfo.amount,
159
+ costSavings: costInfo.savings,
160
+ pricingWarnings: pricingInfo.errors,
161
+ budgetWarnings: budgetResult.warnings,
162
+ budgetExceeded: true,
163
+ toolEvents: turn.toolEvents,
164
+ longagent: mode === "longagent"
165
+ ? {
166
+ status: turn.status,
167
+ phase: turn.phase,
168
+ gateStatus: turn.gateStatus,
169
+ currentGate: turn.currentGate,
170
+ lastGateFailures: turn.lastGateFailures || [],
171
+ iterations: turn.iterations,
172
+ recoveryCount: turn.recoveryCount,
173
+ progress: turn.progress,
174
+ elapsed: turn.elapsed,
175
+ stageIndex: turn.stageIndex,
176
+ stageCount: turn.stageCount,
177
+ currentStageId: turn.currentStageId,
178
+ planFrozen: turn.planFrozen,
179
+ taskProgress: turn.taskProgress,
180
+ stageProgress: turn.stageProgress,
181
+ remainingFilesCount: turn.remainingFilesCount,
182
+ fileChanges: turn.fileChanges || []
183
+ }
184
+ : null
185
+ }
186
+ }
187
+
188
+ return {
189
+ reply: turn.reply,
190
+ mode,
191
+ model,
192
+ sessionId,
193
+ turnId: turn.turnId,
194
+ emittedText: turn.emittedText,
195
+ context: turn.context,
196
+ tokenMeter: { ...meter, estimated: estimated || costInfo.unknown },
197
+ cost: costInfo.amount,
198
+ costSavings: costInfo.savings,
199
+ pricingWarnings: pricingInfo.errors,
200
+ budgetWarnings: budgetResult.warnings,
201
+ budgetExceeded: false,
202
+ toolEvents: turn.toolEvents,
203
+ longagent: mode === "longagent"
204
+ ? {
205
+ status: turn.status,
206
+ phase: turn.phase,
207
+ gateStatus: turn.gateStatus,
208
+ currentGate: turn.currentGate,
209
+ lastGateFailures: turn.lastGateFailures || [],
210
+ iterations: turn.iterations,
211
+ recoveryCount: turn.recoveryCount,
212
+ progress: turn.progress,
213
+ elapsed: turn.elapsed,
214
+ stageIndex: turn.stageIndex,
215
+ stageCount: turn.stageCount,
216
+ currentStageId: turn.currentStageId,
217
+ planFrozen: turn.planFrozen,
218
+ taskProgress: turn.taskProgress,
219
+ stageProgress: turn.stageProgress,
220
+ remainingFilesCount: turn.remainingFilesCount,
221
+ fileChanges: turn.fileChanges || []
222
+ }
223
+ : null
224
+ }
225
+ }
@@ -0,0 +1,172 @@
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises"
2
+ import path from "node:path"
3
+ import { memoryDir } from "../storage/paths.mjs"
4
+
5
+ /**
6
+ * Instinct Manager — automatic pattern learning system.
7
+ *
8
+ * Instincts are small, atomic learned behaviors extracted from sessions.
9
+ * Each instinct has a confidence score (0.3–0.9) that increases with repeated observation.
10
+ * High-confidence instincts are injected into the system prompt to guide future behavior.
11
+ */
12
+
13
+ function instinctsPath(cwd) {
14
+ return path.join(memoryDir(cwd), "instincts.json")
15
+ }
16
+
17
+ function generateId() {
18
+ return "inst_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
19
+ }
20
+
21
+ async function loadStore(cwd) {
22
+ try {
23
+ const raw = await readFile(instinctsPath(cwd), "utf8")
24
+ return JSON.parse(raw)
25
+ } catch {
26
+ return { instincts: [], version: 1 }
27
+ }
28
+ }
29
+
30
+ async function saveStore(cwd, store) {
31
+ const dir = memoryDir(cwd)
32
+ await mkdir(dir, { recursive: true })
33
+ await writeFile(instinctsPath(cwd), JSON.stringify(store, null, 2) + "\n", "utf8")
34
+ }
35
+
36
+ /**
37
+ * Add or reinforce an instinct.
38
+ * If a similar pattern already exists (fuzzy match), increase its confidence.
39
+ * Otherwise create a new instinct at base confidence 0.3.
40
+ */
41
+ export async function addInstinct(cwd, pattern, category = "workflow") {
42
+ const store = await loadStore(cwd)
43
+ const normalized = pattern.trim().toLowerCase()
44
+
45
+ // Fuzzy match: check if any existing instinct is substantially similar
46
+ const existing = store.instincts.find((inst) => {
47
+ const existingNorm = inst.pattern.trim().toLowerCase()
48
+ return existingNorm === normalized || similarity(existingNorm, normalized) > 0.8
49
+ })
50
+
51
+ if (existing) {
52
+ existing.observations = (existing.observations || 1) + 1
53
+ existing.confidence = Math.min(0.9, existing.confidence + 0.1)
54
+ existing.lastSeenAt = Date.now()
55
+ } else {
56
+ store.instincts.push({
57
+ id: generateId(),
58
+ pattern: pattern.trim(),
59
+ confidence: 0.3,
60
+ observations: 1,
61
+ category,
62
+ createdAt: Date.now(),
63
+ lastSeenAt: Date.now()
64
+ })
65
+ }
66
+
67
+ await saveStore(cwd, store)
68
+ return existing || store.instincts[store.instincts.length - 1]
69
+ }
70
+
71
+ /**
72
+ * List instincts at or above a minimum confidence threshold.
73
+ */
74
+ export async function listInstincts(cwd, minConfidence = 0.0) {
75
+ const store = await loadStore(cwd)
76
+ return store.instincts
77
+ .filter((inst) => inst.confidence >= minConfidence)
78
+ .sort((a, b) => b.confidence - a.confidence)
79
+ }
80
+
81
+ /**
82
+ * Remove an instinct by ID.
83
+ */
84
+ export async function removeInstinct(cwd, id) {
85
+ const store = await loadStore(cwd)
86
+ const before = store.instincts.length
87
+ store.instincts = store.instincts.filter((inst) => inst.id !== id)
88
+ if (store.instincts.length < before) {
89
+ await saveStore(cwd, store)
90
+ return true
91
+ }
92
+ return false
93
+ }
94
+
95
+ /**
96
+ * Export all instincts for team sharing.
97
+ */
98
+ export async function exportInstincts(cwd) {
99
+ const store = await loadStore(cwd)
100
+ return {
101
+ exportedAt: Date.now(),
102
+ count: store.instincts.length,
103
+ instincts: store.instincts
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Import instincts from a teammate's export.
109
+ * Merges by pattern similarity — existing patterns get reinforced, new ones get added at 0.3.
110
+ */
111
+ export async function importInstincts(cwd, data) {
112
+ if (!data || !Array.isArray(data.instincts)) return { imported: 0, reinforced: 0 }
113
+ let imported = 0
114
+ let reinforced = 0
115
+ for (const inst of data.instincts) {
116
+ if (!inst.pattern) continue
117
+ const result = await addInstinct(cwd, inst.pattern, inst.category || "workflow")
118
+ if (result.observations > 1) reinforced++
119
+ else imported++
120
+ }
121
+ return { imported, reinforced }
122
+ }
123
+
124
+ /**
125
+ * Format high-confidence instincts for system prompt injection.
126
+ * Returns a prompt section string, or empty string if no qualifying instincts.
127
+ */
128
+ export async function formatInstinctsForPrompt(cwd, minConfidence = 0.5) {
129
+ const instincts = await listInstincts(cwd, minConfidence)
130
+ if (instincts.length === 0) return ""
131
+
132
+ const lines = [
133
+ "",
134
+ "## Learned Patterns",
135
+ "",
136
+ "These patterns have been observed across your sessions. Follow them unless the user explicitly requests otherwise:",
137
+ ""
138
+ ]
139
+
140
+ for (const inst of instincts.slice(0, 20)) {
141
+ const conf = inst.confidence.toFixed(1)
142
+ lines.push(`- [${conf}] ${inst.pattern}`)
143
+ }
144
+
145
+ if (instincts.length > 20) {
146
+ lines.push(` ... and ${instincts.length - 20} more learned patterns`)
147
+ }
148
+
149
+ return lines.join("\n")
150
+ }
151
+
152
+ // ── Simple string similarity (Dice coefficient) ──
153
+
154
+ function similarity(a, b) {
155
+ if (a === b) return 1
156
+ if (a.length < 2 || b.length < 2) return 0
157
+ const bigrams = new Map()
158
+ for (let i = 0; i < a.length - 1; i++) {
159
+ const bi = a.slice(i, i + 2)
160
+ bigrams.set(bi, (bigrams.get(bi) || 0) + 1)
161
+ }
162
+ let matches = 0
163
+ for (let i = 0; i < b.length - 1; i++) {
164
+ const bi = b.slice(i, i + 2)
165
+ const count = bigrams.get(bi) || 0
166
+ if (count > 0) {
167
+ matches++
168
+ bigrams.set(bi, count - 1)
169
+ }
170
+ }
171
+ return (2 * matches) / (a.length + b.length - 2)
172
+ }
@@ -0,0 +1,25 @@
1
+ import path from "node:path"
2
+ import { access, readFile } from "node:fs/promises"
3
+
4
+ const CANDIDATES = ["AGENTS.md", "CLAUDE.md", "CONTEXT.md", "KKCODE.md", ".kkcode.md", "kkcode.md"]
5
+
6
+ async function exists(file) {
7
+ try {
8
+ await access(file)
9
+ return true
10
+ } catch {
11
+ return false
12
+ }
13
+ }
14
+
15
+ export async function loadInstructions(cwd = process.cwd()) {
16
+ const blocks = []
17
+ for (const file of CANDIDATES) {
18
+ const target = path.join(cwd, file)
19
+ if (!(await exists(target))) continue
20
+ const content = (await readFile(target, "utf8")).trim()
21
+ if (!content) continue
22
+ blocks.push(`Instructions from ${target}\n${content}`)
23
+ }
24
+ return blocks
25
+ }