@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,260 +1,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 { 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
- return ""
100
- }
101
-
102
- // Layer 5: Tool descriptions (stable across session — ideal cache target)
103
- export async function toolDescriptions(tools) {
104
- if (!tools || !tools.length) return ""
105
- const descriptions = []
106
- for (const tool of tools) {
107
- const prompt = await loadToolPrompt(tool.name)
108
- if (prompt) {
109
- descriptions.push(`## ${tool.name}\n${prompt}`)
110
- }
111
- }
112
- if (!descriptions.length) return ""
113
- return `# Available Tools\n\n${descriptions.join("\n\n")}`
114
- }
115
-
116
- // Layer 6: User custom instructions (loaded externally via instruction-loader.mjs and rules)
117
- // Assembled in loop.mjs from loadInstructions() and renderRulesPrompt()
118
-
119
- /**
120
- * Build system prompt as structured blocks for provider-level cache optimization.
121
- *
122
- * Returns { text, blocks } where:
123
- * - text: single concatenated string (for providers that don't support block-level caching)
124
- * - blocks: array of { label, text, cacheable } objects
125
- *
126
- * Cache strategy:
127
- * - Blocks marked cacheable=true are stable across turns (provider/agent/tools/skills)
128
- * - Blocks marked cacheable=false are dynamic per turn (env/user instructions)
129
- * - Providers use this to place cache_control breakpoints optimally
130
- *
131
- * Anthropic: up to 4 cache breakpoints — place on stable blocks
132
- * OpenAI: automatic prefix caching — stable blocks should come first
133
- */
134
- export async function buildSystemPromptBlocks({ mode, model, cwd, agent = null, tools = [], skills = [], userInstructions = "", projectContext = "", language = "en" }) {
135
- // Cache key: hash of all inputs that affect block content
136
- const cacheKey = hashInputs({
137
- mode, model, cwd, language,
138
- agent: agent?.name || null,
139
- tools: tools.map(t => t.name).sort(),
140
- skills: skills.map(s => s.name).sort(),
141
- userInstructions: userInstructions.slice(0, 200) // first 200 chars as fingerprint
142
- })
143
-
144
- if (blockCache.key === cacheKey && blockCache.result) {
145
- // Only env block changes per turn — rebuild just that
146
- const cached = blockCache.result
147
- const envIdx = cached.blocks.findIndex(b => b.label === "env")
148
- if (envIdx >= 0) {
149
- const freshEnv = environmentPrompt({ model, cwd })
150
- if (cached.blocks[envIdx].text === freshEnv) {
151
- return cached // fully identical
152
- }
153
- // Clone and update only the env block
154
- const updatedBlocks = cached.blocks.map((b, i) =>
155
- i === envIdx ? { ...b, text: freshEnv } : b
156
- )
157
- const text = updatedBlocks.map(b => b.text).join("\n\n")
158
- const result = { text, blocks: updatedBlocks }
159
- blockCache = { key: cacheKey, result }
160
- return result
161
- }
162
- }
163
-
164
- const blocks = []
165
-
166
- // Block 0: Provider prompt (stable — loaded once per model)
167
- const providerText = await providerPromptByModel(model)
168
- if (providerText) {
169
- blocks.push({ label: "provider", text: providerText, cacheable: true })
170
- }
171
-
172
- // Block 1: Agent prompt (stable — loaded once per agent)
173
- const agentText = agent ? await getAgentPrompt(agent.name) : ""
174
- if (agentText) {
175
- blocks.push({ label: "agent", text: agentText, cacheable: true })
176
- }
177
-
178
- // Block 2: Mode reminder (stable within mode)
179
- const modeText = await modeReminder(mode)
180
- if (modeText) {
181
- blocks.push({ label: "mode", text: modeText, cacheable: true })
182
- }
183
-
184
- // Block 3: Tool descriptions (stable — changes only when tools change)
185
- const toolText = await toolDescriptions(tools)
186
- if (toolText) {
187
- blocks.push({ label: "tools", text: toolText, cacheable: true })
188
- }
189
-
190
- // Block 4: Skills descriptions (stable — changes only when skills change)
191
- if (skills.length) {
192
- const skillLines = skills.map((s) => `- /${s.name}: ${s.description || s.name}`).join("\n")
193
- const skillText = `# Available Skills\n\nInvoke with /<skill-name> [arguments].\n\n${skillLines}`
194
- blocks.push({ label: "skills", text: skillText, cacheable: true })
195
- }
196
-
197
- // Block 4.5: Available sub-agents (stable changes only when custom agents change)
198
- const allAgents = listAgents({ includeHidden: false })
199
- const customSubagents = allAgents.filter((a) => a.mode === "subagent" && a._customAgent)
200
- if (customSubagents.length) {
201
- const agentLines = customSubagents.map((a) => {
202
- const perms = a.permission === "readonly" ? " (read-only)" : a.permission === "full" ? " (full access)" : ""
203
- return `- ${a.name}: ${a.description}${perms}`
204
- })
205
- const subagentText = [
206
- "# Available Sub-agents",
207
- "",
208
- "Delegate specialized work to these sub-agents using the `task` tool with `subagent_type` parameter.",
209
- "Use sub-agents when a task is self-contained and would benefit from a specialist, or to save context window space.",
210
- "",
211
- ...agentLines
212
- ].join("\n")
213
- blocks.push({ label: "subagents", text: subagentText, cacheable: true })
214
- }
215
-
216
- // Block 4.7: Project context (semi-stable — changes when cwd changes)
217
- if (projectContext) {
218
- blocks.push({ label: "project", text: projectContext, cacheable: false })
219
- }
220
-
221
- // Block 4.9: Language constraint (stable changes only when config changes)
222
- if (language && language !== "en") {
223
- const langMap = {
224
- 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)."
225
- }
226
- const langText = langMap[language]
227
- if (langText) {
228
- blocks.push({ label: "language", text: `# Language\n\n${langText}`, cacheable: true })
229
- }
230
- }
231
-
232
- // Block 4.95: Auto Memory (semi-stable — changes when user updates memory files)
233
- const memoryText = await loadAutoMemory(cwd)
234
- if (memoryText) {
235
- blocks.push({ label: "memory", text: memoryText, cacheable: false })
236
- }
237
-
238
- // Block 5: Environment (dynamic per turn)
239
- const envText = environmentPrompt({ model, cwd })
240
- blocks.push({ label: "env", text: envText, cacheable: false })
241
-
242
- // Block 6: User instructions + rules (semi-stable — cacheable if unchanged between turns)
243
- if (userInstructions) {
244
- blocks.push({ label: "user", text: userInstructions, cacheable: false })
245
- }
246
-
247
- const text = blocks.map((b) => b.text).join("\n\n")
248
- const result = { text, blocks }
249
- blockCache = { key: cacheKey, result }
250
- return result
251
- }
252
-
253
- // Legacy flat assembly (kept for backward compatibility)
254
- export async function buildSystemPromptLayers({ mode, model, cwd, agent = null }) {
255
- const layer1 = environmentPrompt({ model, cwd })
256
- const layer2 = await providerPromptByModel(model)
257
- const layer3 = await agentPrompt(agent)
258
- const layer4 = await modeReminder(mode)
259
- return { layer1, layer2, layer3, layer4 }
260
- }
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,9 +1,10 @@
1
- import { exec as execCb } from "node:child_process"
1
+ import { exec as execCb, execFile as execFileCb } from "node:child_process"
2
2
  import { promisify } from "node:util"
