@kkelly-offical/kkcode 0.1.6 → 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 +19 -2
  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 +90 -0
  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 -2929
  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 +36 -14
  96. package/src/session/engine.mjs +417 -227
  97. package/src/session/longagent-4stage.mjs +467 -460
  98. package/src/session/longagent-hybrid.mjs +1344 -1081
  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 -884
  105. package/src/session/loop.mjs +1005 -905
  106. package/src/session/prompt/agent.txt +25 -0
  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 +28 -6
  110. package/src/session/prompt/qwen.txt +46 -46
  111. package/src/session/recovery.mjs +21 -0
  112. package/src/session/rollback.mjs +197 -0
  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 -510
  116. package/src/session/system-prompt.mjs +56 -8
  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 +17 -4
  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
@@ -4,6 +4,7 @@ import path from "node:path"
4
4
  import { fileURLToPath } from "node:url"
5
5
  import { createHash } from "node:crypto"
6
6
  import { loadSessionPrompt } from "./prompt-loader.mjs"
7
+ import { renderPublicModeContract } from "./engine.mjs"
7
8
  import { getAgentPrompt, listAgents } from "../agent/agent.mjs"
8
9
  import { loadAutoMemory } from "./memory-loader.mjs"
9
10
 
@@ -95,8 +96,14 @@ export async function agentPrompt(agent) {
95
96
 
96
97
  // Layer 4: Mode reminder (stable within mode)
97
98
  export async function modeReminder(mode) {
98
- if (mode === "plan") return loadSessionPrompt("plan.txt")
99
- return ""
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
100
107
  }
101
108
 
102
109
  // Layer 5: Tool descriptions (stable across session — ideal cache target)
@@ -138,7 +145,7 @@ export async function buildSystemPromptBlocks({ mode, model, cwd, agent = null,
138
145
  agent: agent?.name || null,
139
146
  tools: tools.map(t => t.name).sort(),
140
147
  skills: skills.map(s => s.name).sort(),
141
- userInstructions: userInstructions.slice(0, 200) // first 200 chars as fingerprint
148
+ userInstructions: hashInputs({ ui: userInstructions }) // hash full string to avoid collisions
142
149
  })
143
150
 
144
151
  if (blockCache.key === cacheKey && blockCache.result) {
@@ -187,14 +194,55 @@ export async function buildSystemPromptBlocks({ mode, model, cwd, agent = null,
187
194
  blocks.push({ label: "tools", text: toolText, cacheable: true })
188
195
  }
189
196
 
190
- // Block 4: Skills descriptions (stable — changes only when skills change)
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)
191
239
  if (skills.length) {
192
240
  const skillLines = skills.map((s) => `- /${s.name}: ${s.description || s.name}`).join("\n")
193
241
  const skillText = `# Available Skills\n\nInvoke with /<skill-name> [arguments].\n\n${skillLines}`
194
242
  blocks.push({ label: "skills", text: skillText, cacheable: true })
195
243
  }
196
244
 
197
- // Block 4.5: Available sub-agents (stable — changes only when custom agents change)
245
+ // Block 5.5: Available sub-agents (stable — changes only when custom agents change)
198
246
  const allAgents = listAgents({ includeHidden: false })
199
247
  const customSubagents = allAgents.filter((a) => a.mode === "subagent" && a._customAgent)
200
248
  if (customSubagents.length) {
@@ -213,12 +261,12 @@ export async function buildSystemPromptBlocks({ mode, model, cwd, agent = null,
213
261
  blocks.push({ label: "subagents", text: subagentText, cacheable: true })
214
262
  }
215
263
 
216
- // Block 4.7: Project context (semi-stable — changes when cwd changes)
264
+ // Block 5.7: Project context (semi-stable — changes when cwd changes)
217
265
  if (projectContext) {
218
266
  blocks.push({ label: "project", text: projectContext, cacheable: false })
219
267
  }
220
268
 
221
- // Block 4.9: Language constraint (stable — changes only when config changes)
269
+ // Block 5.9: Language constraint (stable — changes only when config changes)
222
270
  if (language && language !== "en") {
223
271
  const langMap = {
224
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)."
@@ -229,7 +277,7 @@ export async function buildSystemPromptBlocks({ mode, model, cwd, agent = null,
229
277
  }
230
278
  }
231
279
 
232
- // Block 4.95: Auto Memory (semi-stable — changes when user updates memory files)
280
+ // Block 5.95: Auto Memory (semi-stable — changes when user updates memory files)
233
281
  const memoryText = await loadAutoMemory(cwd)
234
282
  if (memoryText) {
235
283
  blocks.push({ label: "memory", text: memoryText, cacheable: false })
@@ -1,267 +1,267 @@
1
- import { exec as execCb, execFile as execFileCb } 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
- const execFile = promisify(execFileCb)
8
-
9
- async function fileExists(p) {
10
- try { await access(p); return true } catch { return false }
11
- }
12
-
13
- export class TaskValidator {
14
- constructor({ cwd, configState }) {
15
- this.cwd = cwd
16
- this.configState = configState
17
- }
18
-
19
- async checkTodoCompletion(todoState) {
20
- if (!todoState || !Array.isArray(todoState)) {
21
- return {
22
- passed: true,
23
- message: "No todo list found"
24
- }
25
- }
26
-
27
- const incomplete = todoState.filter(t => t.status !== "completed")
28
- if (incomplete.length === 0) {
29
- return {
30
- passed: true,
31
- message: "All todo items completed"
32
- }
33
- }
34
-
35
- const items = incomplete.map(t => `- ${t.content}`).join("\n")
36
- return {
37
- passed: false,
38
- message: `Incomplete todo items:\n${items}`
39
- }
40
- }
41
-
42
- async checkJavaScriptSyntax() {
43
- const jsFiles = await this.findFilesByExtension(["js", "mjs", "cjs"])
44
- if (jsFiles.length === 0) {
45
- return {
46
- passed: true,
47
- message: "No JavaScript files to check"
48
- }
49
- }
50
-
51
- const errors = []
52
- for (const file of jsFiles.slice(0, 20)) {
53
- try {
54
- await execFile("node", ["--check", file], { cwd: this.cwd, timeout: 10000 })
55
- } catch (error) {
56
- errors.push(`${file}: ${(error.stderr || error.message || "").trim()}`)
57
- }
58
- }
59
-
60
- return {
61
- passed: errors.length === 0,
62
- message: errors.length === 0 ? "JavaScript syntax check passed" : `JavaScript syntax errors:\n${errors.join("\n")}`
63
- }
64
- }
65
-
66
- async checkTypeScript() {
67
- const tsconfigPath = path.join(this.cwd, "tsconfig.json")
68
- if (!(await fileExists(tsconfigPath))) {
69
- return {
70
- passed: true,
71
- message: "No tsconfig.json found"
72
- }
73
- }
74
-
75
- try {
76
- await exec("npx tsc --noEmit", {
77
- cwd: this.cwd,
78
- timeout: 30000
79
- })
80
- return {
81
- passed: true,
82
- message: "TypeScript check passed"
83
- }
84
- } catch (error) {
85
- const output = (error.stdout || error.stderr || "").trim()
86
- return {
87
- passed: false,
88
- message: `TypeScript errors:\n${output.slice(0, 2000)}`
89
- }
90
- }
91
- }
92
-
93
- async checkPythonSyntax() {
94
- const pyFiles = await this.findFilesByExtension(["py"])
95
- if (pyFiles.length === 0) {
96
- return {
97
- passed: true,
98
- message: "No Python files to check"
99
- }
100
- }
101
-
102
- const errors = []
103
- for (const file of pyFiles.slice(0, 20)) {
104
- try {
105
- await execFile("python", ["-m", "py_compile", file], { cwd: this.cwd, timeout: 10000 })
106
- } catch (error) {
107
- errors.push(`${file}: ${(error.stderr || error.message || "").trim()}`)
108
- }
109
- }
110
-
111
- return {
112
- passed: errors.length === 0,
113
- message: errors.length === 0 ? "Python syntax check passed" : `Python syntax errors:\n${errors.join("\n")}`
114
- }
115
- }
116
-
117
- async runTests() {
118
- const packageJsonPath = path.join(this.cwd, "package.json")
119
- if (!(await fileExists(packageJsonPath))) {
120
- return {
121
- passed: true,
122
- message: "No package.json found"
123
- }
124
- }
125
-
126
- try {
127
- const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"))
128
- const hasTestScript = packageJson.scripts?.test
129
- if (!hasTestScript) {
130
- return {
131
- passed: true,
132
- message: "No test script found"
133
- }
134
- }
135
-
136
- await exec("npm test", {
137
- cwd: this.cwd,
138
- timeout: 120000
139
- })
140
- return {
141
- passed: true,
142
- message: "Tests passed"
143
- }
144
- } catch (error) {
145
- const output = (error.stdout || error.stderr || "").trim()
146
- return {
147
- passed: false,
148
- message: `Test failures:\n${output.slice(0, 2000)}`
149
- }
150
- }
151
- }
152
-
153
- async findFilesByExtension(extensions) {
154
- const files = []
155
- for (const ext of extensions) {
156
- try {
157
- const matches = await this.globPattern(`**/*.${ext}`)
158
- files.push(...matches)
159
- } catch {
160
- }
161
- }
162
- return files
163
- }
164
-
165
- async globPattern(pattern) {
166
- try {
167
- const { Glob } = await import("glob")
168
- const g = new Glob(pattern, {
169
- cwd: this.cwd,
170
- ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"]
171
- })
172
- const matches = []
173
- for await (const match of g) {
174
- matches.push(path.join(this.cwd, match))
175
- }
176
- return matches
177
- } catch {
178
- return []
179
- }
180
- }
181
-
182
- async checkBuild() {
183
- const packageJsonPath = path.join(this.cwd, "package.json")
184
- if (!(await fileExists(packageJsonPath))) {
185
- return { passed: true, message: "No package.json found", severity: "skip" }
186
- }
187
- try {
188
- const pkg = JSON.parse(await readFile(packageJsonPath, "utf8"))
189
- if (!pkg.scripts?.build) {
190
- return { passed: true, message: "No build script", severity: "skip" }
191
- }
192
- await exec("npm run build --silent", { cwd: this.cwd, timeout: 60000 })
193
- return { passed: true, message: "Build succeeded", severity: "pass" }
194
- } catch (error) {
195
- const output = (error.stdout || error.stderr || "").trim()
196
- return { passed: false, message: `Build failed:\n${output.slice(0, 1500)}`, severity: "critical" }
197
- }
198
- }
199
-
200
- async checkLint() {
201
- const packageJsonPath = path.join(this.cwd, "package.json")
202
- if (!(await fileExists(packageJsonPath))) {
203
- return { passed: true, message: "No package.json found", severity: "skip" }
204
- }
205
- try {
206
- const pkg = JSON.parse(await readFile(packageJsonPath, "utf8"))
207
- if (!pkg.scripts?.lint) {
208
- return { passed: true, message: "No lint script", severity: "skip" }
209
- }
210
- await exec("npm run lint --silent", { cwd: this.cwd, timeout: 30000 })
211
- return { passed: true, message: "Lint passed", severity: "pass" }
212
- } catch (error) {
213
- const output = (error.stdout || error.stderr || "").trim()
214
- return { passed: false, message: `Lint issues:\n${output.slice(0, 1500)}`, severity: "warning" }
215
- }
216
- }
217
-
218
- async validate({ todoState, level = "standard" }) {
219
- const results = []
220
-
221
- const todoResult = await this.checkTodoCompletion(todoState)
222
- results.push({ name: "Todo", ...todoResult, severity: todoResult.passed ? "pass" : "critical" })
223
-
224
- const jsResult = await this.checkJavaScriptSyntax()
225
- results.push({ name: "JS Syntax", ...jsResult, severity: jsResult.passed ? "pass" : "critical" })
226
-
227
- if (level !== "quick") {
228
- const tsResult = await this.checkTypeScript()
229
- results.push({ name: "TypeScript", ...tsResult, severity: tsResult.passed ? "pass" : "critical" })
230
-
231
- const buildResult = await this.checkBuild()
232
- results.push({ name: "Build", ...buildResult })
233
-
234
- const testResult = await this.runTests()
235
- results.push({ name: "Tests", ...testResult, severity: testResult.passed ? "pass" : "critical" })
236
- }
237
-
238
- if (level === "strict") {
239
- const lintResult = await this.checkLint()
240
- results.push({ name: "Lint", ...lintResult })
241
-
242
- const pyResult = await this.checkPythonSyntax()
243
- results.push({ name: "Python", ...pyResult, severity: pyResult.passed ? "pass" : "warning" })
244
- }
245
-
246
- const critical = results.filter(r => !r.passed && r.severity === "critical").length
247
- const warnings = results.filter(r => !r.passed && r.severity === "warning").length
248
- const verdict = critical > 0 ? "BLOCK" : warnings > 0 ? "WARNING" : "APPROVE"
249
- const allPassed = verdict !== "BLOCK"
250
-
251
- const lines = [
252
- "VERIFICATION REPORT",
253
- "===================",
254
- ...results.map(r => `${r.passed ? "PASS" : "FAIL"} ${r.name}: ${r.message}`),
255
- "",
256
- `VERDICT: ${verdict}`,
257
- `CRITICAL: ${critical} WARNING: ${warnings}`,
258
- allPassed ? "Ready to proceed." : "Must fix critical issues before proceeding."
259
- ]
260
-
261
- return { passed: allPassed, verdict, results, message: lines.join("\n") }
262
- }
263
- }
264
-
265
- export async function createValidator({ cwd, configState }) {
266
- return new TaskValidator({ cwd, configState })
267
- }
1
+ import { exec as execCb, execFile as execFileCb } 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
+ const execFile = promisify(execFileCb)
8
+
9
+ async function fileExists(p) {
10
+ try { await access(p); return true } catch { return false }
11
+ }
12
+
13
+ export class TaskValidator {
14
+ constructor({ cwd, configState }) {
15
+ this.cwd = cwd
16
+ this.configState = configState
17
+ }
18
+
19
+ async checkTodoCompletion(todoState) {
20
+ if (!todoState || !Array.isArray(todoState)) {
21
+ return {
22
+ passed: true,
23
+ message: "No todo list found"
24
+ }
25
+ }
26
+
27
+ const incomplete = todoState.filter(t => t.status !== "completed")
28
+ if (incomplete.length === 0) {
29
+ return {
30
+ passed: true,
31
+ message: "All todo items completed"
32
+ }
33
+ }
34
+
35
+ const items = incomplete.map(t => `- ${t.content}`).join("\n")
36
+ return {
37
+ passed: false,
38
+ message: `Incomplete todo items:\n${items}`
39
+ }
40
+ }
41
+
42
+ async checkJavaScriptSyntax() {
43
+ const jsFiles = await this.findFilesByExtension(["js", "mjs", "cjs"])
44
+ if (jsFiles.length === 0) {
45
+ return {
46
+ passed: true,
47
+ message: "No JavaScript files to check"
48
+ }
49
+ }
50
+
51
+ const errors = []
52
+ for (const file of jsFiles.slice(0, 20)) {
53
+ try {
54
+ await execFile("node", ["--check", file], { cwd: this.cwd, timeout: 10000 })
55
+ } catch (error) {
56
+ errors.push(`${file}: ${(error.stderr || error.message || "").trim()}`)
57
+ }
58
+ }
59
+
60
+ return {
61
+ passed: errors.length === 0,
62
+ message: errors.length === 0 ? "JavaScript syntax check passed" : `JavaScript syntax errors:\n${errors.join("\n")}`
63
+ }
64
+ }
65
+
66
+ async checkTypeScript() {
67
+ const tsconfigPath = path.join(this.cwd, "tsconfig.json")
68
+ if (!(await fileExists(tsconfigPath))) {
69
+ return {
70
+ passed: true,
71
+ message: "No tsconfig.json found"
72
+ }
73
+ }
74
+
75
+ try {
76
+ await exec("npx tsc --noEmit", {
77
+ cwd: this.cwd,
78
+ timeout: 30000
79
+ })
80
+ return {
81
+ passed: true,
82
+ message: "TypeScript check passed"
83
+ }
84
+ } catch (error) {
85
+ const output = (error.stdout || error.stderr || "").trim()
86
+ return {
87
+ passed: false,
88
+ message: `TypeScript errors:\n${output.slice(0, 2000)}`
89
+ }
90
+ }
91
+ }
92
+
93
+ async checkPythonSyntax() {
94
+ const pyFiles = await this.findFilesByExtension(["py"])
95
+ if (pyFiles.length === 0) {
96
+ return {
97
+ passed: true,
98
+ message: "No Python files to check"
99
+ }
100
+ }
101
+
102
+ const errors = []
103
+ for (const file of pyFiles.slice(0, 20)) {
104
+ try {
105
+ await execFile("python", ["-m", "py_compile", file], { cwd: this.cwd, timeout: 10000 })
106
+ } catch (error) {
107
+ errors.push(`${file}: ${(error.stderr || error.message || "").trim()}`)
108
+ }
109
+ }
110
+
111
+ return {
112
+ passed: errors.length === 0,
113
+ message: errors.length === 0 ? "Python syntax check passed" : `Python syntax errors:\n${errors.join("\n")}`
114
+ }
115
+ }
116
+
117
+ async runTests() {
118
+ const packageJsonPath = path.join(this.cwd, "package.json")
119
+ if (!(await fileExists(packageJsonPath))) {
120
+ return {
121
+ passed: true,
122
+ message: "No package.json found"
123
+ }
124
+ }
125
+
126
+ try {
127
+ const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"))
128
+ const hasTestScript = packageJson.scripts?.test
129
+ if (!hasTestScript) {
130
+ return {
131
+ passed: true,
132
+ message: "No test script found"
133
+ }
134
+ }
135
+
136
+ await exec("npm test", {
137
+ cwd: this.cwd,
138
+ timeout: 120000
139
+ })
140
+ return {
141
+ passed: true,
142
+ message: "Tests passed"
143
+ }
144
+ } catch (error) {
145
+ const output = (error.stdout || error.stderr || "").trim()
146
+ return {
147
+ passed: false,
148
+ message: `Test failures:\n${output.slice(0, 2000)}`
149
+ }
150
+ }
151
+ }
152
+
153
+ async findFilesByExtension(extensions) {
154
+ const files = []
155
+ for (const ext of extensions) {
156
+ try {
157
+ const matches = await this.globPattern(`**/*.${ext}`)
158
+ files.push(...matches)
159
+ } catch {
160
+ }
161
+ }
162
+ return files
163
+ }
164
+
165
+ async globPattern(pattern) {
166
+ try {
167
+ const { Glob } = await import("glob")
168
+ const g = new Glob(pattern, {
169
+ cwd: this.cwd,
170
+ ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"]
171
+ })
172
+ const matches = []
173
+ for await (const match of g) {
174
+ matches.push(path.join(this.cwd, match))
175
+ }
176
+ return matches
177
+ } catch {
178
+ return []
179
+ }
180
+ }
181
+
182
+ async checkBuild() {
183
+ const packageJsonPath = path.join(this.cwd, "package.json")
184
+ if (!(await fileExists(packageJsonPath))) {
185
+ return { passed: true, message: "No package.json found", severity: "skip" }
186
+ }
187
+ try {
188
+ const pkg = JSON.parse(await readFile(packageJsonPath, "utf8"))
189
+ if (!pkg.scripts?.build) {
190
+ return { passed: true, message: "No build script", severity: "skip" }
191
+ }
192
+ await exec("npm run build --silent", { cwd: this.cwd, timeout: 60000 })
193
+ return { passed: true, message: "Build succeeded", severity: "pass" }
194
+ } catch (error) {
195
+ const output = (error.stdout || error.stderr || "").trim()
196
+ return { passed: false, message: `Build failed:\n${output.slice(0, 1500)}`, severity: "critical" }
197
+ }
198
+ }
199
+
200
+ async checkLint() {
201
+ const packageJsonPath = path.join(this.cwd, "package.json")
202
+ if (!(await fileExists(packageJsonPath))) {
203
+ return { passed: true, message: "No package.json found", severity: "skip" }
204
+ }
205
+ try {
206
+ const pkg = JSON.parse(await readFile(packageJsonPath, "utf8"))
207
+ if (!pkg.scripts?.lint) {
208
+ return { passed: true, message: "No lint script", severity: "skip" }
209
+ }
210
+ await exec("npm run lint --silent", { cwd: this.cwd, timeout: 30000 })
211
+ return { passed: true, message: "Lint passed", severity: "pass" }
212
+ } catch (error) {
213
+ const output = (error.stdout || error.stderr || "").trim()
214
+ return { passed: false, message: `Lint issues:\n${output.slice(0, 1500)}`, severity: "warning" }
215
+ }
216
+ }
217
+
218
+ async validate({ todoState, level = "standard" }) {
219
+ const results = []
220
+
221
+ const todoResult = await this.checkTodoCompletion(todoState)
222
+ results.push({ name: "Todo", ...todoResult, severity: todoResult.passed ? "pass" : "critical" })
223
+
224
+ const jsResult = await this.checkJavaScriptSyntax()
225
+ results.push({ name: "JS Syntax", ...jsResult, severity: jsResult.passed ? "pass" : "critical" })
226
+
227
+ if (level !== "quick") {
228
+ const tsResult = await this.checkTypeScript()
229
+ results.push({ name: "TypeScript", ...tsResult, severity: tsResult.passed ? "pass" : "critical" })
230
+
231
+ const buildResult = await this.checkBuild()
232
+ results.push({ name: "Build", ...buildResult })
233
+
234
+ const testResult = await this.runTests()
235
+ results.push({ name: "Tests", ...testResult, severity: testResult.passed ? "pass" : "critical" })
236
+ }
237
+
238
+ if (level === "strict") {
239
+ const lintResult = await this.checkLint()
240
+ results.push({ name: "Lint", ...lintResult })
241
+
242
+ const pyResult = await this.checkPythonSyntax()
243
+ results.push({ name: "Python", ...pyResult, severity: pyResult.passed ? "pass" : "warning" })
244
+ }
245
+
246
+ const critical = results.filter(r => !r.passed && r.severity === "critical").length
247
+ const warnings = results.filter(r => !r.passed && r.severity === "warning").length
248
+ const verdict = critical > 0 ? "BLOCK" : warnings > 0 ? "WARNING" : "APPROVE"
249
+ const allPassed = verdict !== "BLOCK"
250
+
251
+ const lines = [
252
+ "VERIFICATION REPORT",
253
+ "===================",
254
+ ...results.map(r => `${r.passed ? "PASS" : "FAIL"} ${r.name}: ${r.message}`),
255
+ "",
256
+ `VERDICT: ${verdict}`,
257
+ `CRITICAL: ${critical} WARNING: ${warnings}`,
258
+ allPassed ? "Ready to proceed." : "Must fix critical issues before proceeding."
259
+ ]
260
+
261
+ return { passed: allPassed, verdict, results, message: lines.join("\n") }
262
+ }
263
+ }
264
+
265
+ export async function createValidator({ cwd, configState }) {
266
+ return new TaskValidator({ cwd, configState })
267
+ }
@@ -1,14 +1,14 @@
1
1
  import path from "node:path"
2
2
  import { access, readFile, writeFile, mkdir } from "node:fs/promises"
3
- import { homedir } from "node:os"
4
3
  import { spawn } from "node:child_process"
5
4
  import { readReviewState } from "../review/review-store.mjs"
6
5
  import { fsckSessionStore, getSession } from "./store.mjs"
7
6
  import { EventBus } from "../core/events.mjs"
8
7
  import { EVENT_TYPES } from "../core/constants.mjs"
8
+ import { userRootDir } from "../storage/paths.mjs"
9
9
 
10
10
  const DEFAULT_GATE_TIMEOUT_MS = 15 * 60 * 1000
11
- const GATE_PREFS_FILE = path.join(homedir(), ".kkcode", "gate-preferences.json")
11
+ const GATE_PREFS_FILE = path.join(userRootDir(), "gate-preferences.json")
12
12
 
13
13
  // --- Gate result cache (5-min TTL, only caches passing results) ---
14
14
  const gateCache = new Map()