@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.
- package/LICENSE +674 -674
- package/README.md +452 -387
- package/package.json +50 -46
- package/src/agent/agent.mjs +19 -2
- 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 +90 -0
- 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/config/defaults.mjs +280 -260
- package/src/config/import-config.mjs +1 -1
- package/src/config/load-config.mjs +61 -4
- package/src/config/schema.mjs +591 -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 +84 -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 +2 -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 +3368 -2929
- 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 +36 -14
- package/src/session/engine.mjs +417 -227
- package/src/session/longagent-4stage.mjs +467 -460
- package/src/session/longagent-hybrid.mjs +1344 -1081
- 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 -884
- package/src/session/loop.mjs +1005 -905
- package/src/session/prompt/agent.txt +25 -0
- package/src/session/prompt/anthropic.txt +150 -150
- package/src/session/prompt/beast.txt +1 -1
- package/src/session/prompt/plan.txt +28 -6
- package/src/session/prompt/qwen.txt +46 -46
- package/src/session/recovery.mjs +21 -0
- package/src/session/rollback.mjs +197 -0
- package/src/session/routing-observability.mjs +72 -0
- package/src/session/runtime-state.mjs +47 -0
- package/src/session/store.mjs +523 -510
- package/src/session/system-prompt.mjs +56 -8
- 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 +13 -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 +17 -4
- 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/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
|
@@ -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
|
-
|
|
99
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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()
|