3
3
  import { access, readFile } from "node:fs/promises"
4
4
  import path from "node:path"
5
5
 
6
6
  const exec = promisify(execCb)
7
+ const execFile = promisify(execFileCb)
7
8
 
8
9
  async function fileExists(p) {
9
10
  try { await access(p); return true } catch { return false }
@@ -50,7 +51,7 @@ export class TaskValidator {
50
51
  const errors = []
51
52
  for (const file of jsFiles.slice(0, 20)) {
52
53
  try {
53
- await exec(`node --check "${file}"`, { cwd: this.cwd, timeout: 10000 })
54
+ await execFile("node", ["--check", file], { cwd: this.cwd, timeout: 10000 })
54
55
  } catch (error) {
55
56
  errors.push(`${file}: ${(error.stderr || error.message || "").trim()}`)
56
57
  }
@@ -101,7 +102,7 @@ export class TaskValidator {
101
102
  const errors = []
102
103
  for (const file of pyFiles.slice(0, 20)) {
103
104
  try {
104
- await exec(`python -m py_compile "${file}"`, { cwd: this.cwd, timeout: 10000 })
105
+ await execFile("python", ["-m", "py_compile", file], { cwd: this.cwd, timeout: 10000 })
105
106
  } catch (error) {
106
107
  errors.push(`${file}: ${(error.stderr || error.message || "").trim()}`)
107
108
  }
@@ -0,0 +1,76 @@
1
+ import { readFile } from "node:fs/promises"
2
+ import path from "node:path"
3
+
4
+ export const name = "design"
5
+ export const description = "Frontend design mode — generates polished, distinctive UI with strong aesthetics (usage: /design <task>)"
6
+
7
+ async function detectDesignContext(cwd) {
8
+ try {
9
+ const pkg = JSON.parse(await readFile(path.join(cwd, "package.json"), "utf8"))
10
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies }
11
+ const ctx = {}
12
+ // Framework
13
+ if (deps.next) ctx.framework = "next"
14
+ else if (deps.nuxt) ctx.framework = "nuxt"
15
+ else if (deps.vue) ctx.framework = "vue"
16
+ else if (deps.react) ctx.framework = "react"
17
+ else if (deps.svelte || deps["@sveltejs/kit"]) ctx.framework = "svelte"
18
+ // CSS
19
+ if (deps.tailwindcss) ctx.css = "tailwind"
20
+ else if (deps.unocss) ctx.css = "unocss"
21
+ else if (deps["styled-components"]) ctx.css = "styled-components"
22
+ // Component lib
23
+ if (deps.antd) ctx.lib = "antd"
24
+ else if (deps["element-plus"]) ctx.lib = "element-plus"
25
+ else if (deps["@mui/material"]) ctx.lib = "mui"
26
+ else if (deps["@chakra-ui/react"]) ctx.lib = "chakra-ui"
27
+ else if (deps["@mantine/core"]) ctx.lib = "mantine"
28
+ else if (deps["naive-ui"]) ctx.lib = "naive-ui"
29
+ return ctx
30
+ } catch { return {} }
31
+ }
32
+
33
+ const AESTHETICS_PROMPT = `<frontend_aesthetics>
34
+ You are in DESIGN MODE. Create polished, distinctive frontends — NOT generic AI output.
35
+
36
+ Typography: Avoid Inter/Roboto/Arial. Use distinctive fonts (Space Grotesk, Playfair Display, Satoshi, IBM Plex). Extreme weight contrast (200 vs 800), 3x+ size jumps.
37
+
38
+ Color: CSS variables for ALL colors. Dominant color + sharp accent. Draw from IDE themes (Nord, Catppuccin), cultural aesthetics. AVOID purple-gradient-on-white.
39
+
40
+ Motion: One high-impact staggered reveal per page. Micro-interactions on hover/focus/press. CSS transitions + animation-delay.
41
+
42
+ Layout: CSS Grid for pages, Flexbox for components. Generous whitespace. Consistent 4px spacing scale. Mobile-first.
43
+
44
+ Depth: Layered gradients, backdrop-filter glass, box-shadow elevation hierarchy.
45
+
46
+ NEVER: cookie-cutter card grids, generic hero sections, border-radius:9999px everywhere, gray wireframe text, no visual rhythm.
47
+ </frontend_aesthetics>`
48
+
49
+ export async function run(ctx) {
50
+ const task = (ctx.args || "").trim()
51
+ const cwd = ctx.cwd || process.cwd()
52
+ const design = await detectDesignContext(cwd)
53
+
54
+ const parts = [AESTHETICS_PROMPT, ""]
55
+
56
+ if (Object.keys(design).length) {
57
+ parts.push("## Project Design Context")
58
+ if (design.framework) parts.push(`- Framework: ${design.framework}`)
59
+ if (design.css) parts.push(`- CSS: ${design.css}`)
60
+ if (design.lib) parts.push(`- Component Library: ${design.lib}`)
61
+ parts.push("")
62
+ parts.push("Read the project's existing styles/theme before writing new code. Extend, don't replace.")
63
+ parts.push("")
64
+ }
65
+
66
+ if (task) {
67
+ parts.push(`## Task`)
68
+ parts.push(task)
69
+ parts.push("")
70
+ parts.push("Implement this with production-grade design quality. Make it look like a professional designer built it.")
71
+ } else {
72
+ parts.push("No task specified. Usage: /design <description of what to build>")
73
+ }
74
+
75
+ return parts.join("\n")
76
+ }