@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.
- package/LICENSE +674 -0
- package/README.md +445 -0
- package/package.json +46 -0
- package/src/agent/agent.mjs +170 -0
- package/src/agent/custom-agent-loader.mjs +158 -0
- package/src/agent/generator.mjs +115 -0
- package/src/agent/prompt/architect.txt +36 -0
- package/src/agent/prompt/build-fixer.txt +71 -0
- package/src/agent/prompt/build.txt +101 -0
- package/src/agent/prompt/compaction.txt +12 -0
- package/src/agent/prompt/explore.txt +29 -0
- package/src/agent/prompt/guide.txt +40 -0
- package/src/agent/prompt/longagent.txt +178 -0
- package/src/agent/prompt/plan.txt +50 -0
- package/src/agent/prompt/researcher.txt +23 -0
- package/src/agent/prompt/reviewer.txt +44 -0
- package/src/agent/prompt/security-reviewer.txt +62 -0
- package/src/agent/prompt/tdd-guide.txt +84 -0
- package/src/agent/prompt/title.txt +8 -0
- package/src/command/custom-commands.mjs +57 -0
- package/src/commands/agent.mjs +71 -0
- package/src/commands/audit.mjs +77 -0
- package/src/commands/background.mjs +86 -0
- package/src/commands/chat.mjs +114 -0
- package/src/commands/command.mjs +41 -0
- package/src/commands/config.mjs +44 -0
- package/src/commands/doctor.mjs +148 -0
- package/src/commands/hook.mjs +29 -0
- package/src/commands/init.mjs +141 -0
- package/src/commands/longagent.mjs +100 -0
- package/src/commands/mcp.mjs +89 -0
- package/src/commands/permission.mjs +36 -0
- package/src/commands/prompt.mjs +42 -0
- package/src/commands/review.mjs +266 -0
- package/src/commands/rule.mjs +34 -0
- package/src/commands/session.mjs +235 -0
- package/src/commands/theme.mjs +98 -0
- package/src/commands/usage.mjs +91 -0
- package/src/config/defaults.mjs +195 -0
- package/src/config/import-config.mjs +76 -0
- package/src/config/load-config.mjs +76 -0
- package/src/config/schema.mjs +509 -0
- package/src/context.mjs +40 -0
- package/src/core/constants.mjs +46 -0
- package/src/core/errors.mjs +57 -0
- package/src/core/events.mjs +29 -0
- package/src/core/types.mjs +57 -0
- package/src/github/api.mjs +78 -0
- package/src/github/auth.mjs +286 -0
- package/src/github/flow.mjs +298 -0
- package/src/github/workspace.mjs +212 -0
- package/src/index.mjs +82 -0
- package/src/knowledge/api-design.txt +9 -0
- package/src/knowledge/cpp.txt +10 -0
- package/src/knowledge/docker.txt +10 -0
- package/src/knowledge/dotnet.txt +9 -0
- package/src/knowledge/electron.txt +10 -0
- package/src/knowledge/flutter.txt +10 -0
- package/src/knowledge/go.txt +9 -0
- package/src/knowledge/graphql.txt +10 -0
- package/src/knowledge/java.txt +9 -0
- package/src/knowledge/kotlin.txt +10 -0
- package/src/knowledge/loader.mjs +125 -0
- package/src/knowledge/next.txt +8 -0
- package/src/knowledge/node.txt +8 -0
- package/src/knowledge/nuxt.txt +9 -0
- package/src/knowledge/php.txt +10 -0
- package/src/knowledge/python.txt +10 -0
- package/src/knowledge/react-native.txt +10 -0
- package/src/knowledge/react.txt +9 -0
- package/src/knowledge/ruby.txt +11 -0
- package/src/knowledge/rust.txt +9 -0
- package/src/knowledge/svelte.txt +9 -0
- package/src/knowledge/swift.txt +10 -0
- package/src/knowledge/tailwind.txt +10 -0
- package/src/knowledge/testing.txt +8 -0
- package/src/knowledge/typescript.txt +8 -0
- package/src/knowledge/vue.txt +9 -0
- package/src/mcp/client-http.mjs +157 -0
- package/src/mcp/client-sse.mjs +286 -0
- package/src/mcp/client-stdio.mjs +451 -0
- package/src/mcp/registry.mjs +394 -0
- package/src/mcp/stdio-framing.mjs +127 -0
- package/src/orchestration/background-manager.mjs +358 -0
- package/src/orchestration/background-worker.mjs +245 -0
- package/src/orchestration/longagent-manager.mjs +116 -0
- package/src/orchestration/stage-scheduler.mjs +489 -0
- package/src/orchestration/subagent-router.mjs +62 -0
- package/src/orchestration/task-scheduler.mjs +74 -0
- package/src/permission/engine.mjs +92 -0
- package/src/permission/exec-policy.mjs +372 -0
- package/src/permission/prompt.mjs +39 -0
- package/src/permission/rules.mjs +120 -0
- package/src/permission/workspace-trust.mjs +44 -0
- package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
- package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
- package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
- package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
- package/src/plugin/hook-bus.mjs +154 -0
- package/src/provider/anthropic.mjs +389 -0
- package/src/provider/ollama.mjs +236 -0
- package/src/provider/openai-compatible.mjs +1 -0
- package/src/provider/openai.mjs +339 -0
- package/src/provider/retry-policy.mjs +68 -0
- package/src/provider/router.mjs +228 -0
- package/src/provider/sse.mjs +91 -0
- package/src/repl.mjs +2929 -0
- package/src/review/diff-parser.mjs +36 -0
- package/src/review/rejection-queue.mjs +62 -0
- package/src/review/review-store.mjs +21 -0
- package/src/review/risk-score.mjs +61 -0
- package/src/rules/load-rules.mjs +64 -0
- package/src/runtime.mjs +1 -0
- package/src/session/checkpoint.mjs +239 -0
- package/src/session/compaction.mjs +276 -0
- package/src/session/engine.mjs +225 -0
- package/src/session/instinct-manager.mjs +172 -0
- package/src/session/instruction-loader.mjs +25 -0
- package/src/session/longagent-plan.mjs +329 -0
- package/src/session/longagent-scaffold.mjs +100 -0
- package/src/session/longagent.mjs +1462 -0
- package/src/session/loop.mjs +905 -0
- package/src/session/memory-loader.mjs +75 -0
- package/src/session/project-context.mjs +367 -0
- package/src/session/prompt/anthropic.txt +151 -0
- package/src/session/prompt/beast.txt +37 -0
- package/src/session/prompt/max-steps.txt +6 -0
- package/src/session/prompt/plan.txt +9 -0
- package/src/session/prompt/qwen.txt +46 -0
- package/src/session/prompt-loader.mjs +18 -0
- package/src/session/recovery.mjs +52 -0
- package/src/session/store.mjs +503 -0
- package/src/session/system-prompt.mjs +260 -0
- package/src/session/task-validator.mjs +266 -0
- package/src/session/usability-gates.mjs +379 -0
- package/src/skill/builtin/backend-patterns.mjs +123 -0
- package/src/skill/builtin/commit.mjs +64 -0
- package/src/skill/builtin/debug.mjs +45 -0
- package/src/skill/builtin/frontend-patterns.mjs +120 -0
- package/src/skill/builtin/frontend.mjs +188 -0
- package/src/skill/builtin/init.mjs +220 -0
- package/src/skill/builtin/review.mjs +49 -0
- package/src/skill/builtin/security-checklist.mjs +80 -0
- package/src/skill/builtin/tdd.mjs +54 -0
- package/src/skill/generator.mjs +113 -0
- package/src/skill/registry.mjs +336 -0
- package/src/storage/audit-store.mjs +83 -0
- package/src/storage/event-log.mjs +82 -0
- package/src/storage/ghost-commit-store.mjs +235 -0
- package/src/storage/json-store.mjs +53 -0
- package/src/storage/paths.mjs +148 -0
- package/src/theme/color.mjs +64 -0
- package/src/theme/default-theme.mjs +29 -0
- package/src/theme/load-theme.mjs +71 -0
- package/src/theme/markdown.mjs +135 -0
- package/src/theme/schema.mjs +45 -0
- package/src/theme/status-bar.mjs +158 -0
- package/src/tool/audit-wrapper.mjs +38 -0
- package/src/tool/edit-transaction.mjs +126 -0
- package/src/tool/executor.mjs +109 -0
- package/src/tool/file-lock-manager.mjs +85 -0
- package/src/tool/git-auto.mjs +545 -0
- package/src/tool/git-full-auto.mjs +478 -0
- package/src/tool/image-util.mjs +276 -0
- package/src/tool/prompt/background_cancel.txt +1 -0
- package/src/tool/prompt/background_output.txt +1 -0
- package/src/tool/prompt/bash.txt +71 -0
- package/src/tool/prompt/codesearch.txt +18 -0
- package/src/tool/prompt/edit.txt +27 -0
- package/src/tool/prompt/enter_plan.txt +74 -0
- package/src/tool/prompt/exit_plan.txt +62 -0
- package/src/tool/prompt/glob.txt +33 -0
- package/src/tool/prompt/grep.txt +43 -0
- package/src/tool/prompt/list.txt +8 -0
- package/src/tool/prompt/multiedit.txt +20 -0
- package/src/tool/prompt/notebookedit.txt +21 -0
- package/src/tool/prompt/patch.txt +24 -0
- package/src/tool/prompt/question.txt +44 -0
- package/src/tool/prompt/read.txt +40 -0
- package/src/tool/prompt/task.txt +83 -0
- package/src/tool/prompt/todowrite.txt +117 -0
- package/src/tool/prompt/webfetch.txt +38 -0
- package/src/tool/prompt/websearch.txt +43 -0
- package/src/tool/prompt/write.txt +38 -0
- package/src/tool/prompt-loader.mjs +18 -0
- package/src/tool/question-prompt.mjs +86 -0
- package/src/tool/registry.mjs +1309 -0
- package/src/tool/task-tool.mjs +28 -0
- package/src/ui/activity-renderer.mjs +410 -0
- package/src/ui/repl-dashboard.mjs +357 -0
- package/src/usage/pricing.mjs +121 -0
- package/src/usage/usage-meter.mjs +113 -0
- package/src/util/git.mjs +496 -0
- package/src/util/template.mjs +10 -0
- package/src/util/yaml.mjs +100 -0
|
@@ -0,0 +1,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
|
+
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
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { exec as execCb } from "node:child_process"
|
|
2
|
+
import { promisify } from "node:util"
|
|
3
|
+
import { access, readFile } from "node:fs/promises"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
|
|
6
|
+
const exec = promisify(execCb)
|
|
7
|
+
|
|
8
|
+
async function fileExists(p) {
|
|
9
|
+
try { await access(p); return true } catch { return false }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class TaskValidator {
|
|
13
|
+
constructor({ cwd, configState }) {
|
|
14
|
+
this.cwd = cwd
|
|
15
|
+
this.configState = configState
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async checkTodoCompletion(todoState) {
|
|
19
|
+
if (!todoState || !Array.isArray(todoState)) {
|
|
20
|
+
return {
|
|
21
|
+
passed: true,
|
|
22
|
+
message: "No todo list found"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const incomplete = todoState.filter(t => t.status !== "completed")
|
|
27
|
+
if (incomplete.length === 0) {
|
|
28
|
+
return {
|
|
29
|
+
passed: true,
|
|
30
|
+
message: "All todo items completed"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const items = incomplete.map(t => `- ${t.content}`).join("\n")
|
|
35
|
+
return {
|
|
36
|
+
passed: false,
|
|
37
|
+
message: `Incomplete todo items:\n${items}`
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async checkJavaScriptSyntax() {
|
|
42
|
+
const jsFiles = await this.findFilesByExtension(["js", "mjs", "cjs"])
|
|
43
|
+
if (jsFiles.length === 0) {
|
|
44
|
+
return {
|
|
45
|
+
passed: true,
|
|
46
|
+
message: "No JavaScript files to check"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const errors = []
|
|
51
|
+
for (const file of jsFiles.slice(0, 20)) {
|
|
52
|
+
try {
|
|
53
|
+
await exec(`node --check "${file}"`, { cwd: this.cwd, timeout: 10000 })
|
|
54
|
+
} catch (error) {
|
|
55
|
+
errors.push(`${file}: ${(error.stderr || error.message || "").trim()}`)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
passed: errors.length === 0,
|
|
61
|
+
message: errors.length === 0 ? "JavaScript syntax check passed" : `JavaScript syntax errors:\n${errors.join("\n")}`
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async checkTypeScript() {
|
|
66
|
+
const tsconfigPath = path.join(this.cwd, "tsconfig.json")
|
|
67
|
+
if (!(await fileExists(tsconfigPath))) {
|
|
68
|
+
return {
|
|
69
|
+
passed: true,
|
|
70
|
+
message: "No tsconfig.json found"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await exec("npx tsc --noEmit", {
|
|
76
|
+
cwd: this.cwd,
|
|
77
|
+
timeout: 30000
|
|
78
|
+
})
|
|
79
|
+
return {
|
|
80
|
+
passed: true,
|
|
81
|
+
message: "TypeScript check passed"
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const output = (error.stdout || error.stderr || "").trim()
|
|
85
|
+
return {
|
|
86
|
+
passed: false,
|
|
87
|
+
message: `TypeScript errors:\n${output.slice(0, 2000)}`
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async checkPythonSyntax() {
|
|
93
|
+
const pyFiles = await this.findFilesByExtension(["py"])
|
|
94
|
+
if (pyFiles.length === 0) {
|
|
95
|
+
return {
|
|
96
|
+
passed: true,
|
|
97
|
+
message: "No Python files to check"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const errors = []
|
|
102
|
+
for (const file of pyFiles.slice(0, 20)) {
|
|
103
|
+
try {
|
|
104
|
+
await exec(`python -m py_compile "${file}"`, { cwd: this.cwd, timeout: 10000 })
|
|
105
|
+
} catch (error) {
|
|
106
|
+
errors.push(`${file}: ${(error.stderr || error.message || "").trim()}`)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
passed: errors.length === 0,
|
|
112
|
+
message: errors.length === 0 ? "Python syntax check passed" : `Python syntax errors:\n${errors.join("\n")}`
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async runTests() {
|
|
117
|
+
const packageJsonPath = path.join(this.cwd, "package.json")
|
|
118
|
+
if (!(await fileExists(packageJsonPath))) {
|
|
119
|
+
return {
|
|
120
|
+
passed: true,
|
|
121
|
+
message: "No package.json found"
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"))
|
|
127
|
+
const hasTestScript = packageJson.scripts?.test
|
|
128
|
+
if (!hasTestScript) {
|
|
129
|
+
return {
|
|
130
|
+
passed: true,
|
|
131
|
+
message: "No test script found"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await exec("npm test", {
|
|
136
|
+
cwd: this.cwd,
|
|
137
|
+
timeout: 120000
|
|
138
|
+
})
|
|
139
|
+
return {
|
|
140
|
+
passed: true,
|
|
141
|
+
message: "Tests passed"
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
const output = (error.stdout || error.stderr || "").trim()
|
|
145
|
+
return {
|
|
146
|
+
passed: false,
|
|
147
|
+
message: `Test failures:\n${output.slice(0, 2000)}`
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async findFilesByExtension(extensions) {
|
|
153
|
+
const files = []
|
|
154
|
+
for (const ext of extensions) {
|
|
155
|
+
try {
|
|
156
|
+
const matches = await this.globPattern(`**/*.${ext}`)
|
|
157
|
+
files.push(...matches)
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return files
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async globPattern(pattern) {
|
|
165
|
+
try {
|
|
166
|
+
const { Glob } = await import("glob")
|
|
167
|
+
const g = new Glob(pattern, {
|
|
168
|
+
cwd: this.cwd,
|
|
169
|
+
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"]
|
|
170
|
+
})
|
|
171
|
+
const matches = []
|
|
172
|
+
for await (const match of g) {
|
|
173
|
+
matches.push(path.join(this.cwd, match))
|
|
174
|
+
}
|
|
175
|
+
return matches
|
|
176
|
+
} catch {
|
|
177
|
+
return []
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async checkBuild() {
|
|
182
|
+
const packageJsonPath = path.join(this.cwd, "package.json")
|
|
183
|
+
if (!(await fileExists(packageJsonPath))) {
|
|
184
|
+
return { passed: true, message: "No package.json found", severity: "skip" }
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const pkg = JSON.parse(await readFile(packageJsonPath, "utf8"))
|
|
188
|
+
if (!pkg.scripts?.build) {
|
|
189
|
+
return { passed: true, message: "No build script", severity: "skip" }
|
|
190
|
+
}
|
|
191
|
+
await exec("npm run build --silent", { cwd: this.cwd, timeout: 60000 })
|
|
192
|
+
return { passed: true, message: "Build succeeded", severity: "pass" }
|
|
193
|
+
} catch (error) {
|
|
194
|
+
const output = (error.stdout || error.stderr || "").trim()
|
|
195
|
+
return { passed: false, message: `Build failed:\n${output.slice(0, 1500)}`, severity: "critical" }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async checkLint() {
|
|
200
|
+
const packageJsonPath = path.join(this.cwd, "package.json")
|
|
201
|
+
if (!(await fileExists(packageJsonPath))) {
|
|
202
|
+
return { passed: true, message: "No package.json found", severity: "skip" }
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const pkg = JSON.parse(await readFile(packageJsonPath, "utf8"))
|
|
206
|
+
if (!pkg.scripts?.lint) {
|
|
207
|
+
return { passed: true, message: "No lint script", severity: "skip" }
|
|
208
|
+
}
|
|
209
|
+
await exec("npm run lint --silent", { cwd: this.cwd, timeout: 30000 })
|
|
210
|
+
return { passed: true, message: "Lint passed", severity: "pass" }
|
|
211
|
+
} catch (error) {
|
|
212
|
+
const output = (error.stdout || error.stderr || "").trim()
|
|
213
|
+
return { passed: false, message: `Lint issues:\n${output.slice(0, 1500)}`, severity: "warning" }
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async validate({ todoState, level = "standard" }) {
|
|
218
|
+
const results = []
|
|
219
|
+
|
|
220
|
+
const todoResult = await this.checkTodoCompletion(todoState)
|
|
221
|
+
results.push({ name: "Todo", ...todoResult, severity: todoResult.passed ? "pass" : "critical" })
|
|
222
|
+
|
|
223
|
+
const jsResult = await this.checkJavaScriptSyntax()
|
|
224
|
+
results.push({ name: "JS Syntax", ...jsResult, severity: jsResult.passed ? "pass" : "critical" })
|
|
225
|
+
|
|
226
|
+
if (level !== "quick") {
|
|
227
|
+
const tsResult = await this.checkTypeScript()
|
|
228
|
+
results.push({ name: "TypeScript", ...tsResult, severity: tsResult.passed ? "pass" : "critical" })
|
|
229
|
+
|
|
230
|
+
const buildResult = await this.checkBuild()
|
|
231
|
+
results.push({ name: "Build", ...buildResult })
|
|
232
|
+
|
|
233
|
+
const testResult = await this.runTests()
|
|
234
|
+
results.push({ name: "Tests", ...testResult, severity: testResult.passed ? "pass" : "critical" })
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (level === "strict") {
|
|
238
|
+
const lintResult = await this.checkLint()
|
|
239
|
+
results.push({ name: "Lint", ...lintResult })
|
|
240
|
+
|
|
241
|
+
const pyResult = await this.checkPythonSyntax()
|
|
242
|
+
results.push({ name: "Python", ...pyResult, severity: pyResult.passed ? "pass" : "warning" })
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const critical = results.filter(r => !r.passed && r.severity === "critical").length
|
|
246
|
+
const warnings = results.filter(r => !r.passed && r.severity === "warning").length
|
|
247
|
+
const verdict = critical > 0 ? "BLOCK" : warnings > 0 ? "WARNING" : "APPROVE"
|
|
248
|
+
const allPassed = verdict !== "BLOCK"
|
|
249
|
+
|
|
250
|
+
const lines = [
|
|
251
|
+
"VERIFICATION REPORT",
|
|
252
|
+
"===================",
|
|
253
|
+
...results.map(r => `${r.passed ? "PASS" : "FAIL"} ${r.name}: ${r.message}`),
|
|
254
|
+
"",
|
|
255
|
+
`VERDICT: ${verdict}`,
|
|
256
|
+
`CRITICAL: ${critical} WARNING: ${warnings}`,
|
|
257
|
+
allPassed ? "Ready to proceed." : "Must fix critical issues before proceeding."
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
return { passed: allPassed, verdict, results, message: lines.join("\n") }
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function createValidator({ cwd, configState }) {
|
|
265
|
+
return new TaskValidator({ cwd, configState })
|
|
266
|
+
}
|