@kkelly-offical/kkcode 0.1.7 → 0.2.1

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 (163) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +452 -387
  3. package/package.json +50 -46
  4. package/src/agent/agent.mjs +228 -220
  5. package/src/agent/custom-agent-loader.mjs +6 -3
  6. package/src/agent/generator.mjs +2 -2
  7. package/src/agent/prompt/assistant.txt +12 -0
  8. package/src/agent/prompt/bug-hunter.txt +89 -89
  9. package/src/agent/prompt/frontend-designer.txt +58 -58
  10. package/src/agent/prompt/guide.txt +1 -1
  11. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
  12. package/src/agent/prompt/longagent-coding-agent.txt +37 -37
  13. package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
  14. package/src/agent/prompt/longagent-preview-agent.txt +63 -63
  15. package/src/command/custom-commands.mjs +2 -2
  16. package/src/commands/agent.mjs +1 -1
  17. package/src/commands/background.mjs +145 -4
  18. package/src/commands/chat.mjs +117 -76
  19. package/src/commands/config.mjs +148 -1
  20. package/src/commands/doctor.mjs +30 -6
  21. package/src/commands/init.mjs +32 -6
  22. package/src/commands/longagent.mjs +117 -0
  23. package/src/commands/mcp.mjs +275 -43
  24. package/src/commands/permission.mjs +1 -1
  25. package/src/commands/session.mjs +195 -140
  26. package/src/commands/skill.mjs +63 -0
  27. package/src/commands/theme.mjs +1 -1
  28. package/src/config/defaults.mjs +280 -260
  29. package/src/config/import-config.mjs +1 -1
  30. package/src/config/load-config.mjs +61 -4
  31. package/src/config/schema.mjs +591 -574
  32. package/src/context.mjs +4 -1
  33. package/src/core/constants.mjs +97 -91
  34. package/src/core/types.mjs +1 -1
  35. package/src/github/api.mjs +78 -78
  36. package/src/github/auth.mjs +294 -286
  37. package/src/github/flow.mjs +298 -298
  38. package/src/github/workspace.mjs +225 -212
  39. package/src/index.mjs +84 -82
  40. package/src/knowledge/frontend-aesthetics.txt +38 -38
  41. package/src/mcp/client-http.mjs +139 -141
  42. package/src/mcp/client-sse.mjs +297 -288
  43. package/src/mcp/client-stdio.mjs +534 -533
  44. package/src/mcp/constants.mjs +2 -2
  45. package/src/mcp/registry.mjs +498 -479
  46. package/src/mcp/stdio-framing.mjs +135 -133
  47. package/src/mcp/tool-result.mjs +24 -24
  48. package/src/observability/edit-diagnostics.mjs +449 -0
  49. package/src/observability/index.mjs +42 -42
  50. package/src/observability/metrics.mjs +165 -137
  51. package/src/observability/tracer.mjs +137 -137
  52. package/src/onboarding.mjs +209 -0
  53. package/src/orchestration/background-manager.mjs +567 -372
  54. package/src/orchestration/background-worker.mjs +419 -305
  55. package/src/orchestration/interruption-reason.mjs +21 -0
  56. package/src/orchestration/longagent-manager.mjs +197 -171
  57. package/src/orchestration/stage-scheduler.mjs +733 -728
  58. package/src/orchestration/subagent-router.mjs +7 -1
  59. package/src/orchestration/task-scheduler.mjs +219 -7
  60. package/src/permission/engine.mjs +1 -1
  61. package/src/permission/exec-policy.mjs +370 -370
  62. package/src/permission/file-edit-policy.mjs +108 -0
  63. package/src/permission/prompt.mjs +1 -1
  64. package/src/permission/rules.mjs +116 -7
  65. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  66. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  67. package/src/plugin/hook-bus.mjs +19 -5
  68. package/src/plugin/manifest-loader.mjs +222 -0
  69. package/src/provider/anthropic.mjs +396 -390
  70. package/src/provider/ollama.mjs +7 -1
  71. package/src/provider/openai.mjs +382 -340
  72. package/src/provider/retry-policy.mjs +74 -68
  73. package/src/provider/router.mjs +242 -241
  74. package/src/provider/sse.mjs +104 -104
  75. package/src/provider/wizard.mjs +556 -0
  76. package/src/repl/capability-facade.mjs +30 -0
  77. package/src/repl/command-surface.mjs +23 -0
  78. package/src/repl/controller-entry.mjs +40 -0
  79. package/src/repl/core-shell.mjs +208 -0
  80. package/src/repl/dialog-router.mjs +87 -0
  81. package/src/repl/input-engine.mjs +76 -0
  82. package/src/repl/keymap.mjs +7 -0
  83. package/src/repl/operator-surface.mjs +15 -0
  84. package/src/repl/permission-flow.mjs +49 -0
  85. package/src/repl/runtime-facade.mjs +36 -0
  86. package/src/repl/slash-router.mjs +62 -0
  87. package/src/repl/state-store.mjs +29 -0
  88. package/src/repl/turn-controller.mjs +58 -0
  89. package/src/repl/verification.mjs +23 -0
  90. package/src/repl.mjs +3368 -2981
  91. package/src/rules/load-rules.mjs +3 -3
  92. package/src/runtime.mjs +1 -1
  93. package/src/session/agent-transaction.mjs +86 -0
  94. package/src/session/checkpoint.mjs +302 -302
  95. package/src/session/compaction.mjs +298 -298
  96. package/src/session/engine.mjs +417 -232
  97. package/src/session/longagent-4stage.mjs +467 -460
  98. package/src/session/longagent-hybrid.mjs +1344 -1097
  99. package/src/session/longagent-plan.mjs +376 -365
  100. package/src/session/longagent-project-memory.mjs +53 -53
  101. package/src/session/longagent-scaffold.mjs +291 -291
  102. package/src/session/longagent-task-bus.mjs +138 -54
  103. package/src/session/longagent-utils.mjs +828 -472
  104. package/src/session/longagent.mjs +911 -900
  105. package/src/session/loop.mjs +1005 -930
  106. package/src/session/prompt/agent.txt +25 -25
  107. package/src/session/prompt/anthropic.txt +150 -150
  108. package/src/session/prompt/beast.txt +1 -1
  109. package/src/session/prompt/plan.txt +31 -31
  110. package/src/session/prompt/qwen.txt +46 -46
  111. package/src/session/recovery.mjs +21 -0
  112. package/src/session/rollback.mjs +196 -195
  113. package/src/session/routing-observability.mjs +72 -0
  114. package/src/session/runtime-state.mjs +47 -0
  115. package/src/session/store.mjs +523 -519
  116. package/src/session/system-prompt.mjs +308 -273
  117. package/src/session/task-validator.mjs +267 -267
  118. package/src/session/usability-gates.mjs +2 -2
  119. package/src/skill/builtin/commit.mjs +64 -64
  120. package/src/skill/builtin/design.mjs +76 -76
  121. package/src/skill/generator.mjs +18 -2
  122. package/src/skill/registry.mjs +642 -390
  123. package/src/storage/audit-store.mjs +18 -11
  124. package/src/storage/event-log.mjs +7 -1
  125. package/src/storage/ghost-commit-store.mjs +243 -245
  126. package/src/storage/paths.mjs +13 -0
  127. package/src/theme/default-theme.mjs +1 -1
  128. package/src/theme/markdown.mjs +4 -0
  129. package/src/theme/schema.mjs +1 -1
  130. package/src/theme/status-bar.mjs +162 -158
  131. package/src/tool/audit-wrapper.mjs +18 -2
  132. package/src/tool/edit-transaction.mjs +23 -0
  133. package/src/tool/executor.mjs +26 -1
  134. package/src/tool/file-read-state.mjs +65 -0
  135. package/src/tool/git-auto.mjs +526 -526
  136. package/src/tool/git-full-auto.mjs +487 -478
  137. package/src/tool/mutation-guard.mjs +54 -0
  138. package/src/tool/prompt/edit.txt +3 -3
  139. package/src/tool/prompt/multiedit.txt +1 -0
  140. package/src/tool/prompt/notebookedit.txt +2 -1
  141. package/src/tool/prompt/patch.txt +25 -24
  142. package/src/tool/prompt/read.txt +3 -3
  143. package/src/tool/prompt/sysinfo.txt +29 -0
  144. package/src/tool/prompt/task.txt +66 -4
  145. package/src/tool/prompt/write.txt +2 -2
  146. package/src/tool/question-prompt.mjs +99 -93
  147. package/src/tool/registry.mjs +1701 -1343
  148. package/src/tool/task-tool.mjs +14 -6
  149. package/src/ui/activity-renderer.mjs +667 -664
  150. package/src/ui/repl-background-panel.mjs +7 -0
  151. package/src/ui/repl-capability-panel.mjs +9 -0
  152. package/src/ui/repl-dashboard.mjs +54 -4
  153. package/src/ui/repl-help.mjs +110 -0
  154. package/src/ui/repl-operator-panel.mjs +12 -0
  155. package/src/ui/repl-route-feedback.mjs +35 -0
  156. package/src/ui/repl-status-view.mjs +76 -0
  157. package/src/ui/repl-task-panel.mjs +5 -0
  158. package/src/ui/repl-transcript-panel.mjs +56 -0
  159. package/src/ui/repl-turn-summary.mjs +135 -0
  160. package/src/usage/pricing.mjs +122 -121
  161. package/src/usage/usage-meter.mjs +1 -0
  162. package/src/util/git.mjs +562 -519
  163. package/src/util/template.mjs +6 -1
