@kkelly-offical/kkcode 0.1.7 → 0.2.3-preview.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.
- package/LICENSE +674 -674
- package/README.md +474 -387
- package/package.json +50 -46
- package/src/agent/agent.mjs +228 -220
- package/src/agent/custom-agent-loader.mjs +6 -3
- package/src/agent/generator.mjs +2 -2
- package/src/agent/prompt/assistant.txt +12 -0
- package/src/agent/prompt/bug-hunter.txt +89 -89
- package/src/agent/prompt/frontend-designer.txt +58 -58
- package/src/agent/prompt/guide.txt +1 -1
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
- package/src/agent/prompt/longagent-coding-agent.txt +37 -37
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
- package/src/agent/prompt/longagent-preview-agent.txt +63 -63
- package/src/command/custom-commands.mjs +2 -2
- package/src/commands/agent.mjs +1 -1
- package/src/commands/background.mjs +145 -4
- package/src/commands/chat.mjs +117 -76
- package/src/commands/config.mjs +148 -1
- package/src/commands/doctor.mjs +30 -6
- package/src/commands/init.mjs +32 -6
- package/src/commands/longagent.mjs +117 -0
- package/src/commands/mcp.mjs +275 -43
- package/src/commands/permission.mjs +1 -1
- package/src/commands/session.mjs +195 -140
- package/src/commands/skill.mjs +63 -0
- package/src/commands/theme.mjs +1 -1
- package/src/commands/update.mjs +32 -0
- package/src/config/defaults.mjs +289 -260
- package/src/config/import-config.mjs +1 -1
- package/src/config/load-config.mjs +61 -4
- package/src/config/schema.mjs +604 -574
- package/src/context.mjs +4 -1
- package/src/core/constants.mjs +97 -91
- package/src/core/types.mjs +1 -1
- package/src/github/api.mjs +78 -78
- package/src/github/auth.mjs +294 -286
- package/src/github/flow.mjs +298 -298
- package/src/github/workspace.mjs +225 -212
- package/src/index.mjs +87 -82
- package/src/knowledge/frontend-aesthetics.txt +38 -38
- package/src/mcp/client-http.mjs +139 -141
- package/src/mcp/client-sse.mjs +297 -288
- package/src/mcp/client-stdio.mjs +534 -533
- package/src/mcp/constants.mjs +4 -2
- package/src/mcp/registry.mjs +498 -479
- package/src/mcp/stdio-framing.mjs +135 -133
- package/src/mcp/tool-result.mjs +24 -24
- package/src/observability/edit-diagnostics.mjs +449 -0
- package/src/observability/index.mjs +42 -42
- package/src/observability/metrics.mjs +165 -137
- package/src/observability/tracer.mjs +137 -137
- package/src/onboarding.mjs +209 -0
- package/src/orchestration/background-manager.mjs +567 -372
- package/src/orchestration/background-worker.mjs +419 -305
- package/src/orchestration/interruption-reason.mjs +21 -0
- package/src/orchestration/longagent-manager.mjs +197 -171
- package/src/orchestration/stage-scheduler.mjs +733 -728
- package/src/orchestration/subagent-router.mjs +7 -1
- package/src/orchestration/task-scheduler.mjs +219 -7
- package/src/permission/engine.mjs +1 -1
- package/src/permission/exec-policy.mjs +370 -370
- package/src/permission/file-edit-policy.mjs +108 -0
- package/src/permission/prompt.mjs +1 -1
- package/src/permission/rules.mjs +116 -7
- package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
- package/src/plugin/hook-bus.mjs +19 -5
- package/src/plugin/manifest-loader.mjs +222 -0
- package/src/provider/anthropic.mjs +396 -390
- package/src/provider/ollama.mjs +7 -1
- package/src/provider/openai.mjs +382 -340
- package/src/provider/retry-policy.mjs +74 -68
- package/src/provider/router.mjs +242 -241
- package/src/provider/sse.mjs +104 -104
- package/src/provider/wizard.mjs +556 -0
- package/src/repl/capability-facade.mjs +30 -0
- package/src/repl/command-surface.mjs +23 -0
- package/src/repl/controller-entry.mjs +40 -0
- package/src/repl/core-shell.mjs +208 -0
- package/src/repl/dialog-router.mjs +87 -0
- package/src/repl/input-engine.mjs +76 -0
- package/src/repl/keymap.mjs +7 -0
- package/src/repl/operator-surface.mjs +15 -0
- package/src/repl/permission-flow.mjs +49 -0
- package/src/repl/runtime-facade.mjs +36 -0
- package/src/repl/slash-router.mjs +62 -0
- package/src/repl/state-store.mjs +29 -0
- package/src/repl/turn-controller.mjs +58 -0
- package/src/repl/verification.mjs +23 -0
- package/src/repl.mjs +3371 -2981
- package/src/rules/load-rules.mjs +3 -3
- package/src/runtime.mjs +1 -1
- package/src/session/agent-transaction.mjs +86 -0
- package/src/session/checkpoint.mjs +302 -302
- package/src/session/compaction.mjs +298 -298
- package/src/session/engine.mjs +417 -232
- package/src/session/longagent-4stage.mjs +467 -460
- package/src/session/longagent-hybrid.mjs +1344 -1097
- package/src/session/longagent-plan.mjs +376 -365
- package/src/session/longagent-project-memory.mjs +53 -53
- package/src/session/longagent-scaffold.mjs +291 -291
- package/src/session/longagent-task-bus.mjs +138 -54
- package/src/session/longagent-utils.mjs +828 -472
- package/src/session/longagent.mjs +911 -900
- package/src/session/loop.mjs +1005 -930
- package/src/session/prompt/agent.txt +25 -25
- package/src/session/prompt/anthropic.txt +150 -150
- package/src/session/prompt/beast.txt +1 -1
- package/src/session/prompt/plan.txt +31 -31
- package/src/session/prompt/qwen.txt +46 -46
- package/src/session/recovery.mjs +21 -0
- package/src/session/rollback.mjs +196 -195
- package/src/session/routing-observability.mjs +72 -0
- package/src/session/runtime-state.mjs +47 -0
- package/src/session/store.mjs +523 -519
- package/src/session/system-prompt.mjs +308 -273
- package/src/session/task-validator.mjs +267 -267
- package/src/session/usability-gates.mjs +2 -2
- package/src/skill/builtin/commit.mjs +64 -64
- package/src/skill/builtin/design.mjs +76 -76
- package/src/skill/generator.mjs +18 -2
- package/src/skill/registry.mjs +642 -390
- package/src/storage/audit-store.mjs +18 -11
- package/src/storage/event-log.mjs +7 -1
- package/src/storage/ghost-commit-store.mjs +243 -245
- package/src/storage/paths.mjs +17 -0
- package/src/theme/default-theme.mjs +1 -1
- package/src/theme/markdown.mjs +4 -0
- package/src/theme/schema.mjs +1 -1
- package/src/theme/status-bar.mjs +162 -158
- package/src/tool/audit-wrapper.mjs +18 -2
- package/src/tool/edit-transaction.mjs +23 -0
- package/src/tool/executor.mjs +26 -1
- package/src/tool/file-read-state.mjs +65 -0
- package/src/tool/git-auto.mjs +526 -526
- package/src/tool/git-full-auto.mjs +487 -478
- package/src/tool/mutation-guard.mjs +54 -0
- package/src/tool/prompt/edit.txt +3 -3
- package/src/tool/prompt/multiedit.txt +1 -0
- package/src/tool/prompt/notebookedit.txt +2 -1
- package/src/tool/prompt/patch.txt +25 -24
- package/src/tool/prompt/read.txt +3 -3
- package/src/tool/prompt/sysinfo.txt +29 -0
- package/src/tool/prompt/task.txt +66 -4
- package/src/tool/prompt/write.txt +2 -2
- package/src/tool/question-prompt.mjs +99 -93
- package/src/tool/registry.mjs +1701 -1343
- package/src/tool/task-tool.mjs +14 -6
- package/src/ui/activity-renderer.mjs +667 -664
- package/src/ui/repl-background-panel.mjs +7 -0
- package/src/ui/repl-capability-panel.mjs +9 -0
- package/src/ui/repl-dashboard.mjs +54 -4
- package/src/ui/repl-help.mjs +110 -0
- package/src/ui/repl-operator-panel.mjs +12 -0
- package/src/ui/repl-route-feedback.mjs +35 -0
- package/src/ui/repl-status-view.mjs +76 -0
- package/src/ui/repl-task-panel.mjs +5 -0
- package/src/ui/repl-transcript-panel.mjs +56 -0
- package/src/ui/repl-turn-summary.mjs +135 -0
- package/src/update/checker.mjs +184 -0
- package/src/usage/pricing.mjs +122 -121
- package/src/usage/usage-meter.mjs +1 -0
- package/src/util/git.mjs +562 -519
- package/src/util/template.mjs +6 -1
- package/src/version.mjs +3 -0
|
@@ -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(
|
|
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()
|