@karajan-family/code 4.21.0

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 (746) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +98 -0
  3. package/bin/karajan-mcp.js +2 -0
  4. package/bin/kj-rag-mcp.js +75 -0
  5. package/bin/kj-tail +361 -0
  6. package/bin/kj.js +2 -0
  7. package/docs/README.es.md +80 -0
  8. package/docs/karajan-code-logo-small.png +0 -0
  9. package/library/logical-clocks-lamport.md +40 -0
  10. package/package.json +140 -0
  11. package/packages/ai-trash/README.md +37 -0
  12. package/packages/ai-trash/bin/kj-trash.js +10 -0
  13. package/packages/ai-trash/package.json +43 -0
  14. package/packages/ai-trash/src/cli.js +200 -0
  15. package/packages/ai-trash/src/destructive-parser.js +74 -0
  16. package/packages/ai-trash/src/git-snapshot.js +128 -0
  17. package/packages/ai-trash/src/hook.js +137 -0
  18. package/packages/ai-trash/src/index.js +14 -0
  19. package/packages/ai-trash/src/install.js +73 -0
  20. package/packages/ai-trash/src/logger.js +60 -0
  21. package/packages/ai-trash/src/manifest.js +91 -0
  22. package/packages/ai-trash/src/permissions.js +26 -0
  23. package/packages/ai-trash/src/snapshot.js +66 -0
  24. package/packages/ai-trash/src/ulid.js +66 -0
  25. package/packages/hu-board/README.md +69 -0
  26. package/packages/hu-board/package.json +28 -0
  27. package/packages/hu-board/public/app.js +259 -0
  28. package/packages/hu-board/public/index.html +77 -0
  29. package/packages/hu-board/public/pipeline.css +132 -0
  30. package/packages/hu-board/public/pipeline.html +48 -0
  31. package/packages/hu-board/public/pipeline.js +200 -0
  32. package/packages/hu-board/public/rag.html +21 -0
  33. package/packages/hu-board/public/rag.js +39 -0
  34. package/packages/hu-board/public/styles.css +973 -0
  35. package/packages/hu-board/public/utils/api.js +139 -0
  36. package/packages/hu-board/public/utils/board-view.js +473 -0
  37. package/packages/hu-board/public/utils/command-launcher.js +213 -0
  38. package/packages/hu-board/public/utils/config-editor.js +181 -0
  39. package/packages/hu-board/public/utils/dashboard-view.js +103 -0
  40. package/packages/hu-board/public/utils/formatters.js +300 -0
  41. package/packages/hu-board/public/utils/graph-view.js +216 -0
  42. package/packages/hu-board/public/utils/hu-actions.js +224 -0
  43. package/packages/hu-board/public/utils/init-listeners.js +180 -0
  44. package/packages/hu-board/public/utils/log-panel.js +222 -0
  45. package/packages/hu-board/public/utils/modals.js +309 -0
  46. package/packages/hu-board/public/utils/plan-rollup-view.js +53 -0
  47. package/packages/hu-board/public/utils/preflight-view.js +89 -0
  48. package/packages/hu-board/public/utils/project-actions.js +143 -0
  49. package/packages/hu-board/public/utils/project-picker-view.js +139 -0
  50. package/packages/hu-board/public/utils/run-launcher.js +169 -0
  51. package/packages/hu-board/public/utils/server-push.js +252 -0
  52. package/packages/hu-board/public/utils/sessions-view.js +199 -0
  53. package/packages/hu-board/public/utils/story-detail-view.js +324 -0
  54. package/packages/hu-board/public/utils/story-edit-form.js +271 -0
  55. package/packages/hu-board/public/wiki.html +32 -0
  56. package/packages/hu-board/public/wiki.js +53 -0
  57. package/packages/hu-board/src/auth.js +92 -0
  58. package/packages/hu-board/src/boot-info.js +16 -0
  59. package/packages/hu-board/src/cleanup-zombies.js +145 -0
  60. package/packages/hu-board/src/command-runner.js +133 -0
  61. package/packages/hu-board/src/config-yaml.js +531 -0
  62. package/packages/hu-board/src/db.js +963 -0
  63. package/packages/hu-board/src/ephemeral-cleaner.js +196 -0
  64. package/packages/hu-board/src/event-bus.js +58 -0
  65. package/packages/hu-board/src/format.js +207 -0
  66. package/packages/hu-board/src/hu-zombie-reaper.js +103 -0
  67. package/packages/hu-board/src/journal-parser.js +180 -0
  68. package/packages/hu-board/src/plan-mutations.js +424 -0
  69. package/packages/hu-board/src/preflight.js +286 -0
  70. package/packages/hu-board/src/routes/api.js +1374 -0
  71. package/packages/hu-board/src/routes/pipeline.js +45 -0
  72. package/packages/hu-board/src/routes/rag.js +42 -0
  73. package/packages/hu-board/src/routes/wiki.js +43 -0
  74. package/packages/hu-board/src/run-tracker.js +121 -0
  75. package/packages/hu-board/src/server.js +438 -0
  76. package/packages/hu-board/src/sync.js +732 -0
  77. package/packages/hu-board/src/token-store.js +56 -0
  78. package/packages/hu-board/src/zombie-reaper.js +206 -0
  79. package/scripts/ai-attribution-guard.yml +84 -0
  80. package/scripts/audit-test-diet.mjs +151 -0
  81. package/scripts/build-sea.mjs +150 -0
  82. package/scripts/diet-baseline.mjs +170 -0
  83. package/scripts/dual-publish.mjs +67 -0
  84. package/scripts/esbuild-sea.config.mjs +273 -0
  85. package/scripts/git-hooks/post-merge +18 -0
  86. package/scripts/install-binary.ps1 +174 -0
  87. package/scripts/install-binary.sh +159 -0
  88. package/scripts/install-guard-all-repos.sh +85 -0
  89. package/scripts/install-kj.sh +193 -0
  90. package/scripts/install.js +901 -0
  91. package/scripts/install.sh +7 -0
  92. package/scripts/postinstall.js +117 -0
  93. package/scripts/protect-all-repos.sh +88 -0
  94. package/scripts/quickstart-gate.mjs +97 -0
  95. package/scripts/regen-arch-stats.sh +71 -0
  96. package/scripts/setup-multi-instance.sh +150 -0
  97. package/scripts/verify-pack.mjs +278 -0
  98. package/src/activity-log.js +59 -0
  99. package/src/agents/agy-agent.js +62 -0
  100. package/src/agents/aider-agent.js +78 -0
  101. package/src/agents/availability.js +52 -0
  102. package/src/agents/base-agent.js +134 -0
  103. package/src/agents/claude-agent.js +413 -0
  104. package/src/agents/codex-agent.js +112 -0
  105. package/src/agents/copilot-agent.js +65 -0
  106. package/src/agents/gemini-agent.js +85 -0
  107. package/src/agents/host-agent.js +38 -0
  108. package/src/agents/index.js +59 -0
  109. package/src/agents/kimi-agent.js +45 -0
  110. package/src/agents/model-registry.js +137 -0
  111. package/src/agents/opencode-agent.js +72 -0
  112. package/src/agents/qwen-agent.js +20 -0
  113. package/src/agents/resolve-bin.js +73 -0
  114. package/src/audit/agent-readiness.js +259 -0
  115. package/src/audit/ai-slop-findings.js +102 -0
  116. package/src/audit/audit-fallback.js +66 -0
  117. package/src/audit/audit-history-display.js +61 -0
  118. package/src/audit/audit-history.js +102 -0
  119. package/src/audit/basal-cost.js +281 -0
  120. package/src/audit/circular-deps.js +144 -0
  121. package/src/audit/dead-exports.js +186 -0
  122. package/src/audit/deterministic-summary.js +327 -0
  123. package/src/audit/harness-scorecard.js +100 -0
  124. package/src/audit/harness-section.js +81 -0
  125. package/src/audit/infra-findings.js +100 -0
  126. package/src/audit/injection-findings.js +114 -0
  127. package/src/audit/issue-filter.js +133 -0
  128. package/src/audit/osv-findings.js +167 -0
  129. package/src/audit/plan-adherence.js +224 -0
  130. package/src/audit/semgrep-findings.js +129 -0
  131. package/src/audit/sonar-findings.js +93 -0
  132. package/src/audit/webperf-input.js +133 -0
  133. package/src/autonomy/arbiter-role.js +71 -0
  134. package/src/autonomy/arbiter.js +22 -0
  135. package/src/autonomy/decider.js +18 -0
  136. package/src/autonomy/decision-resolver.js +58 -0
  137. package/src/autonomy/outcome-report.js +55 -0
  138. package/src/autonomy/policy.js +41 -0
  139. package/src/autonomy/stage-ask.js +30 -0
  140. package/src/bootstrap.js +276 -0
  141. package/src/brain/agent-error-classifier.js +116 -0
  142. package/src/brain/decision-tracker.js +125 -0
  143. package/src/brain/decisor.js +243 -0
  144. package/src/brain/solomon-consult.js +171 -0
  145. package/src/brain/standby-scheduler.js +8 -0
  146. package/src/brain/standby-store.js +12 -0
  147. package/src/brain/with-brain-recovery.js +235 -0
  148. package/src/budget/comparison.js +96 -0
  149. package/src/canvas/parse-md.js +238 -0
  150. package/src/canvas/schema.js +101 -0
  151. package/src/canvas/to-flat.js +85 -0
  152. package/src/canvas/validate.js +163 -0
  153. package/src/checks/ai-surface.js +79 -0
  154. package/src/checks/ai-trash.js +34 -0
  155. package/src/checks/binaries.js +207 -0
  156. package/src/checks/ci.js +105 -0
  157. package/src/checks/config-files.js +217 -0
  158. package/src/checks/dir-setup.js +103 -0
  159. package/src/checks/guarantee-level.js +38 -0
  160. package/src/checks/hardware.js +150 -0
  161. package/src/checks/harness-scorecard.js +41 -0
  162. package/src/checks/mcp-health.js +169 -0
  163. package/src/checks/method.js +73 -0
  164. package/src/checks/native-build.js +67 -0
  165. package/src/checks/node.js +70 -0
  166. package/src/checks/ollama.js +41 -0
  167. package/src/checks/orphans.js +77 -0
  168. package/src/checks/ports.js +188 -0
  169. package/src/checks/project-checks.js +245 -0
  170. package/src/checks/qmd.js +41 -0
  171. package/src/checks/rag-hooks.js +54 -0
  172. package/src/checks/release-check.js +102 -0
  173. package/src/checks/rtk.js +36 -0
  174. package/src/checks/runner.js +326 -0
  175. package/src/checks/skills.js +53 -0
  176. package/src/checks/sonar.js +67 -0
  177. package/src/checks/squeezr.js +36 -0
  178. package/src/checks/system.js +53 -0
  179. package/src/checks/tokens.js +172 -0
  180. package/src/checks/types.js +103 -0
  181. package/src/ci/dispatch.js +99 -0
  182. package/src/ci/index.js +3 -0
  183. package/src/ci/pr-diff.js +26 -0
  184. package/src/ci/repo.js +45 -0
  185. package/src/cli/_shared.js +30 -0
  186. package/src/cli/advanced-commands.js +61 -0
  187. package/src/cli/register-meta.js +772 -0
  188. package/src/cli/register-pipeline.js +335 -0
  189. package/src/cli/register-plan.js +224 -0
  190. package/src/cli/register-roles-skills.js +54 -0
  191. package/src/cli/register-sonar.js +28 -0
  192. package/src/cli/register-standby.js +26 -0
  193. package/src/cli.js +155 -0
  194. package/src/commands/advanced.js +31 -0
  195. package/src/commands/agent-run.js +63 -0
  196. package/src/commands/agents.js +105 -0
  197. package/src/commands/architect.js +123 -0
  198. package/src/commands/audit.js +430 -0
  199. package/src/commands/autorun.js +49 -0
  200. package/src/commands/board.js +458 -0
  201. package/src/commands/check.js +40 -0
  202. package/src/commands/clean.js +145 -0
  203. package/src/commands/code.js +45 -0
  204. package/src/commands/config.js +74 -0
  205. package/src/commands/discover.js +105 -0
  206. package/src/commands/doctor.js +251 -0
  207. package/src/commands/env.js +207 -0
  208. package/src/commands/harden.js +203 -0
  209. package/src/commands/hu.js +210 -0
  210. package/src/commands/identity.js +60 -0
  211. package/src/commands/init.js +950 -0
  212. package/src/commands/install-tools.js +432 -0
  213. package/src/commands/mutate.js +94 -0
  214. package/src/commands/ollama.js +44 -0
  215. package/src/commands/onboard.js +45 -0
  216. package/src/commands/plan/_shared.js +55 -0
  217. package/src/commands/plan/add-hu.js +23 -0
  218. package/src/commands/plan/delete.js +9 -0
  219. package/src/commands/plan/fix.js +99 -0
  220. package/src/commands/plan/generate.js +532 -0
  221. package/src/commands/plan/index.js +13 -0
  222. package/src/commands/plan/list.js +21 -0
  223. package/src/commands/plan/migrate-result.js +41 -0
  224. package/src/commands/plan/ready.js +46 -0
  225. package/src/commands/plan/remove-hu.js +17 -0
  226. package/src/commands/plan/share.js +85 -0
  227. package/src/commands/plan/show.js +81 -0
  228. package/src/commands/plan/unshare.js +41 -0
  229. package/src/commands/plan/validate.js +20 -0
  230. package/src/commands/plan.js +17 -0
  231. package/src/commands/policy.js +164 -0
  232. package/src/commands/privacy.js +45 -0
  233. package/src/commands/qmd.js +86 -0
  234. package/src/commands/rag-mcp.js +18 -0
  235. package/src/commands/rag.js +173 -0
  236. package/src/commands/report-issue.js +77 -0
  237. package/src/commands/report.js +469 -0
  238. package/src/commands/researcher.js +54 -0
  239. package/src/commands/resume.js +68 -0
  240. package/src/commands/review-gate.js +355 -0
  241. package/src/commands/review.js +89 -0
  242. package/src/commands/roles.js +116 -0
  243. package/src/commands/run.js +185 -0
  244. package/src/commands/scan.js +18 -0
  245. package/src/commands/skills.js +86 -0
  246. package/src/commands/sonar.js +53 -0
  247. package/src/commands/standby.js +117 -0
  248. package/src/commands/start.js +121 -0
  249. package/src/commands/status.js +36 -0
  250. package/src/commands/sync.js +103 -0
  251. package/src/commands/telemetry.js +70 -0
  252. package/src/commands/tournament.js +85 -0
  253. package/src/commands/triage.js +78 -0
  254. package/src/commands/undo.js +62 -0
  255. package/src/commands/watch.js +46 -0
  256. package/src/commands/webperf.js +162 -0
  257. package/src/commands/worktree.js +76 -0
  258. package/src/config/defaults.js +270 -0
  259. package/src/config/loader.js +204 -0
  260. package/src/config/overrides.js +220 -0
  261. package/src/config/role-resolver.js +171 -0
  262. package/src/config/schema.js +299 -0
  263. package/src/config/test-harness.js +78 -0
  264. package/src/config.js +34 -0
  265. package/src/domains/domain-loader.js +245 -0
  266. package/src/domains/domain-registry.js +140 -0
  267. package/src/domains/domain-synthesizer.js +129 -0
  268. package/src/environment/adr.js +45 -0
  269. package/src/environment/board-access.js +97 -0
  270. package/src/environment/board-select.js +59 -0
  271. package/src/environment/briefs.js +129 -0
  272. package/src/environment/issue-report.js +72 -0
  273. package/src/environment/mcp-wiring.js +44 -0
  274. package/src/environment/playbook.js +127 -0
  275. package/src/git/automation.js +334 -0
  276. package/src/git/hu-automation.js +171 -0
  277. package/src/git/hu-snapshot.js +10 -0
  278. package/src/golden/asserter.js +92 -0
  279. package/src/golden/loader.js +52 -0
  280. package/src/golden/runner.js +92 -0
  281. package/src/golden/schema.js +41 -0
  282. package/src/guards/intent-guard.js +161 -0
  283. package/src/guards/output-guard.js +149 -0
  284. package/src/guards/perf-guard.js +126 -0
  285. package/src/guards/policy-guard.js +90 -0
  286. package/src/guards/policy-resolver.js +85 -0
  287. package/src/guards/secret-redactor.js +71 -0
  288. package/src/harden/advisory.js +203 -0
  289. package/src/harden/alternatives.js +50 -0
  290. package/src/harden/check.js +81 -0
  291. package/src/harden/config-engine.js +128 -0
  292. package/src/harden/config-templates.js +122 -0
  293. package/src/harden/guidelines-engine.js +48 -0
  294. package/src/harden/guidelines-templates.js +36 -0
  295. package/src/harden/harden-engine.js +80 -0
  296. package/src/harden/harness-hooks.js +107 -0
  297. package/src/harden/hook-commands.js +25 -0
  298. package/src/harden/hook-templates.js +183 -0
  299. package/src/harden/interactive.js +76 -0
  300. package/src/harden/sentinel-hooks.js +721 -0
  301. package/src/harden/stack-roots.js +105 -0
  302. package/src/harden/test-on-push.js +38 -0
  303. package/src/harden/workflow-engine.js +116 -0
  304. package/src/harden/workflow-templates.js +285 -0
  305. package/src/hu/acceptance-runner.js +138 -0
  306. package/src/hu/auto-generator.js +358 -0
  307. package/src/hu/graph.js +71 -0
  308. package/src/hu/lazy-planner.js +109 -0
  309. package/src/hu/model-router.js +108 -0
  310. package/src/hu/parallel-executor.js +94 -0
  311. package/src/hu/split-validator.js +143 -0
  312. package/src/hu/splitting-detector.js +158 -0
  313. package/src/hu/splitting-generator.js +175 -0
  314. package/src/hu/store.js +392 -0
  315. package/src/hu/worktree-bootstrap.js +59 -0
  316. package/src/identity/bootstrap.js +48 -0
  317. package/src/identity/compare.js +38 -0
  318. package/src/identity/detect.js +51 -0
  319. package/src/identity/store.js +66 -0
  320. package/src/infrastructure/command-runner.js +44 -0
  321. package/src/infrastructure/environment.js +38 -0
  322. package/src/infrastructure/file-system-service.js +63 -0
  323. package/src/infrastructure/mocks.js +182 -0
  324. package/src/lang/chunk-go.js +94 -0
  325. package/src/lang/chunk-java.js +90 -0
  326. package/src/lang/chunk-python.js +88 -0
  327. package/src/lang/chunk-rust.js +102 -0
  328. package/src/lang/go.js +14 -0
  329. package/src/lang/java.js +16 -0
  330. package/src/lang/js.js +8 -0
  331. package/src/lang/python.js +14 -0
  332. package/src/lang/registry.js +79 -0
  333. package/src/lang/rust.js +14 -0
  334. package/src/lang/tree-sitter-loader.js +45 -0
  335. package/src/mcp/direct-role-runner.js +112 -0
  336. package/src/mcp/error-classifiers.js +66 -0
  337. package/src/mcp/handlers/direct-handlers.js +486 -0
  338. package/src/mcp/handlers/hu-handlers.js +93 -0
  339. package/src/mcp/handlers/management-handlers.js +265 -0
  340. package/src/mcp/handlers/rag-handler.js +52 -0
  341. package/src/mcp/handlers/run-handler.js +313 -0
  342. package/src/mcp/orphan-guard.js +131 -0
  343. package/src/mcp/preflight.js +47 -0
  344. package/src/mcp/progress.js +149 -0
  345. package/src/mcp/response-compressor.js +81 -0
  346. package/src/mcp/run-kj.js +135 -0
  347. package/src/mcp/server-handlers.js +79 -0
  348. package/src/mcp/server.js +66 -0
  349. package/src/mcp/shared-helpers.js +297 -0
  350. package/src/mcp/sovereignty-guard.js +112 -0
  351. package/src/mcp/suggest-handler.js +92 -0
  352. package/src/mcp/tool-arg-normalizers.js +16 -0
  353. package/src/mcp/tools.js +432 -0
  354. package/src/mutate/diff-scope.js +105 -0
  355. package/src/mutate/reviewer-signal.js +76 -0
  356. package/src/mutate/runner.js +72 -0
  357. package/src/mutate/tool-registry.js +90 -0
  358. package/src/onboarder/cache.js +24 -0
  359. package/src/onboarder/collectors/framework-multilang.js +64 -0
  360. package/src/onboarder/collectors/index.js +136 -0
  361. package/src/orchestrator/agent-fallback.js +81 -0
  362. package/src/orchestrator/brain-coordinator.js +186 -0
  363. package/src/orchestrator/ci-integration.js +139 -0
  364. package/src/orchestrator/config-init.js +510 -0
  365. package/src/orchestrator/direct-actions.js +217 -0
  366. package/src/orchestrator/drivers/error-recovery.js +287 -0
  367. package/src/orchestrator/drivers/init-context.js +227 -0
  368. package/src/orchestrator/drivers/iteration-loop.js +380 -0
  369. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +69 -0
  370. package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
  371. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
  372. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +114 -0
  373. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
  374. package/src/orchestrator/drivers/post-loop.js +383 -0
  375. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +136 -0
  376. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
  377. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
  378. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +391 -0
  379. package/src/orchestrator/drivers/pre-loop.js +507 -0
  380. package/src/orchestrator/drivers/run-hu-batch.js +427 -0
  381. package/src/orchestrator/feedback-enrichment.js +140 -0
  382. package/src/orchestrator/feedback-queue.js +151 -0
  383. package/src/orchestrator/flow-control.js +170 -0
  384. package/src/orchestrator/flow-runner.js +364 -0
  385. package/src/orchestrator/fs-leak-detector.js +210 -0
  386. package/src/orchestrator/hu-scheduler.js +114 -0
  387. package/src/orchestrator/hu-sub-pipeline.js +616 -0
  388. package/src/orchestrator/integrations.js +81 -0
  389. package/src/orchestrator/iteration-gate.js +73 -0
  390. package/src/orchestrator/iteration-stages.js +9 -0
  391. package/src/orchestrator/parallel-limiter.js +74 -0
  392. package/src/orchestrator/pipeline-context.js +72 -0
  393. package/src/orchestrator/post-loop-stages.js +468 -0
  394. package/src/orchestrator/pre-loop-stages.js +11 -0
  395. package/src/orchestrator/preflight-checks.js +462 -0
  396. package/src/orchestrator/repair/repair-detector.js +117 -0
  397. package/src/orchestrator/repair/repair-runner.js +142 -0
  398. package/src/orchestrator/reviewer-fallback.js +45 -0
  399. package/src/orchestrator/role-output-compressor.js +189 -0
  400. package/src/orchestrator/session-journal.js +308 -0
  401. package/src/orchestrator/solomon-escalation.js +305 -0
  402. package/src/orchestrator/solomon-rules.js +212 -0
  403. package/src/orchestrator/stages/architect-stage.js +172 -0
  404. package/src/orchestrator/stages/coder-stage.js +480 -0
  405. package/src/orchestrator/stages/domain-curator-stage.js +82 -0
  406. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
  407. package/src/orchestrator/stages/hu-reviewer-stage.js +394 -0
  408. package/src/orchestrator/stages/perf-stage.js +140 -0
  409. package/src/orchestrator/stages/planner-stage.js +85 -0
  410. package/src/orchestrator/stages/rag-context-stage.js +77 -0
  411. package/src/orchestrator/stages/rag-preload-decisor.js +58 -0
  412. package/src/orchestrator/stages/research-stage.js +69 -0
  413. package/src/orchestrator/stages/reviewer-stage.js +422 -0
  414. package/src/orchestrator/stages/sonar-stage.js +421 -0
  415. package/src/orchestrator/stages/stage-classes.js +84 -0
  416. package/src/orchestrator/stages/stage-executor.js +155 -0
  417. package/src/orchestrator/stages/tdd-discipline-stage.js +56 -0
  418. package/src/orchestrator/stages/tool-judge-stage.js +53 -0
  419. package/src/orchestrator/stages/triage-stage.js +172 -0
  420. package/src/orchestrator/standby.js +71 -0
  421. package/src/orchestrator/verification-gate.js +183 -0
  422. package/src/orchestrator.js +22 -0
  423. package/src/plan/adr-generator.js +170 -0
  424. package/src/plan/adr-loader.js +60 -0
  425. package/src/plan/plan-executor.js +100 -0
  426. package/src/plan/plan-fixer.js +192 -0
  427. package/src/plan/plan-hu-ops.js +16 -0
  428. package/src/plan/plan-id.js +4 -0
  429. package/src/plan/plan-migrate-result.js +24 -0
  430. package/src/plan/plan-reviewer.js +236 -0
  431. package/src/plan/plan-schema.js +334 -0
  432. package/src/plan/plan-store.js +260 -0
  433. package/src/plan/plan-structural-pass.js +122 -0
  434. package/src/plan/plan-validation.js +4 -0
  435. package/src/plan/preflight-hu.js +105 -0
  436. package/src/plan/spec-detector.js +61 -0
  437. package/src/plan/tests-synthesizer.js +131 -0
  438. package/src/planning-game/adapter.js +108 -0
  439. package/src/planning-game/architect-adrs.js +45 -0
  440. package/src/planning-game/client.js +108 -0
  441. package/src/planning-game/decomposition.js +87 -0
  442. package/src/planning-game/pipeline-adapter.js +251 -0
  443. package/src/plugins/loader.js +67 -0
  444. package/src/policy/add.js +111 -0
  445. package/src/policy/decisions.js +42 -0
  446. package/src/policy/engine.js +82 -0
  447. package/src/policy/exceptions.js +60 -0
  448. package/src/privacy/onboarding.js +35 -0
  449. package/src/privacy/scan.js +120 -0
  450. package/src/prompts/arbiter.js +38 -0
  451. package/src/prompts/architect.js +103 -0
  452. package/src/prompts/audit.js +474 -0
  453. package/src/prompts/coder.js +287 -0
  454. package/src/prompts/diff-clip.js +27 -0
  455. package/src/prompts/discover.js +222 -0
  456. package/src/prompts/hu-reviewer.js +219 -0
  457. package/src/prompts/planner.js +312 -0
  458. package/src/prompts/prompt-layout.js +80 -0
  459. package/src/prompts/prompt-resolver.js +120 -0
  460. package/src/prompts/reviewer.js +75 -0
  461. package/src/prompts/rtk-snippet.js +25 -0
  462. package/src/prompts/spec-reviewer.js +66 -0
  463. package/src/prompts/split-signal.js +82 -0
  464. package/src/prompts/start-decision.js +52 -0
  465. package/src/prompts/tool-correctness-judge.js +31 -0
  466. package/src/prompts/triage.js +57 -0
  467. package/src/rag/auto-update.js +72 -0
  468. package/src/rag/chunker-ast.js +56 -0
  469. package/src/rag/chunker.js +132 -0
  470. package/src/rag/embedder.js +2 -0
  471. package/src/rag/embedders/_cloud-base.js +2 -0
  472. package/src/rag/embedders/cohere.js +2 -0
  473. package/src/rag/embedders/factory.js +2 -0
  474. package/src/rag/embedders/mistral.js +2 -0
  475. package/src/rag/embedders/onnx.js +2 -0
  476. package/src/rag/embedders/openai.js +2 -0
  477. package/src/rag/embedders/voyage.js +2 -0
  478. package/src/rag/eval.js +61 -0
  479. package/src/rag/governed-embedder.js +39 -0
  480. package/src/rag/indexer.js +199 -0
  481. package/src/rag/library.js +62 -0
  482. package/src/rag/ollama-capability.js +59 -0
  483. package/src/rag/ollama-manager.js +98 -0
  484. package/src/rag/onnx-fallback.js +69 -0
  485. package/src/rag/rerank.js +4 -0
  486. package/src/rag/retriever.js +2 -0
  487. package/src/rag/vec-store.js +17 -0
  488. package/src/rag/watcher.js +62 -0
  489. package/src/rag/where-parser.js +4 -0
  490. package/src/repeat-detector.js +88 -0
  491. package/src/review/card-first.js +99 -0
  492. package/src/review/card-verify.js +83 -0
  493. package/src/review/diff-generator.js +112 -0
  494. package/src/review/gate-gitignore.js +77 -0
  495. package/src/review/git-stash-helper.js +104 -0
  496. package/src/review/mutation-pregate.js +48 -0
  497. package/src/review/one-shot-review.js +122 -0
  498. package/src/review/parser.js +80 -0
  499. package/src/review/policy-gate.js +26 -0
  500. package/src/review/profiles.js +66 -0
  501. package/src/review/reviewer-fallback.js +125 -0
  502. package/src/review/schema.js +31 -0
  503. package/src/review/scope-filter.js +152 -0
  504. package/src/review/snapshot-diff.js +122 -0
  505. package/src/review/solomon-arbitration.js +113 -0
  506. package/src/review/sonar-pregate.js +63 -0
  507. package/src/review/tdd-discipline.js +90 -0
  508. package/src/review/tdd-policy.js +69 -0
  509. package/src/review/tests-with-code.js +33 -0
  510. package/src/review/verdict-store.js +87 -0
  511. package/src/review/workspace.js +21 -0
  512. package/src/roles/agent-role.js +194 -0
  513. package/src/roles/architect-role.js +77 -0
  514. package/src/roles/audit-role.js +258 -0
  515. package/src/roles/base-role.js +165 -0
  516. package/src/roles/coder-role.js +107 -0
  517. package/src/roles/commiter-role.js +110 -0
  518. package/src/roles/discover-role.js +81 -0
  519. package/src/roles/domain-curator-role.js +141 -0
  520. package/src/roles/hu-reviewer-role.js +64 -0
  521. package/src/roles/impeccable-role.js +85 -0
  522. package/src/roles/index.js +18 -0
  523. package/src/roles/karajan-brain-role.js +134 -0
  524. package/src/roles/onboarder-role.js +61 -0
  525. package/src/roles/perf-role.js +122 -0
  526. package/src/roles/planner-role.js +151 -0
  527. package/src/roles/refactorer-role.js +30 -0
  528. package/src/roles/repairer-role.js +181 -0
  529. package/src/roles/researcher-role.js +70 -0
  530. package/src/roles/reviewer-role.js +104 -0
  531. package/src/roles/security-role.js +73 -0
  532. package/src/roles/solomon-role.js +265 -0
  533. package/src/roles/sonar-role.js +70 -0
  534. package/src/roles/spec-reviewer-role.js +106 -0
  535. package/src/roles/tester-role.js +235 -0
  536. package/src/roles/tool-correctness-judge-role.js +78 -0
  537. package/src/roles/triage-role.js +99 -0
  538. package/src/session/journal/decisions-writer.js +200 -0
  539. package/src/session/journal/iteration-logger.js +194 -0
  540. package/src/session/journal/summary-writer.js +320 -0
  541. package/src/session/journal/tree-writer.js +164 -0
  542. package/src/session/journal/write-all.js +89 -0
  543. package/src/session/mutators.js +339 -0
  544. package/src/session/runtime-overrides.js +59 -0
  545. package/src/session/store.js +106 -0
  546. package/src/session-cleanup.js +116 -0
  547. package/src/skills/addyosmani-catalog.js +315 -0
  548. package/src/skills/addyosmani-role-map.js +139 -0
  549. package/src/skills/openskills-client.js +157 -0
  550. package/src/skills/semantic-detector.js +167 -0
  551. package/src/skills/skill-detector.js +385 -0
  552. package/src/skills/skill-loader.js +219 -0
  553. package/src/skills/skills-cache.js +128 -0
  554. package/src/sonar/api.js +128 -0
  555. package/src/sonar/cloud-scanner.js +78 -0
  556. package/src/sonar/config-resolver.js +88 -0
  557. package/src/sonar/credentials.js +60 -0
  558. package/src/sonar/discovery.js +124 -0
  559. package/src/sonar/enforcer.js +19 -0
  560. package/src/sonar/issue-filter.js +12 -0
  561. package/src/sonar/manager.js +181 -0
  562. package/src/sonar/project-key.js +120 -0
  563. package/src/sonar/scanner.js +307 -0
  564. package/src/sonar/token-bootstrap.js +205 -0
  565. package/src/sonar/token-recovery.js +67 -0
  566. package/src/spec-review/refine-loop.js +58 -0
  567. package/src/spec-review/run-spec-review.js +119 -0
  568. package/src/start/assessment.js +74 -0
  569. package/src/start/maturity.js +73 -0
  570. package/src/start/start-decider-role.js +79 -0
  571. package/src/start/sweep.js +157 -0
  572. package/src/sync/apply-canvas-patch.js +55 -0
  573. package/src/sync/detect-drift.js +113 -0
  574. package/src/tournament/crown.js +88 -0
  575. package/src/tournament/judge.js +150 -0
  576. package/src/tournament/run.js +142 -0
  577. package/src/tournament/scoreboard.js +124 -0
  578. package/src/tracker-bootstrap.js +44 -0
  579. package/src/types/agent.js +55 -0
  580. package/src/types/config.js +114 -0
  581. package/src/types/hu.js +47 -0
  582. package/src/types/index.js +38 -0
  583. package/src/types/policy.js +43 -0
  584. package/src/types/session.js +75 -0
  585. package/src/types/solomon.js +87 -0
  586. package/src/types/stage.js +68 -0
  587. package/src/utils/agent-detect.js +93 -0
  588. package/src/utils/ai-trash.js +48 -0
  589. package/src/utils/async-lock.js +31 -0
  590. package/src/utils/atomic-write.js +6 -0
  591. package/src/utils/banner.js +66 -0
  592. package/src/utils/binary-sources.js +70 -0
  593. package/src/utils/board-prompt-bridge.js +108 -0
  594. package/src/utils/budget.js +229 -0
  595. package/src/utils/cli-ask-question.js +93 -0
  596. package/src/utils/cli-progress.js +220 -0
  597. package/src/utils/cli-run-log.js +83 -0
  598. package/src/utils/cwd-confirm.js +39 -0
  599. package/src/utils/derive-project-name-from-cwd.js +119 -0
  600. package/src/utils/display/event-handlers.js +403 -0
  601. package/src/utils/display/formatters.js +132 -0
  602. package/src/utils/display/header.js +47 -0
  603. package/src/utils/display/resume-hint.js +41 -0
  604. package/src/utils/display/session-summary.js +118 -0
  605. package/src/utils/display/solomon.js +42 -0
  606. package/src/utils/display/spec-findings.js +44 -0
  607. package/src/utils/doc-links.js +29 -0
  608. package/src/utils/docker-install.js +34 -0
  609. package/src/utils/escape-regexp.js +15 -0
  610. package/src/utils/events.js +35 -0
  611. package/src/utils/fs.js +19 -0
  612. package/src/utils/garbage-collector.js +481 -0
  613. package/src/utils/git.js +279 -0
  614. package/src/utils/home-migration.js +87 -0
  615. package/src/utils/injection-guard.js +186 -0
  616. package/src/utils/install-hints.js +180 -0
  617. package/src/utils/json-extract.js +64 -0
  618. package/src/utils/locale.js +59 -0
  619. package/src/utils/logger.js +96 -0
  620. package/src/utils/managed-markers.js +93 -0
  621. package/src/utils/manifest-sniff.js +82 -0
  622. package/src/utils/messages.js +61 -0
  623. package/src/utils/model-selector.js +107 -0
  624. package/src/utils/os-detect.js +79 -0
  625. package/src/utils/paths.js +13 -0
  626. package/src/utils/pending-user-action.js +95 -0
  627. package/src/utils/port-check.js +6 -0
  628. package/src/utils/port-occupant.js +96 -0
  629. package/src/utils/pricing.js +29 -0
  630. package/src/utils/process.js +4 -0
  631. package/src/utils/project-detect.js +160 -0
  632. package/src/utils/prompt-project-name.js +35 -0
  633. package/src/utils/provider-env.js +116 -0
  634. package/src/utils/qmd-collection.js +72 -0
  635. package/src/utils/qmd-detect.js +18 -0
  636. package/src/utils/qmd-install.js +37 -0
  637. package/src/utils/quota-simulator.js +56 -0
  638. package/src/utils/rate-limit-detector.js +181 -0
  639. package/src/utils/repo-cleaner.js +79 -0
  640. package/src/utils/retry.js +88 -0
  641. package/src/utils/role-assigner.js +137 -0
  642. package/src/utils/role-env.js +58 -0
  643. package/src/utils/rtk-detect.js +18 -0
  644. package/src/utils/rtk-install.js +38 -0
  645. package/src/utils/rtk-wrapper.js +87 -0
  646. package/src/utils/run-context.js +75 -0
  647. package/src/utils/run-log.js +217 -0
  648. package/src/utils/run-registry.js +9 -0
  649. package/src/utils/shared-paths.js +12 -0
  650. package/src/utils/squeezr-detect.js +21 -0
  651. package/src/utils/squeezr-install.js +37 -0
  652. package/src/utils/stack-detect.js +135 -0
  653. package/src/utils/stall-detector.js +159 -0
  654. package/src/utils/status-dashboard.js +254 -0
  655. package/src/utils/task-file.js +109 -0
  656. package/src/utils/telemetry.js +124 -0
  657. package/src/utils/templates-root.js +61 -0
  658. package/src/utils/tool-call-extractor.js +56 -0
  659. package/src/utils/tool-governor.js +116 -0
  660. package/src/utils/tool-installer.js +93 -0
  661. package/src/utils/update-check.js +280 -0
  662. package/src/utils/vector-store-orphans.js +89 -0
  663. package/src/utils/welcome.js +47 -0
  664. package/src/utils/with-timeout.js +42 -0
  665. package/src/utils/wizard.js +47 -0
  666. package/src/webperf/cwv-gate.js +94 -0
  667. package/src/webperf/devtools-detect.js +112 -0
  668. package/src/webperf/scanner.js +209 -0
  669. package/src/webperf/webperf-role-integration.js +66 -0
  670. package/templates/coder-rules.md +45 -0
  671. package/templates/docker-compose.sonar.yml +60 -0
  672. package/templates/domains/example-dental.md +57 -0
  673. package/templates/kj.config.yml +128 -0
  674. package/templates/review-rules.md +17 -0
  675. package/templates/roles/architect/anthropic.md +74 -0
  676. package/templates/roles/architect/default.md +72 -0
  677. package/templates/roles/architect/google.md +61 -0
  678. package/templates/roles/architect/openai.md +60 -0
  679. package/templates/roles/architect/opencode.md +59 -0
  680. package/templates/roles/architect.md +76 -0
  681. package/templates/roles/audit.md +68 -0
  682. package/templates/roles/coder/anthropic.md +81 -0
  683. package/templates/roles/coder/default.md +78 -0
  684. package/templates/roles/coder/google.md +87 -0
  685. package/templates/roles/coder/openai.md +68 -0
  686. package/templates/roles/coder/opencode.md +58 -0
  687. package/templates/roles/coder.md +152 -0
  688. package/templates/roles/commiter.md +44 -0
  689. package/templates/roles/discover.md +167 -0
  690. package/templates/roles/domain-curator.md +52 -0
  691. package/templates/roles/hu-reviewer.md +236 -0
  692. package/templates/roles/impeccable-design.md +87 -0
  693. package/templates/roles/impeccable.md +125 -0
  694. package/templates/roles/karajan-brain.md +95 -0
  695. package/templates/roles/onboarder.md +45 -0
  696. package/templates/roles/planner/anthropic.md +59 -0
  697. package/templates/roles/planner/default.md +57 -0
  698. package/templates/roles/planner/google.md +75 -0
  699. package/templates/roles/planner/openai.md +55 -0
  700. package/templates/roles/planner/opencode.md +62 -0
  701. package/templates/roles/planner.md +61 -0
  702. package/templates/roles/refactorer.md +48 -0
  703. package/templates/roles/repairer.md +103 -0
  704. package/templates/roles/researcher.md +41 -0
  705. package/templates/roles/reviewer/anthropic.md +61 -0
  706. package/templates/roles/reviewer/default.md +62 -0
  707. package/templates/roles/reviewer/google.md +63 -0
  708. package/templates/roles/reviewer/openai.md +57 -0
  709. package/templates/roles/reviewer/opencode.md +42 -0
  710. package/templates/roles/reviewer-paranoid.md +38 -0
  711. package/templates/roles/reviewer-relaxed.md +34 -0
  712. package/templates/roles/reviewer-strict.md +37 -0
  713. package/templates/roles/reviewer.md +62 -0
  714. package/templates/roles/security.md +54 -0
  715. package/templates/roles/solomon/anthropic.md +71 -0
  716. package/templates/roles/solomon/default.md +83 -0
  717. package/templates/roles/solomon/google.md +61 -0
  718. package/templates/roles/solomon/openai.md +53 -0
  719. package/templates/roles/solomon/opencode.md +55 -0
  720. package/templates/roles/solomon.md +83 -0
  721. package/templates/roles/sonar.md +49 -0
  722. package/templates/roles/spec-reviewer.md +47 -0
  723. package/templates/roles/tester.md +50 -0
  724. package/templates/roles/triage.md +69 -0
  725. package/templates/skills/kj-architect.md +45 -0
  726. package/templates/skills/kj-audit.md +63 -0
  727. package/templates/skills/kj-board.md +16 -0
  728. package/templates/skills/kj-code.md +51 -0
  729. package/templates/skills/kj-csv-transform.md +40 -0
  730. package/templates/skills/kj-data-report.md +44 -0
  731. package/templates/skills/kj-discover.md +24 -0
  732. package/templates/skills/kj-rag-query.md +50 -0
  733. package/templates/skills/kj-review.md +47 -0
  734. package/templates/skills/kj-run.md +73 -0
  735. package/templates/skills/kj-security.md +49 -0
  736. package/templates/skills/kj-sonar.md +41 -0
  737. package/templates/skills/kj-sql-analysis.md +40 -0
  738. package/templates/skills/kj-test.md +40 -0
  739. package/templates/workflows/automerge.yml +30 -0
  740. package/templates/workflows/houston-override.yml +46 -0
  741. package/templates/workflows/kj-ci-gateway.yml +58 -0
  742. package/vendor/tree-sitter-grammars/README.md +21 -0
  743. package/vendor/tree-sitter-grammars/go.wasm +0 -0
  744. package/vendor/tree-sitter-grammars/java.wasm +0 -0
  745. package/vendor/tree-sitter-grammars/python.wasm +0 -0
  746. package/vendor/tree-sitter-grammars/rust.wasm +0 -0
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Generic Karajan-CLI launcher used by the board's "Commands" panel.
3
+ * Wraps the same spawn-detached-with-log pattern that `runPlan`
4
+ * already uses (see plan-mutations.js), but covers the broader set
5
+ * of commands the user wants to trigger from the UI: plan,
6
+ * architect, clean, etc.
7
+ *
8
+ * Security: the command name is matched against an explicit
9
+ * whitelist. Free-form arg arrays are NOT allowed — each command
10
+ * builds its argv from a typed schema. This keeps a curious browser
11
+ * client from doing `node cli.js -e "rm -rf /"` via a crafted POST.
12
+ *
13
+ * Output: each command writes to `~/.karajan/hu-board-runs/
14
+ * cmd-<commandId>.log`, which the existing `/api/plans/:planId/log`
15
+ * endpoint can tail (we generalise it to take a log id, not just a
16
+ * planId, in routes/api.js).
17
+ */
18
+
19
+ import { mkdirSync, openSync } from "node:fs";
20
+ import { join, dirname } from "node:path";
21
+ import { spawn } from "node:child_process";
22
+ import { fileURLToPath } from "node:url";
23
+ import { randomUUID } from "node:crypto";
24
+
25
+ // Repo root → so we can spawn `node <repo>/src/cli.js …` regardless
26
+ // of whether `kj` is on $PATH for the board process.
27
+ const REPO_ROOT = (() => {
28
+ const here = dirname(fileURLToPath(import.meta.url)); // packages/hu-board/src
29
+ return join(here, "..", "..", ".."); // → repo root
30
+ })();
31
+ const KJ_CLI = join(REPO_ROOT, "src", "cli.js");
32
+
33
+ // Path now lives in db.js::getHuBoardRunsDir() — KJC-TSK-0421
34
+ // unification. The local helper is gone; callers import directly.
35
+ import { getHuBoardRunsDir } from "./db.js";
36
+
37
+ /**
38
+ * Whitelist + argv builders. Each entry maps a logical command name
39
+ * (`plan`, `architect`, `clean`) to a function that turns the
40
+ * board-side input into a CLI argv. Adding a new command means: add
41
+ * an entry here and an entry in the board UI's `COMMANDS` table.
42
+ *
43
+ * Why typed builders instead of a generic args array: the browser
44
+ * client can't smuggle arbitrary flags. Anything that's not in the
45
+ * whitelist is rejected with 400.
46
+ *
47
+ * Each builder returns `{ args, projectDir? }`. `projectDir` is the
48
+ * cwd the spawn should use (defaults to the board's cwd).
49
+ */
50
+ const COMMAND_BUILDERS = {
51
+ plan: (input) => {
52
+ const args = ["plan"];
53
+ if (input.taskFile) args.push("--task-file", String(input.taskFile));
54
+ if (input.context) args.push("--context", String(input.context));
55
+ if (input.quick) args.push("--quick");
56
+ if (input.noTestsSynth) args.push("--no-tests-synth");
57
+ if (input.noPlanReview) args.push("--no-plan-review");
58
+ // Positional task goes last when no --task-file was given.
59
+ if (!input.taskFile && input.task) args.push(String(input.task));
60
+ return { args, projectDir: input.projectDir };
61
+ },
62
+ architect: (input) => {
63
+ const args = ["architect"];
64
+ if (input.taskFile) args.push("--task-file", String(input.taskFile));
65
+ if (input.context) args.push("--context", String(input.context));
66
+ if (!input.taskFile && input.task) args.push(String(input.task));
67
+ return { args, projectDir: input.projectDir };
68
+ },
69
+ clean: (input) => {
70
+ const args = ["clean", "--yes"]; // never prompt; UI confirms ahead
71
+ if (input.nuke) args.push("--nuke");
72
+ if (input.dryRun) args.push("--dry-run");
73
+ return { args };
74
+ },
75
+ };
76
+
77
+ /**
78
+ * Spawn the requested CLI command as a detached child. Returns
79
+ * `{ ok, commandId, pid, logPath, argv }` so the caller can offer
80
+ * the log to the user via the existing log viewer.
81
+ *
82
+ * Test seam: `KJ_RUN_SPAWN_MODE=echo` short-circuits the spawn so
83
+ * vitest doesn't actually boot the orchestrator.
84
+ *
85
+ * @param {object} args
86
+ * @param {string} args.command - one of the COMMAND_BUILDERS keys
87
+ * @param {object} [args.input]
88
+ * @returns {{ ok: boolean, commandId?: string, pid?: number,
89
+ * logPath?: string, argv?: string[], error?: string }}
90
+ */
91
+ export function runKjCommand({ command, input = {} } = {}) {
92
+ const build = COMMAND_BUILDERS[command];
93
+ if (!build) {
94
+ return { ok: false, error: `Unsupported command: ${command}. Allowed: ${Object.keys(COMMAND_BUILDERS).join(", ")}` };
95
+ }
96
+
97
+ let built;
98
+ try {
99
+ built = build(input);
100
+ } catch (err) {
101
+ return { ok: false, error: `Bad input for "${command}": ${err.message}` };
102
+ }
103
+
104
+ const dir = getHuBoardRunsDir();
105
+ mkdirSync(dir, { recursive: true });
106
+ const commandId = `cmd-${command}-${Date.now()}-${randomUUID().slice(0, 8)}`;
107
+ const logPath = join(dir, `${commandId}.log`);
108
+ const argv = [KJ_CLI, ...built.args];
109
+
110
+ if (process.env.KJ_RUN_SPAWN_MODE === "echo") {
111
+ return { ok: true, commandId, pid: 0, logPath, argv: [process.execPath, ...argv] };
112
+ }
113
+
114
+ const out = openSync(logPath, "a");
115
+ const err = openSync(logPath, "a");
116
+ const child = spawn(process.execPath, argv, {
117
+ cwd: built.projectDir || process.cwd(),
118
+ detached: true,
119
+ stdio: ["ignore", out, err],
120
+ // KJ_INSIDE_BOARD: lets `kj clean --nuke` skip SIGTERMing the
121
+ // board (it'd kill the page the user is looking at). Other
122
+ // future "is this run hosted by the board" branches can read
123
+ // the same flag.
124
+ env: { ...process.env, CLAUDECODE: undefined, KJ_INSIDE_BOARD: "1" },
125
+ });
126
+ child.unref();
127
+ return { ok: true, commandId, pid: child.pid ?? 0, logPath };
128
+ }
129
+
130
+ /** Test helper: list of command names exposed. */
131
+ export function listSupportedCommands() {
132
+ return Object.keys(COMMAND_BUILDERS);
133
+ }
@@ -0,0 +1,531 @@
1
+ /**
2
+ * Read / write the global Karajan config (~/.karajan/kj.config.yml)
3
+ * from the board.
4
+ *
5
+ * The board is a Node Express server that the user already has
6
+ * running, so it's the natural place to expose a "settings" page —
7
+ * no extra terminal trip, no risk of malformed YAML, plain Spanish
8
+ * labels for non-technical users.
9
+ *
10
+ * Design notes:
11
+ *
12
+ * - The yml may contain dozens of fields (legacy, advanced, future
13
+ * experiments). The board only EXPOSES + EDITS a small whitelist
14
+ * that maps to the labels the UI shows. Everything else is read,
15
+ * preserved verbatim, and written back unchanged. So a power
16
+ * user editing the file by hand never loses keys when they later
17
+ * touch the modal.
18
+ *
19
+ * - Writes go through a tmp-file + atomic rename so a partial
20
+ * write never leaves the user with a corrupt yml.
21
+ *
22
+ * - A `.bak` copy is kept of the previous content so the user can
23
+ * recover from a botched save outside the modal (rare but
24
+ * irrecoverable otherwise).
25
+ *
26
+ * - VITEST aware: when KJ_HOME is set we honour it (every test
27
+ * harness uses tmp dirs already). When not set we use
28
+ * `~/.karajan/kj.config.yml` like the rest of Karajan does.
29
+ */
30
+
31
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, renameSync } from 'node:fs';
32
+ import { join, dirname } from 'node:path';
33
+ import yaml from 'js-yaml';
34
+ import { getKjHome } from './db.js';
35
+
36
+ /**
37
+ * v2.30.0 PR4 — scope resolver.
38
+ *
39
+ * 'global' → ~/.karajan/kj.config.yml (mismo path que el resto de
40
+ * Karajan; afecta a todos los proyectos).
41
+ * 'project' → <projectDir>/.karajan/kj.config.yml (override por
42
+ * proyecto; cuando existe, el loader del padre lo prefiere
43
+ * sobre el global).
44
+ *
45
+ * projectDir se resuelve desde `KJ_PROJECT_DIR` (ya usado por
46
+ * journal-parser.js) con fallback a `process.cwd()` — el board se
47
+ * arranca desde el repo del usuario, así que cwd es el proyecto.
48
+ */
49
+ export const SCOPES = ['global', 'project'];
50
+
51
+ function getProjectDir() {
52
+ return process.env.KJ_PROJECT_DIR || process.cwd();
53
+ }
54
+
55
+ function configPath(scope = 'global') {
56
+ if (scope === 'project') {
57
+ return join(getProjectDir(), '.karajan', 'kj.config.yml');
58
+ }
59
+ // Same path the parent uses (src/utils/paths.js::getKarajanHome).
60
+ // KJC-TSK-0420: delegate to db.js::getKjHome — gives us KARAJAN_HOME
61
+ // priority + KJ_HOME fallback + VITEST guard from one place, instead
62
+ // of duplicating the precedence here.
63
+ return join(getKjHome(), 'kj.config.yml');
64
+ }
65
+
66
+ function backupPath(scope = 'global') { return `${configPath(scope)}.bak`; }
67
+
68
+ /**
69
+ * v2.30.0 — Categorías para agrupar los campos en el modal. El backend
70
+ * sólo declara la metadata (id, label, icon, order). El front (app.js)
71
+ * itera sobre EDITABLE_FIELDS, agrupa por `f.category` y dibuja un
72
+ * <h3> por sección. Si un campo nuevo no declara categoría, cae en
73
+ * "Otros" (defensivo: no se pierde aunque se nos olvide etiquetarlo).
74
+ */
75
+ export const CATEGORIES = [
76
+ { id: 'agents', label: 'Agentes y modelos', icon: '🤖', order: 1 },
77
+ { id: 'pipeline', label: 'Roles del pipeline', icon: '🔌', order: 2 },
78
+ { id: 'rag', label: 'RAG (búsqueda de contexto)', icon: '📚', order: 3 },
79
+ { id: 'session', label: 'Tiempos de sesión', icon: '⏱', order: 4 },
80
+ { id: 'quality', label: 'Calidad', icon: '✅', order: 5 },
81
+ // KJC-PRP-0002 PR5: team-shared HU board controls. Today only carries
82
+ // the conflict-resolution policy when a planId lives in both
83
+ // ~/.karajan/plans/ (per-user) and <projectDir>/.karajan-shared/plans/
84
+ // (team-committed). Will grow as more board-level multi-user knobs land.
85
+ { id: 'huBoard', label: 'HU Board (equipo)', icon: '🔗', order: 6 },
86
+ ];
87
+
88
+ /**
89
+ * Whitelist of fields the board's modal exposes. Everything else
90
+ * stays untouched in the yml. Each entry says where in the yml the
91
+ * field lives (`path`) and how to render it for the UI (`type`,
92
+ * `label`, `options` for selects, `category` for grouping).
93
+ *
94
+ * The path is a dotted notation we resolve manually (so we never
95
+ * need to take a runtime dep on lodash for `_.get`/`_.set`).
96
+ */
97
+ export const EDITABLE_FIELDS = [
98
+ {
99
+ key: 'coder',
100
+ path: 'coder',
101
+ category: 'agents',
102
+ type: 'select',
103
+ label: 'Agente para el rol "coder"',
104
+ help: 'Quién escribe el código. Claude es el más fiable, Codex es más rápido.',
105
+ options: ['claude', 'codex', 'gemini', 'aider'],
106
+ default: 'claude',
107
+ },
108
+ {
109
+ key: 'reviewer',
110
+ path: 'reviewer',
111
+ category: 'agents',
112
+ type: 'select',
113
+ label: 'Agente para el rol "reviewer"',
114
+ help: 'Quién revisa el código del coder. Conviene que sea un agente distinto al coder para detectar errores.',
115
+ options: ['claude', 'codex', 'gemini', 'aider'],
116
+ default: 'codex',
117
+ },
118
+ {
119
+ key: 'modelMode',
120
+ path: '_modelMode_virtual',
121
+ category: 'agents',
122
+ type: 'select',
123
+ label: 'Estrategia de modelos',
124
+ help: 'Auto = Karajan elige el modelo según la complejidad (haiku/sonnet/opus). Sonnet siempre = barato, ideal para pruebas. Opus siempre = caro, máxima calidad.',
125
+ options: ['auto', 'sonnet', 'opus'],
126
+ default: 'auto',
127
+ // Virtual field: doesn't map 1:1 to a yml key. Translate when
128
+ // reading + writing (see readConfig + writeConfigPatch).
129
+ },
130
+ {
131
+ key: 'maxIterationMinutes',
132
+ path: 'session.max_iteration_minutes',
133
+ category: 'session',
134
+ type: 'number',
135
+ label: 'Tope de minutos por iteración',
136
+ help: 'Si una iteración (coder + reviewer) supera este tiempo, Karajan la corta y Solomon decide qué hacer.',
137
+ min: 1,
138
+ max: 120,
139
+ default: 30,
140
+ },
141
+ {
142
+ key: 'maxTotalMinutes',
143
+ path: 'session.max_total_minutes',
144
+ category: 'session',
145
+ type: 'number',
146
+ label: 'Tope de minutos total por sesión',
147
+ help: 'Tope absoluto del wall-clock de la sesión. Si se llega aquí, Karajan para aunque queden HUs.',
148
+ min: 5,
149
+ max: 480,
150
+ default: 120,
151
+ },
152
+ {
153
+ key: 'sonarEnabled',
154
+ path: 'sonarqube.enabled',
155
+ category: 'quality',
156
+ type: 'boolean',
157
+ label: 'Activar análisis SonarQube',
158
+ help: 'Pasa el código por SonarQube y bloquea si el quality gate falla. Requiere SonarQube corriendo.',
159
+ default: false,
160
+ },
161
+ // v2.30.0 — pipeline role toggles. Cada uno controla si una etapa
162
+ // del pipeline se ejecuta. Brain está OFF por defecto porque sigue
163
+ // siendo opt-in (v2 feature); el resto reflejan los defaults de
164
+ // src/config/defaults.js.
165
+ {
166
+ key: 'plannerEnabled',
167
+ path: 'pipeline.planner.enabled',
168
+ category: 'pipeline',
169
+ type: 'boolean',
170
+ label: 'Activar rol planner',
171
+ help: 'Genera un plan de implementación antes de codificar. Útil en tareas con >2 archivos o devPoints ≥ 3.',
172
+ default: true,
173
+ },
174
+ {
175
+ key: 'researcherEnabled',
176
+ path: 'pipeline.researcher.enabled',
177
+ category: 'pipeline',
178
+ type: 'boolean',
179
+ label: 'Activar rol researcher',
180
+ help: 'Investiga la codebase antes del coder (lectura de archivos clave, patrones existentes).',
181
+ default: true,
182
+ },
183
+ {
184
+ key: 'architectEnabled',
185
+ path: 'pipeline.architect.enabled',
186
+ category: 'pipeline',
187
+ type: 'boolean',
188
+ label: 'Activar rol architect',
189
+ help: 'Diseña la arquitectura (capas, patrones, contratos) antes de codificar. Recomendado en cambios estructurales.',
190
+ default: true,
191
+ },
192
+ {
193
+ key: 'testerEnabled',
194
+ path: 'pipeline.tester.enabled',
195
+ category: 'pipeline',
196
+ type: 'boolean',
197
+ label: 'Activar rol tester',
198
+ help: 'Genera tests automáticos durante o después del coder. Refuerza la calidad.',
199
+ default: true,
200
+ },
201
+ {
202
+ key: 'securityEnabled',
203
+ path: 'pipeline.security.enabled',
204
+ category: 'pipeline',
205
+ type: 'boolean',
206
+ label: 'Activar rol security',
207
+ help: 'Audita el código contra OWASP-style issues, secret leaks, dependencias vulnerables.',
208
+ default: true,
209
+ },
210
+ {
211
+ key: 'refactorerEnabled',
212
+ path: 'pipeline.refactorer.enabled',
213
+ category: 'pipeline',
214
+ type: 'boolean',
215
+ label: 'Activar rol refactorer',
216
+ help: 'Pasa una iteración de limpieza tras el coder (extract function, rename, etc.).',
217
+ default: false,
218
+ },
219
+ {
220
+ key: 'impeccableEnabled',
221
+ path: 'pipeline.impeccable.enabled',
222
+ category: 'pipeline',
223
+ type: 'boolean',
224
+ label: 'Activar rol impeccable',
225
+ help: 'Auditoría UI/UX automática (Lighthouse, a11y, contraste). Solo útil en frontend.',
226
+ default: false,
227
+ },
228
+ {
229
+ key: 'brainEnabled',
230
+ path: 'pipeline.brain.enabled',
231
+ category: 'pipeline',
232
+ type: 'boolean',
233
+ label: 'Activar Karajan Brain (v2)',
234
+ help: 'Orquestador IA central: enriquece feedback, comprime outputs y verifica cambios reales. Opt-in.',
235
+ default: false,
236
+ },
237
+ // v2.30.0 — RAG controls. Cubren los tres ejes que afectan al
238
+ // retrieval en tiempo de pipeline: pre-carga (rag.preload.*),
239
+ // proveedor de embeddings (rag.embedder.provider) y parámetros de
240
+ // búsqueda (rag.search.*, cableados al stage en KJC-TSK-0463).
241
+ {
242
+ key: 'ragPreloadEnabled',
243
+ path: 'rag.preload.enabled',
244
+ category: 'rag',
245
+ type: 'boolean',
246
+ label: 'Activar pre-carga RAG en el pipeline',
247
+ help: 'Si está activo, antes de cada iteración se inyecta contexto del vector store en el prompt del coder.',
248
+ default: false,
249
+ },
250
+ {
251
+ key: 'ragPreloadTopK',
252
+ path: 'rag.preload.topK',
253
+ category: 'rag',
254
+ type: 'number',
255
+ label: 'Nº de chunks a pre-cargar (topK)',
256
+ help: 'Cuántos fragmentos del vector store se inyectan como contexto. 3-5 funciona bien; subir gasta tokens.',
257
+ min: 1,
258
+ max: 20,
259
+ default: 5,
260
+ },
261
+ {
262
+ key: 'ragPreloadScope',
263
+ path: 'rag.preload.scope',
264
+ category: 'rag',
265
+ type: 'select',
266
+ label: 'Scope de la pre-carga RAG',
267
+ help: '"all" mezcla todo. "code" solo busca en el código indexado, "plans" en planes previos, "onboarding" en el brief del proyecto.',
268
+ options: ['all', 'code', 'plans', 'onboarding'],
269
+ default: 'all',
270
+ },
271
+ {
272
+ key: 'ragEmbedderProvider',
273
+ path: 'rag.embedder.provider',
274
+ category: 'rag',
275
+ type: 'select',
276
+ label: 'Proveedor de embeddings RAG',
277
+ help: 'Ollama local es gratis. OpenAI/Voyage/Cohere/Mistral son cloud (requieren API key). ONNX corre 100% local con @huggingface/transformers.',
278
+ options: ['ollama', 'openai', 'voyage', 'cohere', 'mistral', 'onnx'],
279
+ default: 'ollama',
280
+ },
281
+ // KJC-TSK-0463 — search-time knobs ahora cableados al stage.
282
+ // mode/alpha controlan el fuse semantic vs keyword; rerank activa el
283
+ // cross-encoder opcional (más calidad, más latencia).
284
+ {
285
+ key: 'ragSearchMode',
286
+ path: 'rag.search.mode',
287
+ category: 'rag',
288
+ type: 'select',
289
+ label: 'Modo de búsqueda RAG',
290
+ help: '"hybrid" mezcla semántico + BM25 (recomendado). "semantic" sólo embeddings. "keyword" sólo BM25 (útil si los embeddings están desafinados).',
291
+ options: ['hybrid', 'semantic', 'keyword'],
292
+ default: 'hybrid',
293
+ },
294
+ {
295
+ key: 'ragSearchAlpha',
296
+ path: 'rag.search.alpha',
297
+ category: 'rag',
298
+ type: 'number',
299
+ label: 'Alpha del modo hybrid (0-1)',
300
+ help: 'Peso del scoring semántico vs keyword en modo hybrid. 1.0 = sólo semántico, 0.0 = sólo BM25, 0.6 (default) = inclinado a semántico.',
301
+ min: 0,
302
+ max: 1,
303
+ // KJC-BUG-0069 — number input defaults to step=1 (HTML5), so any
304
+ // non-integer default fails validity. Allow any fractional value here.
305
+ step: 'any',
306
+ default: 0.6,
307
+ },
308
+ {
309
+ key: 'ragSearchRerank',
310
+ path: 'rag.search.rerank',
311
+ category: 'rag',
312
+ type: 'boolean',
313
+ label: 'Activar rerank cross-encoder',
314
+ help: 'Re-puntúa los topK supervivientes con un cross-encoder. Mejora calidad pero añade latencia y requiere modelo extra. Opt-in.',
315
+ default: false,
316
+ },
317
+ // KJC-PRP-0002 PR5: when the board scans plans, it visits both
318
+ // ~/.karajan/plans/<slug>/ (per-user cache) and .karajan-shared/plans/
319
+ // (team copy). If a planId lives in both, the second pass overwrites
320
+ // the first. Before this knob the order was hard-coded shared-wins
321
+ // (per-user → shared) — fine in a team but unsafe for a solo dev who
322
+ // forgets a stale `.karajan-shared/` exists in the repo.
323
+ {
324
+ key: 'huBoardSharedConflictPolicy',
325
+ path: 'huBoard.sharedConflictPolicy',
326
+ category: 'huBoard',
327
+ type: 'select',
328
+ label: 'Política ante plan compartido y local',
329
+ help: '"shared-wins" (recomendado en equipo): la copia en .karajan-shared/ pisa la local. "local-wins" (solo dev individual): tu copia en ~/.karajan/plans/ pisa la compartida — útil si todavía estás iterando antes de publicarla.',
330
+ options: ['shared-wins', 'local-wins'],
331
+ default: 'shared-wins',
332
+ },
333
+ ];
334
+
335
+ const ROLES_FOR_MODEL_MODE = [
336
+ 'planner', 'architect', 'coder', 'reviewer', 'triage',
337
+ 'researcher', 'tester', 'security', 'refactorer', 'impeccable'
338
+ ];
339
+
340
+ function getDeep(obj, path) {
341
+ if (!obj || !path) return undefined;
342
+ const parts = path.split('.');
343
+ let cur = obj;
344
+ for (const p of parts) {
345
+ if (cur == null || typeof cur !== 'object') return undefined;
346
+ cur = cur[p];
347
+ }
348
+ return cur;
349
+ }
350
+
351
+ // KJC-BUG-0076: never walk into Object.prototype via crafted YAML keys
352
+ // (`__proto__.x`, `constructor.prototype.y`, `prototype.z`). Silent skip
353
+ // over a poisoned segment — the rest of the path is untouched.
354
+ const UNSAFE_KEY_RE = /^(?:__proto__|constructor|prototype)$/;
355
+
356
+ function setDeep(obj, path, value) {
357
+ if (typeof path !== 'string') return;
358
+ const parts = path.split('.');
359
+ const last = parts.pop();
360
+ if (UNSAFE_KEY_RE.test(last)) return;
361
+ let cur = obj;
362
+ for (const p of parts) {
363
+ if (UNSAFE_KEY_RE.test(p)) return;
364
+ if (cur[p] == null || typeof cur[p] !== 'object') cur[p] = {};
365
+ cur = cur[p];
366
+ }
367
+ cur[last] = value;
368
+ }
369
+
370
+ function deleteDeep(obj, path) {
371
+ if (typeof path !== 'string') return;
372
+ const parts = path.split('.');
373
+ const last = parts.pop();
374
+ if (UNSAFE_KEY_RE.test(last)) return;
375
+ let cur = obj;
376
+ for (const p of parts) {
377
+ if (UNSAFE_KEY_RE.test(p)) return;
378
+ if (cur == null || typeof cur !== 'object') return;
379
+ cur = cur[p];
380
+ }
381
+ if (cur && typeof cur === 'object') delete cur[last];
382
+ }
383
+
384
+ /**
385
+ * Derive the virtual `modelMode` from the actual yml shape. Looks at
386
+ * roles.<role>.model: if every editable role pins the same model,
387
+ * that's the mode. Otherwise "auto" (the default — Karajan's
388
+ * smart-select picks per role).
389
+ */
390
+ function deriveModelMode(parsed) {
391
+ const roles = parsed?.roles;
392
+ if (!roles || typeof roles !== 'object') return 'auto';
393
+ const seen = new Set();
394
+ for (const r of ROLES_FOR_MODEL_MODE) {
395
+ const m = roles[r]?.model;
396
+ if (m) seen.add(String(m).toLowerCase());
397
+ }
398
+ if (seen.size === 1) {
399
+ const only = [...seen][0];
400
+ if (only === 'sonnet' || only === 'opus') return only;
401
+ }
402
+ return 'auto';
403
+ }
404
+
405
+ /**
406
+ * Apply the virtual `modelMode` back to the yml: 'auto' deletes the
407
+ * roles.<r>.model overrides; 'sonnet' / 'opus' set them on every
408
+ * editable role. Other yml keys are left alone.
409
+ */
410
+ function applyModelMode(parsed, mode) {
411
+ if (!parsed.roles) parsed.roles = {};
412
+ if (mode === 'auto') {
413
+ for (const r of ROLES_FOR_MODEL_MODE) {
414
+ if (parsed.roles[r]?.model) delete parsed.roles[r].model;
415
+ if (parsed.roles[r] && Object.keys(parsed.roles[r]).length === 0) delete parsed.roles[r];
416
+ }
417
+ if (Object.keys(parsed.roles).length === 0) delete parsed.roles;
418
+ return;
419
+ }
420
+ for (const r of ROLES_FOR_MODEL_MODE) {
421
+ if (!parsed.roles[r] || typeof parsed.roles[r] !== 'object') parsed.roles[r] = {};
422
+ parsed.roles[r].model = mode;
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Read the current config + flatten it into the shape the UI wants:
428
+ * {
429
+ * path: "/abs/path",
430
+ * fields: [{ key, label, help, type, options?, value, default }],
431
+ * raw: <full parsed yml> // power-user inspection
432
+ * }
433
+ *
434
+ * Returns sane defaults when the file doesn't exist yet.
435
+ */
436
+ export function readConfig({ scope = 'global' } = {}) {
437
+ // PR4 — accept scope. Default 'global' preserves the previous behavior;
438
+ // 'project' reads <projectDir>/.karajan/kj.config.yml.
439
+ if (!SCOPES.includes(scope)) {
440
+ throw new Error(`Scope inválido: ${scope}. Permitidos: ${SCOPES.join(', ')}`);
441
+ }
442
+ const p = configPath(scope);
443
+ let parsed = {};
444
+ let exists = false;
445
+ if (existsSync(p)) {
446
+ try {
447
+ parsed = yaml.load(readFileSync(p, 'utf8'), { json: true }) || {};
448
+ exists = true;
449
+ } catch (err) {
450
+ throw new Error(`No se pudo parsear ${p}: ${err.message}`, { cause: err });
451
+ }
452
+ }
453
+ const fields = EDITABLE_FIELDS.map((f) => {
454
+ let value;
455
+ if (f.key === 'modelMode') value = deriveModelMode(parsed);
456
+ else value = getDeep(parsed, f.path);
457
+ if (value === undefined) value = f.default;
458
+ return { ...f, value };
459
+ });
460
+ // v2.30.0 — also expose CATEGORIES so the modal can render grouped
461
+ // sections without re-importing the constant client-side. Ordered
462
+ // by `order` for deterministic UI.
463
+ const categories = [...CATEGORIES].sort((a, b) => (a.order ?? 999) - (b.order ?? 999));
464
+ return { path: p, scope, exists, fields, categories, raw: parsed };
465
+ }
466
+
467
+ /**
468
+ * Apply a patch (subset of EDITABLE_FIELDS keys → new values) to the
469
+ * yml. Validates each field; refuses unknown keys. Writes via tmp +
470
+ * rename; keeps a .bak.
471
+ *
472
+ * @returns {{ path: string, written: boolean, applied: object[], errors: string[] }}
473
+ */
474
+ export function writeConfigPatch(patch, { scope = 'global' } = {}) {
475
+ // PR4 — same shape as readConfig: scope decides target file.
476
+ if (!SCOPES.includes(scope)) {
477
+ return { path: '', scope, written: false, applied: [], errors: [`Scope inválido: ${scope}. Permitidos: ${SCOPES.join(', ')}`] };
478
+ }
479
+ if (!patch || typeof patch !== 'object') {
480
+ return { path: configPath(scope), scope, written: false, applied: [], errors: ['patch debe ser un objeto'] };
481
+ }
482
+ // Read current state (or empty if file doesn't exist yet).
483
+ const p = configPath(scope);
484
+ let parsed = {};
485
+ if (existsSync(p)) {
486
+ try { parsed = yaml.load(readFileSync(p, 'utf8'), { json: true }) || {}; }
487
+ catch (err) { return { path: p, scope, written: false, applied: [], errors: [`No se pudo parsear el yml actual: ${err.message}`] }; }
488
+ }
489
+ const errors = [];
490
+ const applied = [];
491
+ for (const [key, val] of Object.entries(patch)) {
492
+ const field = EDITABLE_FIELDS.find((f) => f.key === key);
493
+ if (!field) { errors.push(`Campo desconocido: ${key}`); continue; }
494
+
495
+ if (field.type === 'select' && !field.options.includes(val)) {
496
+ errors.push(`${key}: valor inválido "${val}". Permitidos: ${field.options.join(', ')}`);
497
+ continue;
498
+ }
499
+ if (field.type === 'number') {
500
+ const n = Number(val);
501
+ if (!Number.isFinite(n)) { errors.push(`${key}: debe ser un número`); continue; }
502
+ if (field.min != null && n < field.min) { errors.push(`${key}: mínimo ${field.min}`); continue; }
503
+ if (field.max != null && n > field.max) { errors.push(`${key}: máximo ${field.max}`); continue; }
504
+ }
505
+ if (field.type === 'boolean' && typeof val !== 'boolean') {
506
+ errors.push(`${key}: debe ser true o false`);
507
+ continue;
508
+ }
509
+
510
+ if (field.key === 'modelMode') applyModelMode(parsed, val);
511
+ else setDeep(parsed, field.path, field.type === 'number' ? Number(val) : val);
512
+ applied.push({ key, value: val });
513
+ }
514
+ if (errors.length > 0) return { path: p, scope, written: false, applied, errors };
515
+
516
+ // Write atomically: serialize → write to .tmp → rename. Backup the
517
+ // existing file first so the user can recover.
518
+ const dir = dirname(p);
519
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
520
+ if (existsSync(p)) { try { copyFileSync(p, backupPath(scope)); } catch { /* best-effort */ } }
521
+ const dump = yaml.dump(parsed, { lineWidth: 120 });
522
+ // mkstemp would be nicer but we want to stay in the same dir for
523
+ // a guaranteed-atomic rename across the same filesystem.
524
+ const tmpFile = join(dir, `.kj.config.yml.${process.pid}.${Date.now()}.tmp`);
525
+ writeFileSync(tmpFile, dump, 'utf8');
526
+ renameSync(tmpFile, p);
527
+ return { path: p, scope, written: true, applied, errors: [] };
528
+ }
529
+
530
+ // Exposed for prototype-pollution regression tests (KJC-BUG-0076).
531
+ export const __pollutionInternals = { setDeep, deleteDeep };