@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.
Files changed (166) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +474 -387
  3. package/package.json +50 -46
  4. package/src/agent/agent.mjs +228 -220
  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 +89 -89
  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/commands/update.mjs +32 -0
  29. package/src/config/defaults.mjs +289 -260
  30. package/src/config/import-config.mjs +1 -1
  31. package/src/config/load-config.mjs +61 -4
  32. package/src/config/schema.mjs +604 -574
  33. package/src/context.mjs +4 -1
  34. package/src/core/constants.mjs +97 -91
  35. package/src/core/types.mjs +1 -1
  36. package/src/github/api.mjs +78 -78
  37. package/src/github/auth.mjs +294 -286
  38. package/src/github/flow.mjs +298 -298
  39. package/src/github/workspace.mjs +225 -212
  40. package/src/index.mjs +87 -82
  41. package/src/knowledge/frontend-aesthetics.txt +38 -38
  42. package/src/mcp/client-http.mjs +139 -141
  43. package/src/mcp/client-sse.mjs +297 -288
  44. package/src/mcp/client-stdio.mjs +534 -533
  45. package/src/mcp/constants.mjs +4 -2
  46. package/src/mcp/registry.mjs +498 -479
  47. package/src/mcp/stdio-framing.mjs +135 -133
  48. package/src/mcp/tool-result.mjs +24 -24
  49. package/src/observability/edit-diagnostics.mjs +449 -0
  50. package/src/observability/index.mjs +42 -42
  51. package/src/observability/metrics.mjs +165 -137
  52. package/src/observability/tracer.mjs +137 -137
  53. package/src/onboarding.mjs +209 -0
  54. package/src/orchestration/background-manager.mjs +567 -372
  55. package/src/orchestration/background-worker.mjs +419 -305
  56. package/src/orchestration/interruption-reason.mjs +21 -0
  57. package/src/orchestration/longagent-manager.mjs +197 -171
  58. package/src/orchestration/stage-scheduler.mjs +733 -728
  59. package/src/orchestration/subagent-router.mjs +7 -1
  60. package/src/orchestration/task-scheduler.mjs +219 -7
  61. package/src/permission/engine.mjs +1 -1
  62. package/src/permission/exec-policy.mjs +370 -370
  63. package/src/permission/file-edit-policy.mjs +108 -0
  64. package/src/permission/prompt.mjs +1 -1
  65. package/src/permission/rules.mjs +116 -7
  66. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  67. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  68. package/src/plugin/hook-bus.mjs +19 -5
  69. package/src/plugin/manifest-loader.mjs +222 -0
  70. package/src/provider/anthropic.mjs +396 -390
  71. package/src/provider/ollama.mjs +7 -1
  72. package/src/provider/openai.mjs +382 -340
  73. package/src/provider/retry-policy.mjs +74 -68
  74. package/src/provider/router.mjs +242 -241
  75. package/src/provider/sse.mjs +104 -104
  76. package/src/provider/wizard.mjs +556 -0
  77. package/src/repl/capability-facade.mjs +30 -0
  78. package/src/repl/command-surface.mjs +23 -0
  79. package/src/repl/controller-entry.mjs +40 -0
  80. package/src/repl/core-shell.mjs +208 -0
  81. package/src/repl/dialog-router.mjs +87 -0
  82. package/src/repl/input-engine.mjs +76 -0
  83. package/src/repl/keymap.mjs +7 -0
  84. package/src/repl/operator-surface.mjs +15 -0
  85. package/src/repl/permission-flow.mjs +49 -0
  86. package/src/repl/runtime-facade.mjs +36 -0
  87. package/src/repl/slash-router.mjs +62 -0
  88. package/src/repl/state-store.mjs +29 -0
  89. package/src/repl/turn-controller.mjs +58 -0
  90. package/src/repl/verification.mjs +23 -0
  91. package/src/repl.mjs +3371 -2981
  92. package/src/rules/load-rules.mjs +3 -3
  93. package/src/runtime.mjs +1 -1
  94. package/src/session/agent-transaction.mjs +86 -0
  95. package/src/session/checkpoint.mjs +302 -302
  96. package/src/session/compaction.mjs +298 -298
  97. package/src/session/engine.mjs +417 -232
  98. package/src/session/longagent-4stage.mjs +467 -460
  99. package/src/session/longagent-hybrid.mjs +1344 -1097
  100. package/src/session/longagent-plan.mjs +376 -365
  101. package/src/session/longagent-project-memory.mjs +53 -53
  102. package/src/session/longagent-scaffold.mjs +291 -291
  103. package/src/session/longagent-task-bus.mjs +138 -54
  104. package/src/session/longagent-utils.mjs +828 -472
  105. package/src/session/longagent.mjs +911 -900
  106. package/src/session/loop.mjs +1005 -930
  107. package/src/session/prompt/agent.txt +25 -25
  108. package/src/session/prompt/anthropic.txt +150 -150
  109. package/src/session/prompt/beast.txt +1 -1
  110. package/src/session/prompt/plan.txt +31 -31
  111. package/src/session/prompt/qwen.txt +46 -46
  112. package/src/session/recovery.mjs +21 -0
  113. package/src/session/rollback.mjs +196 -195
  114. package/src/session/routing-observability.mjs +72 -0
  115. package/src/session/runtime-state.mjs +47 -0
  116. package/src/session/store.mjs +523 -519
  117. package/src/session/system-prompt.mjs +308 -273
  118. package/src/session/task-validator.mjs +267 -267
  119. package/src/session/usability-gates.mjs +2 -2
  120. package/src/skill/builtin/commit.mjs +64 -64
  121. package/src/skill/builtin/design.mjs +76 -76
  122. package/src/skill/generator.mjs +18 -2
  123. package/src/skill/registry.mjs +642 -390
  124. package/src/storage/audit-store.mjs +18 -11
  125. package/src/storage/event-log.mjs +7 -1
  126. package/src/storage/ghost-commit-store.mjs +243 -245
  127. package/src/storage/paths.mjs +17 -0
  128. package/src/theme/default-theme.mjs +1 -1
  129. package/src/theme/markdown.mjs +4 -0
  130. package/src/theme/schema.mjs +1 -1
  131. package/src/theme/status-bar.mjs +162 -158
  132. package/src/tool/audit-wrapper.mjs +18 -2
  133. package/src/tool/edit-transaction.mjs +23 -0
  134. package/src/tool/executor.mjs +26 -1
  135. package/src/tool/file-read-state.mjs +65 -0
  136. package/src/tool/git-auto.mjs +526 -526
  137. package/src/tool/git-full-auto.mjs +487 -478
  138. package/src/tool/mutation-guard.mjs +54 -0
  139. package/src/tool/prompt/edit.txt +3 -3
  140. package/src/tool/prompt/multiedit.txt +1 -0
  141. package/src/tool/prompt/notebookedit.txt +2 -1
  142. package/src/tool/prompt/patch.txt +25 -24
  143. package/src/tool/prompt/read.txt +3 -3
  144. package/src/tool/prompt/sysinfo.txt +29 -0
  145. package/src/tool/prompt/task.txt +66 -4
  146. package/src/tool/prompt/write.txt +2 -2
  147. package/src/tool/question-prompt.mjs +99 -93
  148. package/src/tool/registry.mjs +1701 -1343
  149. package/src/tool/task-tool.mjs +14 -6
  150. package/src/ui/activity-renderer.mjs +667 -664
  151. package/src/ui/repl-background-panel.mjs +7 -0
  152. package/src/ui/repl-capability-panel.mjs +9 -0
  153. package/src/ui/repl-dashboard.mjs +54 -4
  154. package/src/ui/repl-help.mjs +110 -0
  155. package/src/ui/repl-operator-panel.mjs +12 -0
  156. package/src/ui/repl-route-feedback.mjs +35 -0
  157. package/src/ui/repl-status-view.mjs +76 -0
  158. package/src/ui/repl-task-panel.mjs +5 -0
  159. package/src/ui/repl-transcript-panel.mjs +56 -0
  160. package/src/ui/repl-turn-summary.mjs +135 -0
  161. package/src/update/checker.mjs +184 -0
  162. package/src/usage/pricing.mjs +122 -121
  163. package/src/usage/usage-meter.mjs +1 -0
  164. package/src/util/git.mjs +562 -519
  165. package/src/util/template.mjs +6 -1
  166. package/src/version.mjs +3 -0