@@ -1,273 +1,308 @@
1
- import { readFile, access } from "node:fs/promises"
2
- import { execSync } from "node:child_process"
3
- import path from "node:path"
4
- import { fileURLToPath } from "node:url"
5
- import { createHash } from "node:crypto"
6
- import { loadSessionPrompt } from "./prompt-loader.mjs"
7
- import { getAgentPrompt, listAgents } from "../agent/agent.mjs"
8
- import { loadAutoMemory } from "./memory-loader.mjs"
9
-
10
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
11
- const TOOL_PROMPT_DIR = path.join(__dirname, "..", "tool", "prompt")
12
-
13
- const toolPromptCache = new Map()
14
-
15
- // Session-level block cache: avoids rebuilding identical blocks across turns
16
- // Key = hash of inputs, Value = { blocks, text, timestamp }
17
- let blockCache = { key: null, result: null }
18
-
19
- function hashInputs(obj) {
20
- return createHash("md5").update(JSON.stringify(obj)).digest("hex")
21
- }
22
-
23
- async function loadToolPrompt(name) {
24
- if (!toolPromptCache.has(name)) {
25
- try {
26
- const file = path.join(TOOL_PROMPT_DIR, `${name}.txt`)
27
- const text = (await readFile(file, "utf8")).trim()
28
- toolPromptCache.set(name, text)
29
- } catch {
30
- toolPromptCache.set(name, "")
31
- }
32
- }
33
- return toolPromptCache.get(name)
34
- }
35
-
36
- // Detect if cwd is a git repo
37
- function detectGitRepo(cwd) {
38
- try {
39
- execSync("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe", timeout: 3000 })
40
- return true
41
- } catch {
42
- return false
43
- }
44
- }
45
-
46
- // Detect the user's default shell
47
- function detectShell() {
48
- if (process.platform === "win32") {
49
- // On Windows, kkcode uses bash (git bash / WSL) internally
50
- return "bash (use Unix shell syntax, not Windows — e.g., /dev/null not NUL, forward slashes in paths)"
51
- }
52
- const shell = process.env.SHELL || "/bin/bash"
53
- return path.basename(shell)
54
- }
55
-
56
- // Layer 1: Environment information (dynamic per turn — changes with cwd/date)
57
- export function environmentPrompt({ model, cwd }) {
58
- const isGit = detectGitRepo(cwd)
59
- const shell = detectShell()
60
- const today = new Date().toISOString().slice(0, 10)
61
- const lines = [
62
- `<env>`,
63
- ` model: ${model}`,
64
- ` cwd: ${cwd}`,
65
- ` platform: ${process.platform}`,
66
- ` shell: ${shell}`,
67
- ` node: ${process.version}`,
68
- ` date: ${today}`,
69
- ` git_repo: ${isGit}`,
70
- `</env>`,
71
- ``,
72
- `Knowledge cutoff: early 2025. Current date: ${today}.`,
73
- `When searching for recent information, use the current year (${today.slice(0, 4)}) in queries.`
74
- ]
75
- return lines.join("\n")
76
- }
77
-
78
- // Layer 2: System prompt (model-specific — stable across session)
79
- export async function providerPromptByModel(model) {
80
- const m = String(model).toLowerCase()
81
- if (m.includes("claude")) return loadSessionPrompt("anthropic.txt")
82
- if (m.includes("gpt-5") || m.includes("codex")) return loadSessionPrompt("beast.txt")
83
- if (m.includes("gpt") || m.includes("o1") || m.includes("o3")) return loadSessionPrompt("beast.txt")
84
- if (m.includes("gemini")) return loadSessionPrompt("qwen.txt")
85
- if (m.includes("deepseek")) return loadSessionPrompt("qwen.txt")
86
- if (m.includes("qwen")) return loadSessionPrompt("qwen.txt")
87
- return loadSessionPrompt("qwen.txt")
88
- }
89
-
90
- // Layer 3: Agent-specific prompt (stable across session)
91
- export async function agentPrompt(agent) {
92
- if (!agent) return ""
93
- return getAgentPrompt(agent.name)
94
- }
95
-
96
- // Layer 4: Mode reminder (stable within mode)
97
- export async function modeReminder(mode) {
98
- if (mode === "plan") return loadSessionPrompt("plan.txt")
99
- if (mode === "agent") return loadSessionPrompt("agent.txt")
100
- return ""
101
- }
102
-
103
- // Layer 5: Tool descriptions (stable across session — ideal cache target)
104
- export async function toolDescriptions(tools) {
105
- if (!tools || !tools.length) return ""
106
- const descriptions = []
107
- for (const tool of tools) {
108
- const prompt = await loadToolPrompt(tool.name)
109
- if (prompt) {
110
- descriptions.push(`## ${tool.name}\n${prompt}`)
111
- }
112
- }
113
- if (!descriptions.length) return ""
114
- return `# Available Tools\n\n${descriptions.join("\n\n")}`
115
- }
116
-
117
- // Layer 6: User custom instructions (loaded externally via instruction-loader.mjs and rules)
118
- // Assembled in loop.mjs from loadInstructions() and renderRulesPrompt()
119
-
120
- /**
121
- * Build system prompt as structured blocks for provider-level cache optimization.
122
- *
123
- * Returns { text, blocks } where:
124
- * - text: single concatenated string (for providers that don't support block-level caching)
125
- * - blocks: array of { label, text, cacheable } objects
126
- *
127
- * Cache strategy:
128
- * - Blocks marked cacheable=true are stable across turns (provider/agent/tools/skills)
129
- * - Blocks marked cacheable=false are dynamic per turn (env/user instructions)
130
- * - Providers use this to place cache_control breakpoints optimally
131
- *
132
- * Anthropic: up to 4 cache breakpoints — place on stable blocks
133
- * OpenAI: automatic prefix caching — stable blocks should come first
134
- */
135
- export async function buildSystemPromptBlocks({ mode, model, cwd, agent = null, tools = [], skills = [], userInstructions = "", projectContext = "", language = "en" }) {
136
- // Cache key: hash of all inputs that affect block content
137
- const cacheKey = hashInputs({
138
- mode, model, cwd, language,
139
- agent: agent?.name || null,
140
- tools: tools.map(t => t.name).sort(),
141
- skills: skills.map(s => s.name).sort(),
142
- userInstructions: userInstructions.slice(0, 200) // first 200 chars as fingerprint
143
- })
144
-
145
- if (blockCache.key === cacheKey && blockCache.result) {
146
- // Only env block changes per turn — rebuild just that
147
- const cached = blockCache.result
148
- const envIdx = cached.blocks.findIndex(b => b.label === "env")
149
- if (envIdx >= 0) {
150
- const freshEnv = environmentPrompt({ model, cwd })
151
- if (cached.blocks[envIdx].text === freshEnv) {
152
- return cached // fully identical
153
- }
154
- // Clone and update only the env block
155
- const updatedBlocks = cached.blocks.map((b, i) =>
156
- i === envIdx ? { ...b, text: freshEnv } : b
157
- )
158
- const text = updatedBlocks.map(b => b.text).join("\n\n")
159
- const result = { text, blocks: updatedBlocks }
160
- blockCache = { key: cacheKey, result }
161
- return result
162
- }
163
- }
164
-
165
- const blocks = []
166
-
167
- // Block 0: Provider prompt (stable — loaded once per model)
168
- const providerText = await providerPromptByModel(model)
169
- if (providerText) {
170
- blocks.push({ label: "provider", text: providerText, cacheable: true })
171
- }
172
-
173
- // Block 1: Agent prompt (stable — loaded once per agent)
174
- const agentText = agent ? await getAgentPrompt(agent.name) : ""
175
- if (agentText) {
176
- blocks.push({ label: "agent", text: agentText, cacheable: true })
177
- }
178
-
179
- // Block 2: Mode reminder (stable within mode)
180
- const modeText = await modeReminder(mode)
181
- if (modeText) {
182
- blocks.push({ label: "mode", text: modeText, cacheable: true })
183
- }
184
-
185
- // Block 3: Tool descriptions (stable changes only when tools change)
186
- const toolText = await toolDescriptions(tools)
187
- if (toolText) {
188
- blocks.push({ label: "tools", text: toolText, cacheable: true })
189
- }
190
-
191
- // Block 3.5: Large output strategy (stable — always included)
192
- const outputStrategyLines = [
193
- "# Large Output Strategy",
194
- "",
195
- "When generating large amounts of content:",
196
- "- For large file creation, write no more than 200 lines per tool call; use append mode for subsequent chunks",
197
- "- For partial file edits, use patch with line ranges instead of rewriting the whole file",
198
- "- If a task requires more than 300 lines of code, proactively split into multiple sequential tool calls",
199
- "- Never attempt to write an entire large file in a single tool call"
200
- ]
201
- blocks.push({ label: "output_strategy", text: outputStrategyLines.join("\n"), cacheable: true })
202
-
203
- // Block 4: Skills descriptions (stable changes only when skills change)
204
- if (skills.length) {
205
- const skillLines = skills.map((s) => `- /${s.name}: ${s.description || s.name}`).join("\n")
206
- const skillText = `# Available Skills\n\nInvoke with /<skill-name> [arguments].\n\n${skillLines}`
207
- blocks.push({ label: "skills", text: skillText, cacheable: true })
208
- }
209
-
210
- // Block 4.5: Available sub-agents (stable — changes only when custom agents change)
211
- const allAgents = listAgents({ includeHidden: false })
212
- const customSubagents = allAgents.filter((a) => a.mode === "subagent" && a._customAgent)
213
- if (customSubagents.length) {
214
- const agentLines = customSubagents.map((a) => {
215
- const perms = a.permission === "readonly" ? " (read-only)" : a.permission === "full" ? " (full access)" : ""
216
- return `- ${a.name}: ${a.description}${perms}`
217
- })
218
- const subagentText = [
219
- "# Available Sub-agents",
220
- "",
221
- "Delegate specialized work to these sub-agents using the `task` tool with `subagent_type` parameter.",
222
- "Use sub-agents when a task is self-contained and would benefit from a specialist, or to save context window space.",
223
- "",
224
- ...agentLines
225
- ].join("\n")
226
- blocks.push({ label: "subagents", text: subagentText, cacheable: true })
227
- }
228
-
229
- // Block 4.7: Project context (semi-stable changes when cwd changes)
230
- if (projectContext) {
231
- blocks.push({ label: "project", text: projectContext, cacheable: false })
232
- }
233
-
234
- // Block 4.9: Language constraint (stable — changes only when config changes)
235
- if (language && language !== "en") {
236
- const langMap = {
237
- zh: "Always respond in Chinese (中文). Use Chinese for all explanations, comments, and communications. Technical terms, code identifiers, and code content should remain in their original form (typically English)."
238
- }
239
- const langText = langMap[language]
240
- if (langText) {
241
- blocks.push({ label: "language", text: `# Language\n\n${langText}`, cacheable: true })
242
- }
243
- }
244
-
245
- // Block 4.95: Auto Memory (semi-stable — changes when user updates memory files)
246
- const memoryText = await loadAutoMemory(cwd)
247
- if (memoryText) {
248
- blocks.push({ label: "memory", text: memoryText, cacheable: false })
249
- }
250
-
251
- // Block 5: Environment (dynamic per turn)
252
- const envText = environmentPrompt({ model, cwd })
253
- blocks.push({ label: "env", text: envText, cacheable: false })
254
-
255
- // Block 6: User instructions + rules (semi-stable — cacheable if unchanged between turns)
256
- if (userInstructions) {
257
- blocks.push({ label: "user", text: userInstructions, cacheable: false })
258
- }
259
-
260
- const text = blocks.map((b) => b.text).join("\n\n")
261
- const result = { text, blocks }
262
- blockCache = { key: cacheKey, result }
263
- return result
264
- }
265
-
266
- // Legacy flat assembly (kept for backward compatibility)
267
- export async function buildSystemPromptLayers({ mode, model, cwd, agent = null }) {
268
- const layer1 = environmentPrompt({ model, cwd })
269
- const layer2 = await providerPromptByModel(model)
270
- const layer3 = await agentPrompt(agent)
271
- const layer4 = await modeReminder(mode)
272
- return { layer1, layer2, layer3, layer4 }
273
- }
1
+ import { readFile, access } from "node:fs/promises"
2
+ import { execSync } from "node:child_process"
3
+ import path from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+ import { createHash } from "node:crypto"
6
+ import { loadSessionPrompt } from "./prompt-loader.mjs"
7
+ import { renderPublicModeContract } from "./engine.mjs"
8
+ import { getAgentPrompt, listAgents } from "../agent/agent.mjs"
9
+ import { loadAutoMemory } from "./memory-loader.mjs"
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
12
+ const TOOL_PROMPT_DIR = path.join(__dirname, "..", "tool", "prompt")
13
+
14
+ const toolPromptCache = new Map()
15
+
16
+ // Session-level block cache: avoids rebuilding identical blocks across turns
17
+ // Key = hash of inputs, Value = { blocks, text, timestamp }
18
+ let blockCache = { key: null, result: null }
19
+
20
+ function hashInputs(obj) {
21
+ return createHash("md5").update(JSON.stringify(obj)).digest("hex")
22
+ }
23
+
24
+ async function loadToolPrompt(name) {
25
+ if (!toolPromptCache.has(name)) {
26
+ try {
27
+ const file = path.join(TOOL_PROMPT_DIR, `${name}.txt`)
28
+ const text = (await readFile(file, "utf8")).trim()
29
+ toolPromptCache.set(name, text)
30
+ } catch {
31
+ toolPromptCache.set(name, "")
32
+ }
33
+ }
34
+ return toolPromptCache.get(name)
35
+ }
36
+
37
+ // Detect if cwd is a git repo
38
+ function detectGitRepo(cwd) {
39
+ try {
40
+ execSync("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe", timeout: 3000 })
41
+ return true
42
+ } catch {
43
+ return false
44
+ }
45
+ }
46
+
47
+ // Detect the user's default shell
48
+ function detectShell() {
49
+ if (process.platform === "win32") {
50
+ // On Windows, kkcode uses bash (git bash / WSL) internally
51
+ return "bash (use Unix shell syntax, not Windows — e.g., /dev/null not NUL, forward slashes in paths)"
52
+ }
53
+ const shell = process.env.SHELL || "/bin/bash"
54
+ return path.basename(shell)
55
+ }
56
+
57
+ // Layer 1: Environment information (dynamic per turn — changes with cwd/date)
58
+ export function environmentPrompt({ model, cwd }) {
59
+ const isGit = detectGitRepo(cwd)
60
+ const shell = detectShell()
61
+ const today = new Date().toISOString().slice(0, 10)
62
+ const lines = [
63
+ `<env>`,
64
+ ` model: ${model}`,
65
+ ` cwd: ${cwd}`,
66
+ ` platform: ${process.platform}`,
67
+ ` shell: ${shell}`,
68
+ ` node: ${process.version}`,
69
+ ` date: ${today}`,
70
+ ` git_repo: ${isGit}`,
71
+ `</env>`,
72
+ ``,
73
+ `Knowledge cutoff: early 2025. Current date: ${today}.`,
74
+ `When searching for recent information, use the current year (${today.slice(0, 4)}) in queries.`
75
+ ]
76
+ return lines.join("\n")
77
+ }
78
+
79
+ // Layer 2: System prompt (model-specific — stable across session)
80
+ export async function providerPromptByModel(model) {
81
+ const m = String(model).toLowerCase()
82
+ if (m.includes("claude")) return loadSessionPrompt("anthropic.txt")
83
+ if (m.includes("gpt-5") || m.includes("codex")) return loadSessionPrompt("beast.txt")
84
+ if (m.includes("gpt") || m.includes("o1") || m.includes("o3")) return loadSessionPrompt("beast.txt")
85
+ if (m.includes("gemini")) return loadSessionPrompt("qwen.txt")
86
+ if (m.includes("deepseek")) return loadSessionPrompt("qwen.txt")
87
+ if (m.includes("qwen")) return loadSessionPrompt("qwen.txt")
88
+ return loadSessionPrompt("qwen.txt")
89
+ }
90
+
91
+ // Layer 3: Agent-specific prompt (stable across session)
92
+ export async function agentPrompt(agent) {
93
+ if (!agent) return ""
94
+ return getAgentPrompt(agent.name)
95
+ }
96
+
97
+ // Layer 4: Mode reminder (stable within mode)
98
+ export async function modeReminder(mode) {
99
+ const contractBlock = renderPublicModeContract()
100
+ if (mode === "assistant") return `${contractBlock}\n\nAssistant mode active. Treat this as the default CLI personal assistant lane for bounded terminal-native work: local files, logs, system checks, web lookup, Git/GitHub assistance, notes, task organization, and lightweight automation. Escalate to agent/code for explicit coding mutations and to longagent for staged multi-file delivery.`
101
+ if (mode === "plan") return `${contractBlock}\n\n${await loadSessionPrompt("plan.txt")}`
102
+ if (mode === "agent") return `${contractBlock}\n\n${await loadSessionPrompt("agent.txt")}\n\nCoding lane active. Focus on inspect/patch/verify coding work, keep diffs small, and validate with the narrowest useful tests.`
103
+ if (mode === "longagent") {
104
+ return `${contractBlock}\n\nLongAgent mode active. Treat this as the heavyweight staged delivery lane for multi-file or system-level work. Keep explicit gates, ownership, and recovery behavior intact.`
105
+ }
106
+ return contractBlock
107
+ }
108
+
109
+ // Layer 5: Tool descriptions (stable across session — ideal cache target)
110
+ export async function toolDescriptions(tools) {
111
+ if (!tools || !tools.length) return ""
112
+ const descriptions = []
113
+ for (const tool of tools) {
114
+ const prompt = await loadToolPrompt(tool.name)
115
+ if (prompt) {
116
+ descriptions.push(`## ${tool.name}\n${prompt}`)
117
+ }
118
+ }
119
+ if (!descriptions.length) return ""
120
+ return `# Available Tools\n\n${descriptions.join("\n\n")}`
121
+ }
122
+
123
+ // Layer 6: User custom instructions (loaded externally via instruction-loader.mjs and rules)
124
+ // Assembled in loop.mjs from loadInstructions() and renderRulesPrompt()
125
+
126
+ /**
127
+ * Build system prompt as structured blocks for provider-level cache optimization.
128
+ *
129
+ * Returns { text, blocks } where:
130
+ * - text: single concatenated string (for providers that don't support block-level caching)
131
+ * - blocks: array of { label, text, cacheable } objects
132
+ *
133
+ * Cache strategy:
134
+ * - Blocks marked cacheable=true are stable across turns (provider/agent/tools/skills)
135
+ * - Blocks marked cacheable=false are dynamic per turn (env/user instructions)
136
+ * - Providers use this to place cache_control breakpoints optimally
137
+ *
138
+ * Anthropic: up to 4 cache breakpoints — place on stable blocks
139
+ * OpenAI: automatic prefix caching — stable blocks should come first
140
+ */
141
+ export async function buildSystemPromptBlocks({ mode, model, cwd, agent = null, tools = [], skills = [], userInstructions = "", projectContext = "", language = "en" }) {
142
+ // Cache key: hash of all inputs that affect block content
143
+ const cacheKey = hashInputs({
144
+ mode, model, cwd, language,
145
+ agent: agent?.name || null,
146
+ tools: tools.map(t => t.name).sort(),
147
+ skills: skills.map(s => s.name).sort(),
148
+ userInstructions: hashInputs({ ui: userInstructions }) // hash full string to avoid collisions
149
+ })
150
+
151
+ if (blockCache.key === cacheKey && blockCache.result) {
152
+ // Only env block changes per turn — rebuild just that
153
+ const cached = blockCache.result
154
+ const envIdx = cached.blocks.findIndex(b => b.label === "env")
155
+ if (envIdx >= 0) {
156
+ const freshEnv = environmentPrompt({ model, cwd })
157
+ if (cached.blocks[envIdx].text === freshEnv) {
158
+ return cached // fully identical
159
+ }
160
+ // Clone and update only the env block
161
+ const updatedBlocks = cached.blocks.map((b, i) =>
162
+ i === envIdx ? { ...b, text: freshEnv } : b
163
+ )
164
+ const text = updatedBlocks.map(b => b.text).join("\n\n")
165
+ const result = { text, blocks: updatedBlocks }
166
+ blockCache = { key: cacheKey, result }
167
+ return result
168
+ }
169
+ }
170
+
171
+ const blocks = []
172
+
173
+ // Block 0: Provider prompt (stable — loaded once per model)
174
+ const providerText = await providerPromptByModel(model)
175
+ if (providerText) {
176
+ blocks.push({ label: "provider", text: providerText, cacheable: true })
177
+ }
178
+
179
+ // Block 1: Agent prompt (stable loaded once per agent)
180
+ const agentText = agent ? await getAgentPrompt(agent.name) : ""
181
+ if (agentText) {
182
+ blocks.push({ label: "agent", text: agentText, cacheable: true })
183
+ }
184
+
185
+ // Block 2: Mode reminder (stable within mode)
186
+ const modeText = await modeReminder(mode)
187
+ if (modeText) {
188
+ blocks.push({ label: "mode", text: modeText, cacheable: true })
189
+ }
190
+
191
+ // Block 3: Tool descriptions (stable — changes only when tools change)
192
+ const toolText = await toolDescriptions(tools)
193
+ if (toolText) {
194
+ blocks.push({ label: "tools", text: toolText, cacheable: true })
195
+ }
196
+
197
+ // Block 3.5: Large output strategy (stable always included)
198
+ const outputStrategyLines = [
199
+ "# Large Output Strategy",
200
+ "",
201
+ "When generating large amounts of content:",
202
+ "- For large file creation, write no more than 200 lines per tool call; use append mode for subsequent chunks",
203
+ "- For partial file edits, use patch with line ranges instead of rewriting the whole file",
204
+ "- If a task requires more than 300 lines of code, proactively split into multiple sequential tool calls",
205
+ "- Never attempt to write an entire large file in a single tool call"
206
+ ]
207
+ blocks.push({ label: "output_strategy", text: outputStrategyLines.join("\n"), cacheable: true })
208
+
209
+ // Block 4: CLI assistant contract (stable — release-facing behavior boundary)
210
+ const assistantContractLines = [
211
+ "# CLI Assistant Contract",
212
+ "",
213
+ "Operate as a CLI-first personal assistant, not an IDE shell or GUI automation product.",
214
+ "",
215
+ "Prefer the lightest path that completes the next step well:",
216
+ "- answer directly for short questions",
217
+ "- treat assistant as the default lane for bounded terminal-native personal assistant work",
218
+ "- treat agent/code/coding as the dedicated lane for coding mutation, debugging, refactoring, and test repair",
219
+ "- handle small local inspect/run/summarize tasks without over-upgrading to heavyweight execution",
220
+ "- continue an interrupted local transaction when the follow-up still fits the same bounded scope",
221
+ "- reserve longagent-style behavior for structured multi-file or system-level delivery with explicit heavy evidence",
222
+ "",
223
+ "Current safe capability boundary:",
224
+ "- coding and patching",
225
+ "- local filesystem, config, and log inspection",
226
+ "- shell/task execution",
227
+ "- repo/release assistance",
228
+ "- web lookup/fetch",
229
+ "- bounded delegated sidecar work",
230
+ "",
231
+ "Do not imply unsupported product surfaces such as GUI desktop automation, IDE integration, marketplace installs, or remote bridge platforms."
232
+ ]
233
+ blocks.push({ label: "assistant_contract", text: assistantContractLines.join("\n"), cacheable: true })
234
+
235
+ // Block 4.5: Public mode contract (stable keeps assistant/plan/agent/longagent aligned)
236
+ blocks.push({ label: "mode_contract", text: renderPublicModeContract(), cacheable: true })
237
+
238
+ // Block 5: Skills descriptions (stable — changes only when skills change)
239
+ if (skills.length) {
240
+ const skillLines = skills.map((s) => `- /${s.name}: ${s.description || s.name}`).join("\n")
241
+ const skillText = `# Available Skills\n\nInvoke with /<skill-name> [arguments].\n\n${skillLines}`
242
+ blocks.push({ label: "skills", text: skillText, cacheable: true })
243
+ }
244
+
245
+ // Block 5.5: Available sub-agents (stable — changes only when custom agents change)
246
+ const allAgents = listAgents({ includeHidden: false })
247
+ const customSubagents = allAgents.filter((a) => a.mode === "subagent" && a._customAgent)
248
+ if (customSubagents.length) {
249
+ const agentLines = customSubagents.map((a) => {
250
+ const perms = a.permission === "readonly" ? " (read-only)" : a.permission === "full" ? " (full access)" : ""
251
+ return `- ${a.name}: ${a.description}${perms}`
252
+ })
253
+ const subagentText = [
254
+ "# Available Sub-agents",
255
+ "",
256
+ "Delegate specialized work to these sub-agents using the `task` tool with `subagent_type` parameter.",
257
+ "Use sub-agents when a task is self-contained and would benefit from a specialist, or to save context window space.",
258
+ "",
259
+ ...agentLines
260
+ ].join("\n")
261
+ blocks.push({ label: "subagents", text: subagentText, cacheable: true })
262
+ }
263
+
264
+ // Block 5.7: Project context (semi-stable — changes when cwd changes)
265
+ if (projectContext) {
266
+ blocks.push({ label: "project", text: projectContext, cacheable: false })
267
+ }
268
+
269
+ // Block 5.9: Language constraint (stable — changes only when config changes)
270
+ if (language && language !== "en") {
271
+ const langMap = {
272
+ zh: "Always respond in Chinese (中文). Use Chinese for all explanations, comments, and communications. Technical terms, code identifiers, and code content should remain in their original form (typically English)."
273
+ }
274
+ const langText = langMap[language]
275
+ if (langText) {
276
+ blocks.push({ label: "language", text: `# Language\n\n${langText}`, cacheable: true })
277
+ }
278
+ }
279
+
280
+ // Block 5.95: Auto Memory (semi-stable — changes when user updates memory files)
281
+ const memoryText = await loadAutoMemory(cwd)
282
+ if (memoryText) {
283
+ blocks.push({ label: "memory", text: memoryText, cacheable: false })
284
+ }
285
+
286
+ // Block 5: Environment (dynamic per turn)
287
+ const envText = environmentPrompt({ model, cwd })
288
+ blocks.push({ label: "env", text: envText, cacheable: false })
289
+
290
+ // Block 6: User instructions + rules (semi-stable — cacheable if unchanged between turns)
291
+ if (userInstructions) {
292
+ blocks.push({ label: "user", text: userInstructions, cacheable: false })
293
+ }
294
+
295
+ const text = blocks.map((b) => b.text).join("\n\n")
296
+ const result = { text, blocks }
297
+ blockCache = { key: cacheKey, result }
298
+ return result
299
+ }
300
+
301
+ // Legacy flat assembly (kept for backward compatibility)
302
+ export async function buildSystemPromptLayers({ mode, model, cwd, agent = null }) {
303
+ const layer1 = environmentPrompt({ model, cwd })
304
+ const layer2 = await providerPromptByModel(model)
305
+ const layer3 = await agentPrompt(agent)
306
+ const layer4 = await modeReminder(mode)
307
+ return { layer1, layer2, layer3, layer4 }
308
+ }