@@ -0,0 +1,108 @@
1
+ function globToRegex(pattern) {
2
+ let src = ""
3
+ let i = 0
4
+ while (i < pattern.length) {
5
+ const ch = pattern[i]
6
+ if (ch === "*" && pattern[i + 1] === "*") {
7
+ src += ".*"
8
+ i += 2
9
+ if (pattern[i] === "/") i++
10
+ } else if (ch === "*") {
11
+ src += "[^/]*"
12
+ i++
13
+ } else if (ch === "?") {
14
+ src += "[^/]"
15
+ i++
16
+ } else if (".+^${}()|[]\\".includes(ch)) {
17
+ src += `\\${ch}`
18
+ i++
19
+ } else {
20
+ src += ch
21
+ i++
22
+ }
23
+ }
24
+ return new RegExp(`^${src}$`, "i")
25
+ }
26
+
27
+ function normalizePath(value) {
28
+ return String(value || "")
29
+ .replace(/\\/g, "/")
30
+ .split("/")
31
+ .reduce((acc, segment) => {
32
+ if (!segment || segment === ".") return acc
33
+ if (segment === "..") {
34
+ acc.pop()
35
+ return acc
36
+ }
37
+ acc.push(segment)
38
+ return acc
39
+ }, [])
40
+ .join("/")
41
+ }
42
+
43
+ function matchGlob(pattern, value) {
44
+ return globToRegex(pattern).test(normalizePath(value))
45
+ }
46
+
47
+ export const DEFAULT_SENSITIVE_FILE_PATTERNS = [
48
+ "AGENTS.md",
49
+ "**/AGENTS.md",
50
+ "KKCODE.md",
51
+ "**/KKCODE.md",
52
+ ".kkcode/**",
53
+ "**/.kkcode/**",
54
+ "kkcode.config.yaml",
55
+ "**/kkcode.config.yaml",
56
+ ".mcp.json",
57
+ "**/.mcp.json",
58
+ ".env",
59
+ ".env.*",
60
+ "**/.env",
61
+ "**/.env.*",
62
+ ".github/workflows/**",
63
+ "**/.github/workflows/**"
64
+ ]
65
+
66
+ const SENSITIVE_EDIT_TOOLS = new Set([
67
+ "write",
68
+ "edit",
69
+ "patch",
70
+ "multiedit",
71
+ "notebookedit"
72
+ ])
73
+
74
+ function extractCandidatePaths(input) {
75
+ if (Array.isArray(input)) return input.flatMap(extractCandidatePaths)
76
+ if (typeof input !== "string") return []
77
+ return input
78
+ .split(",")
79
+ .map((part) => normalizePath(part.trim()))
80
+ .filter(Boolean)
81
+ }
82
+
83
+ export function getSensitiveFilePatterns(config = {}) {
84
+ const configured = config.tool?.sensitive_file_patterns
85
+ if (!configured) return [...DEFAULT_SENSITIVE_FILE_PATTERNS]
86
+ if (Array.isArray(configured)) return configured.filter((value) => typeof value === "string" && value.trim())
87
+ return [...DEFAULT_SENSITIVE_FILE_PATTERNS]
88
+ }
89
+
90
+ export function isSensitiveEditTool(toolName) {
91
+ return SENSITIVE_EDIT_TOOLS.has(String(toolName || ""))
92
+ }
93
+
94
+ export function isSensitiveEditPath(pathOrPaths, config = {}) {
95
+ const patterns = getSensitiveFilePatterns(config)
96
+ const candidates = extractCandidatePaths(pathOrPaths)
97
+ return candidates.some((candidate) => patterns.some((pattern) => matchGlob(pattern, candidate)))
98
+ }
99
+
100
+ export function getSensitiveEditPolicy(toolName, pathOrPaths, config = {}) {
101
+ if (!isSensitiveEditTool(toolName)) return null
102
+ if (!isSensitiveEditPath(pathOrPaths, config)) return null
103
+ return {
104
+ action: "ask",
105
+ source: "sensitive_path",
106
+ reason: "sensitive edit target requires explicit approval"
107
+ }
108
+ }
@@ -36,4 +36,4 @@ export async function askPermissionInteractive({ tool, sessionId, reason = "", d
36
36
  } finally {
37
37
  rl.close()
38
38
  }
39
- }
39
+ }
@@ -1,3 +1,84 @@
1
+ import { getSensitiveEditPolicy } from "./file-edit-policy.mjs"
2
+
3
+ export const PERMISSION_MODES = ["auto", "manual", "yolo"]
4
+ export const LEGACY_PERMISSION_POLICIES = ["ask", "allow", "deny"]
5
+
6
+ const AUTO_READONLY_TOOLS = new Set([
7
+ "list",
8
+ "read",
9
+ "glob",
10
+ "grep",
11
+ "codesearch",
12
+ "sysinfo",
13
+ "websearch",
14
+ "webfetch",
15
+ "background_output",
16
+ "task_list",
17
+ "task_get",
18
+ "task_output",
19
+ "todowrite",
20
+ "question",
21
+ "enter_plan",
22
+ "exit_plan"
23
+ ])
24
+
25
+ const AUTO_REVIEW_ASK_TOOLS = new Set([
26
+ "bash",
27
+ "write",
28
+ "edit",
29
+ "patch",
30
+ "multiedit",
31
+ "notebookedit",
32
+ "task",
33
+ "task_stop",
34
+ "background_cancel",
35
+ "skill"
36
+ ])
37
+
38
+ const TRUSTED_BASH_PATTERNS = [
39
+ /^(pwd|ls|cat|head|tail|wc|which|date|whoami|uname)\b/i,
40
+ /^(rg|grep|find)\b/i,
41
+ /^sed\s+-n\b/i,
42
+ /^git\s+(status|log|diff|show|branch|rev-parse)\b/i,
43
+ /^(node|npm|pnpm|yarn)\s+(--version|-v|version|root|list|ls)\b/i
44
+ ]
45
+
46
+ function normalizePermissionMode(permission = {}) {
47
+ const mode = String(permission.mode || "").toLowerCase()
48
+ if (PERMISSION_MODES.includes(mode)) return mode
49
+ const legacy = String(permission.default_policy || "").toLowerCase()
50
+ if (legacy === "auto" || legacy === "yolo") return legacy
51
+ return "manual"
52
+ }
53
+
54
+ function trustedBashCommand(command) {
55
+ const cmd = String(command || "").trim()
56
+ if (!cmd) return false
57
+ if (/[;&|<>`]/.test(cmd)) return false
58
+ return TRUSTED_BASH_PATTERNS.some((pattern) => pattern.test(cmd))
59
+ }
60
+
61
+ function autoAllowsTool({ tool, command = "" }) {
62
+ if (AUTO_READONLY_TOOLS.has(tool)) return true
63
+ if (tool === "bash") return trustedBashCommand(command)
64
+ if (AUTO_REVIEW_ASK_TOOLS.has(tool)) return false
65
+ return false
66
+ }
67
+
68
+ function applySensitiveEscalation(decision, { tool, pattern, config, mode }) {
69
+ if (mode === "yolo") return decision
70
+ const sensitivePolicy = getSensitiveEditPolicy(tool, pattern, config)
71
+ if (sensitivePolicy && decision.action === "allow") {
72
+ return {
73
+ action: sensitivePolicy.action,
74
+ source: sensitivePolicy.source,
75
+ rule: decision.rule || null,
76
+ mode
77
+ }
78
+ }
79
+ return decision
80
+ }
81
+
1
82
  /**
2
83
  * Glob-style pattern matching supporting:
3
84
  * * — any chars except /
@@ -31,9 +112,18 @@ function globToRegex(pattern) {
31
112
  return new RegExp(`^${src}$`, "i")
32
113
  }
33
114
 
115
+ function normalizePath(p) {
116
+ // Resolve ../ and ./ sequences to prevent traversal bypass
117
+ return p.replace(/\\/g, "/").split("/").reduce((acc, seg) => {
118
+ if (seg === "..") { acc.pop(); return acc }
119
+ if (seg !== "." && seg !== "") acc.push(seg)
120
+ return acc
121
+ }, []).join("/")
122
+ }
123
+
34
124
  function matchGlob(value, pattern) {
35
125
  if (!pattern || pattern === "*") return true
36
- const str = String(value || "")
126
+ const str = normalizePath(String(value || ""))
37
127
  const negate = pattern.startsWith("!")
38
128
  const pat = negate ? pattern.slice(1) : pattern
39
129
  const matched = globToRegex(pat).test(str)
@@ -99,22 +189,41 @@ export function matchRule(rule, input) {
99
189
 
100
190
  export function evaluatePermission({ config, tool, mode, pattern = "*", command = "", risk = 0 }) {
101
191
  const permission = config.permission || { default_policy: "ask", rules: [] }
192
+ const permissionMode = normalizePermissionMode(permission)
102
193
  const rules = Array.isArray(permission.rules) ? permission.rules : []
103
194
  for (const rule of rules) {
104
195
  if (matchRule(rule, { tool, mode, pattern, command, risk })) {
105
- return {
196
+ const matchedDecision = {
106
197
  action: rule.action,
107
198
  source: "rule",
108
- rule
199
+ rule,
200
+ mode: permissionMode
109
201
  }
202
+ return applySensitiveEscalation(matchedDecision, { tool, pattern, config, mode: permissionMode })
110
203
  }
111
204
  }
112
- return {
113
- action: permission.default_policy || "ask",
205
+
206
+ if (permissionMode === "yolo") {
207
+ return { action: "allow", source: "mode:yolo", rule: null, mode: permissionMode }
208
+ }
209
+
210
+ if (permissionMode === "auto") {
211
+ const action = autoAllowsTool({ tool, command, risk }) ? "allow" : "ask"
212
+ const decision = { action, source: "auto_review", rule: null, mode: permissionMode }
213
+ return applySensitiveEscalation(decision, { tool, pattern, config, mode: permissionMode })
214
+ }
215
+
216
+ const defaultPolicy = LEGACY_PERMISSION_POLICIES.includes(permission.default_policy)
217
+ ? permission.default_policy
218
+ : "ask"
219
+ const fallbackDecision = {
220
+ action: defaultPolicy,
114
221
  source: "default",
115
- rule: null
222
+ rule: null,
223
+ mode: permissionMode
116
224
  }
225
+ return applySensitiveEscalation(fallbackDecision, { tool, pattern, config, mode: permissionMode })
117
226
  }
118
227
 
119
228
  // Exported for testing
120
- export { matchGlob, matchPatterns, matchCommandPrefix }
229
+ export { matchGlob, matchPatterns, matchCommandPrefix, normalizePermissionMode, trustedBashCommand, autoAllowsTool }
@@ -18,7 +18,8 @@ export default {
18
18
  name: "post-edit-format",
19
19
  tool: {
20
20
  async after(payload) {
21
- const { toolName, args, cwd } = payload
21
+ const toolName = String(payload.toolName || payload.tool || "")
22
+ const { args, cwd } = payload
22
23
  if (!["edit", "write", "multiedit"].includes(toolName)) return payload
23
24
 
24
25
  // Collect affected files
@@ -1,61 +1,125 @@
1
- // Post-edit TypeScript type check hook
2
- // Runs `tsc --noEmit` after editing .ts/.tsx files to catch type errors early
1
+ // Post-edit diagnostics + observability hook
2
+ // Captures baseline diagnostics before mutation tools and appends a concise
3
+ // post-edit diagnostics delta plus mutation summary after mutation tools run.
3
4
 
4
- import { exec as execCb } from "node:child_process"
5
- import { promisify } from "node:util"
6
- import { access } from "node:fs/promises"
7
- import path from "node:path"
5
+ import {
6
+ buildEditDiagnosticsReport,
7
+ buildMutationObservability,
8
+ collectDiagnosticsSnapshot,
9
+ extractTouchedFiles,
10
+ isDiagnosticsEligibleFile,
11
+ isMutationTool
12
+ } from "../../observability/edit-diagnostics.mjs"
8
13
 
9
- const exec = promisify(execCb)
14
+ function normalizeToolName(payload = {}) {
15
+ return String(payload.toolName || payload.tool || "").trim()
16
+ }
17
+
18
+ function isCompletedResult(result) {
19
+ if (!result || typeof result !== "object") return true
20
+ return !result.status || result.status === "completed"
21
+ }
10
22
 
11
- async function fileExists(p) {
12
- try { await access(p); return true } catch { return false }
23
+ function appendFeedback(result, reportText) {
24
+ if (!reportText) return result
25
+ if (typeof result === "string") return `${result}\n${reportText}`.trim()
26
+ if (result && typeof result === "object") {
27
+ return {
28
+ ...result,
29
+ output: `${String(result.output || "")}\n${reportText}`.trim()
30
+ }
31
+ }
32
+ return result
33
+ }
34
+
35
+ function buildReportText({ observability, diagnostics }) {
36
+ const lines = []
37
+ if (observability?.changes?.length) {
38
+ lines.push("Mutation summary:")
39
+ lines.push(`- ${observability.summary}`)
40
+ }
41
+ if (diagnostics?.summary?.text) {
42
+ lines.push("Diagnostics:")
43
+ lines.push(`- ${diagnostics.summary.text}`)
44
+ for (const issue of (diagnostics.delta?.added || []).slice(0, 2)) {
45
+ lines.push(`- introduced ${issue.file || "unknown"} ${issue.code || ""} ${issue.message || ""}`.trim())
46
+ }
47
+ for (const issue of (diagnostics.delta?.resolved || []).slice(0, 2)) {
48
+ lines.push(`- resolved ${issue.file || "unknown"} ${issue.code || ""} ${issue.message || ""}`.trim())
49
+ }
50
+ }
51
+ return lines.join("\n")
13
52
  }
14
53
 
15
54
  export default {
16
55
  name: "post-edit-typecheck",
17
56
  tool: {
18
- async after(payload) {
19
- const { toolName, args, result, cwd } = payload
20
- if (!["edit", "write", "multiedit"].includes(toolName)) return payload
21
-
22
- // Determine affected files
23
- const files = []
24
- if (args?.path) files.push(args.path)
25
- if (args?.changes) {
26
- for (const c of args.changes) {
27
- if (c.path) files.push(c.path)
57
+ async before(payload) {
58
+ const toolName = normalizeToolName(payload)
59
+ if (!isMutationTool(toolName)) return payload
60
+
61
+ const cwd = payload.cwd || process.cwd()
62
+ const files = extractTouchedFiles({ args: payload.args }).filter(isDiagnosticsEligibleFile)
63
+ if (files.length === 0) return payload
64
+
65
+ const baseline = await collectDiagnosticsSnapshot({ cwd, files }).catch(() => null)
66
+ return {
67
+ ...payload,
68
+ _editObservability: {
69
+ files,
70
+ baseline
28
71
  }
29
72
  }
73
+ },
30
74
 
31
- // Only check if at least one TS/TSX file was edited
32
- const tsFiles = files.filter(f => /\.tsx?$/.test(f))
33
- if (tsFiles.length === 0) return payload
75
+ async after(payload) {
76
+ const toolName = normalizeToolName(payload)
77
+ if (!isMutationTool(toolName)) return payload
78
+ if (!isCompletedResult(payload.result)) return payload
79
+
80
+ const cwd = payload.cwd || process.cwd()
81
+ const metadata = payload.result && typeof payload.result === "object" && payload.result.metadata && typeof payload.result.metadata === "object"
82
+ ? { ...payload.result.metadata }
83
+ : {}
84
+ const files = (payload._editObservability?.files || extractTouchedFiles({ args: payload.args, metadata }))
85
+ .filter(isDiagnosticsEligibleFile)
34
86
 
35
- // Verify tsconfig.json exists in project
36
- const tsconfigPath = path.join(cwd || process.cwd(), "tsconfig.json")
37
- if (!(await fileExists(tsconfigPath))) return payload
87
+ const observability = buildMutationObservability(metadata)
88
+ let diagnostics = null
38
89
 
39
- try {
40
- await exec("npx tsc --noEmit --pretty 2>&1", {
41
- cwd: cwd || process.cwd(),
42
- timeout: 15000
90
+ if (files.length > 0) {
91
+ const current = await collectDiagnosticsSnapshot({ cwd, files }).catch(() => null)
92
+ diagnostics = buildEditDiagnosticsReport({
93
+ cwd,
94
+ files,
95
+ baseline: payload._editObservability?.baseline || {},
96
+ current: current || {},
97
+ reason: current ? "" : "snapshot_failed"
43
98
  })
44
- // No errors — silently pass through
45
- } catch (error) {
46
- const output = (error.stdout || error.stderr || "").trim()
47
- if (output) {
48
- // Append type check warnings to tool result
49
- const warning = `\n⚠ TypeScript type check found issues:\n${output.slice(0, 2000)}`
50
- if (typeof result === "string") {
51
- payload.result = result + warning
52
- } else if (result && typeof result === "object") {
53
- payload.result = { ...result, output: (result.output || "") + warning }
99
+ }
100
+
101
+ if (!observability.changes.length && !diagnostics) {
102
+ return payload
103
+ }
104
+
105
+ const reportText = buildReportText({ observability, diagnostics })
106
+ let nextResult = appendFeedback(payload.result, reportText)
107
+
108
+ if (nextResult && typeof nextResult === "object") {
109
+ nextResult = {
110
+ ...nextResult,
111
+ metadata: {
112
+ ...metadata,
113
+ observability,
114
+ ...(diagnostics ? { diagnostics } : {})
54
115
  }
55
116
  }
56
117
  }
57
118
 
58
- return payload
119
+ return {
120
+ ...payload,
121
+ result: nextResult
122
+ }
59
123
  }
60
124
  }
61
125
  }
@@ -1,6 +1,7 @@
1
1
  import path from "node:path"
2
2
  import { access, readdir } from "node:fs/promises"
3
3
  import { pathToFileURL, fileURLToPath } from "node:url"
4
+ import { userRootDir } from "../storage/paths.mjs"
4
5
 
5
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
6
7
 
@@ -17,7 +18,8 @@ const HOOK_EVENTS = [
17
18
  const state = {
18
19
  loaded: false,
19
20
  hooks: [],
20
- errors: []
21
+ errors: [],
22
+ warnedPluginAlias: false
21
23
  }
22
24
 
23
25
  function normalizeHook(mod, source) {
@@ -63,11 +65,23 @@ export async function initHookBus(cwd = process.cwd()) {
63
65
  if (state.loaded) return state
64
66
  // Built-in hooks ship with kkcode (lowest priority — user hooks can override)
65
67
  const builtinHooks = path.join(__dirname, "builtin-hooks")
66
- const userRoot = process.env.USERPROFILE || process.env.HOME || cwd
67
- const userHooks = path.join(userRoot, ".kkcode", "hooks")
68
+ const userHooks = path.join(userRootDir(), "hooks")
69
+ const projectPluginHooks = path.join(cwd, ".kkcode", "plugins")
68
70
  const projectHooks = path.join(cwd, ".kkcode", "hooks")
69
- // Load order: builtin → user → project (later hooks in chain take priority)
70
- const files = [...(await discover(builtinHooks)), ...(await discover(userHooks)), ...(await discover(projectHooks))]
71
+ // Load order: builtin → user → project plugin alias project hooks
72
+ // `.kkcode/plugins` remains a compatibility alias for hook scripts while
73
+ // `.kkcode/hooks` is the explicit project hook path.
74
+ const pluginAliasFiles = await discover(projectPluginHooks)
75
+ if (pluginAliasFiles.length && !state.warnedPluginAlias) {
76
+ state.errors.push("deprecated hook path: .kkcode/plugins is a compatibility alias for loose hook scripts; prefer .kkcode/hooks or a plugin.json package boundary")
77
+ state.warnedPluginAlias = true
78
+ }
79
+ const files = [
80
+ ...(await discover(builtinHooks)),
81
+ ...(await discover(userHooks)),
82
+ ...pluginAliasFiles,
83
+ ...(await discover(projectHooks))
84
+ ]
71
85
  for (const file of files) {
72
86
  const loaded = await loadModule(file)
73
87
  if (loaded.error) {