@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,732 @@
1
+ import { watch } from 'chokidar';
2
+ import { readFileSync, readdirSync, existsSync, rmSync } from 'node:fs';
3
+ import { dirname, join, basename } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ import {
6
+ getKjHome,
7
+ upsertProject,
8
+ upsertStory,
9
+ upsertSession,
10
+ insertContextRequest,
11
+ getDb,
12
+ isTombstoned,
13
+ getTombstone,
14
+ removeTombstone,
15
+ } from './db.js';
16
+ import { publish as publishEvent } from './event-bus.js';
17
+ import { getSharedPlansDir, SHARED_DIR_NAME, isSharedPath } from 'karajan-core/shared-paths';
18
+ import { readConfig } from './config-yaml.js';
19
+
20
+ /**
21
+ * KJC-PRP-0002 PR5: resolve the conflict policy that decides who wins
22
+ * when the same planId is present in both the per-user cache
23
+ * (~/.karajan/plans/<slug>/) and the shared dir (.karajan-shared/plans/).
24
+ *
25
+ * Implementation is "order of last write": `syncPlanFile` upserts by
26
+ * planId, so whichever scan runs second overwrites whatever the first
27
+ * one wrote. We just swap the visit order based on policy.
28
+ *
29
+ * shared-wins (default) → per-user first, shared second
30
+ * local-wins → shared first, per-user second
31
+ *
32
+ * Reads from kj.config.yml live; absent / malformed config falls back to
33
+ * the safe team default. Project scope wins over global scope (same
34
+ * precedence the rest of the config UI honours).
35
+ */
36
+ function getSharedConflictPolicy() {
37
+ const pluck = (cfg) => cfg?.raw?.huBoard?.sharedConflictPolicy;
38
+ const valid = (v) => v === 'local-wins' || v === 'shared-wins';
39
+ try {
40
+ const projectCfg = readConfig({ scope: 'project' });
41
+ if (valid(pluck(projectCfg))) return pluck(projectCfg);
42
+ const globalCfg = readConfig({ scope: 'global' });
43
+ if (valid(pluck(globalCfg))) return pluck(globalCfg);
44
+ } catch { /* swallow — config UI handles surfacing errors */ }
45
+ return 'shared-wins';
46
+ }
47
+
48
+ /**
49
+ * Skip + best-effort fs cleanup when the resource is tombstoned. Returns
50
+ * true if the caller should bail out (resource is dead).
51
+ *
52
+ * KJC-TSK-0380: without this, every fullScan/chokidar event resurrects
53
+ * resources the user already deleted via the API.
54
+ */
55
+ function skipIfTombstoned(resourceType, resourceId, fsPathToRemove) {
56
+ if (!isTombstoned(resourceType, resourceId)) return false;
57
+ if (fsPathToRemove) {
58
+ try { rmSync(fsPathToRemove, { recursive: true, force: true }); } catch { /* best effort */ }
59
+ }
60
+ return true;
61
+ }
62
+
63
+ /**
64
+ * Derive a human-readable project name from batch data.
65
+ * Priority: explicit data.project_name → extracted from story text → sessionId.
66
+ */
67
+ function deriveProjectName(data, fallbackId) {
68
+ if (data.project_name) return data.project_name;
69
+ // Try to find "Part of: <originalTask>" in any story's certified text
70
+ const stories = data.stories || [];
71
+ for (const story of stories) {
72
+ const text = story.certified?.text || '';
73
+ const match = text.match(/Part of:\s*([^\n]+)/);
74
+ if (match) {
75
+ return slugToTitle(match[1].trim());
76
+ }
77
+ }
78
+ // Try first story's original text as a fallback
79
+ if (stories[0]?.original?.text) {
80
+ return slugToTitle(stories[0].original.text);
81
+ }
82
+ return fallbackId;
83
+ }
84
+
85
+ /**
86
+ * Turn a free-text goal into a short title-cased name (max 60 chars).
87
+ */
88
+ /**
89
+ * Derive a stable `project_id` from a plan's absolute `projectDir`.
90
+ * Used so every plan under the same source tree groups under one project
91
+ * on the board (instead of 1 project per plan — the 2 101-stories bug).
92
+ *
93
+ * @param {string} projectDir
94
+ * @returns {string}
95
+ */
96
+ function deriveProjectIdFromDir(projectDir) {
97
+ if (!projectDir || typeof projectDir !== 'string') return 'unknown';
98
+ // Same slug rule the plan-store uses, so identifiers match if we ever
99
+ // cross-reference `~/.kj/plans/<slug>/` with this project_id.
100
+ return projectDir
101
+ .replace(/^\//, '')
102
+ .replace(/[/\\]/g, '_')
103
+ .replace(/[^a-zA-Z0-9_.-]/g, '')
104
+ .slice(0, 120);
105
+ }
106
+
107
+ /**
108
+ * Turn a directory basename into a human-friendly Title Case string,
109
+ * preserving every meaningful word. Unlike `slugToTitle`, this one does
110
+ * NOT apply stopword filtering — directory names are concise on purpose
111
+ * (e.g. "linux-assistant-orchestrator" → "Linux Assistant Orchestrator",
112
+ * "my-dev-tool" → "My Dev Tool").
113
+ *
114
+ * @param {string} name
115
+ * @returns {string}
116
+ */
117
+ function titleCaseBasename(name) {
118
+ const words = String(name || '')
119
+ .replace(/[_\s]+/g, '-')
120
+ .split('-')
121
+ .map((w) => w.replace(/[^\p{L}\p{N}]/gu, ''))
122
+ .filter(Boolean);
123
+ if (words.length === 0) return String(name || '');
124
+ return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');
125
+ }
126
+
127
+ function slugToTitle(text) {
128
+ const STOPWORDS = new Set([
129
+ 'a', 'an', 'the', 'and', 'or', 'with', 'for', 'to', 'of', 'in', 'on',
130
+ 'is', 'it', 'its', 'this', 'that', 'these', 'those', 'be', 'been', 'being',
131
+ 'build', 'create', 'implement', 'make', 'develop', 'add', 'set', 'up',
132
+ 'setup', 'write', 'code', 'new', 'complete', 'from', 'scratch',
133
+ 'application', 'app', 'tool', 'system', 'project', 'using', 'use',
134
+ 'full', 'full-stack', 'fullstack', 'stack', 'based', 'simple', 'basic',
135
+ ]);
136
+ const words = String(text || '')
137
+ .toLowerCase()
138
+ .replace(/[^\p{L}\p{N}\s-]/gu, ' ')
139
+ .split(/\s+/)
140
+ .filter((w) => w && !STOPWORDS.has(w));
141
+ const meaningful = words.slice(0, 6);
142
+ if (meaningful.length === 0) return String(text).slice(0, 60).trim();
143
+ const titled = meaningful.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
144
+ return titled.length > 60 ? titled.slice(0, 57) + '...' : titled;
145
+ }
146
+
147
+ /**
148
+ * Parses a batch.json HU stories file and syncs to SQLite.
149
+ * @param {string} filePath - Absolute path to the batch.json file
150
+ */
151
+ function syncStoryFile(filePath) {
152
+ try {
153
+ const raw = readFileSync(filePath, 'utf-8');
154
+ const data = JSON.parse(raw);
155
+ const sessionId = data.session_id || basename(join(filePath, '..'));
156
+ const projectId = data.project_id || sessionId;
157
+
158
+ // Tombstone gate: if the project or its batch is buried, drop the
159
+ // file from disk and skip. Without this, fullScan resurrects them.
160
+ if (skipIfTombstoned('project', projectId, dirname(filePath))) return;
161
+ if (skipIfTombstoned('story', sessionId, dirname(filePath))) return;
162
+
163
+ // Skip if this is a non-auto batch and an auto- version already exists.
164
+ // Prevents duplicate projects when both auto-s_xxx/ and s_xxx/ exist.
165
+ if (!projectId.startsWith('auto-')) {
166
+ const db = getDb();
167
+ const autoExists = db.prepare('SELECT id FROM projects WHERE id = ?').get(`auto-${projectId}`);
168
+ if (autoExists) {
169
+ console.log(`[sync] Skipping ${projectId} — auto-${projectId} already covers this session`);
170
+ return;
171
+ }
172
+ }
173
+
174
+ upsertProject({
175
+ id: projectId,
176
+ name: deriveProjectName(data, projectId),
177
+ last_activity: data.created_at || new Date().toISOString(),
178
+ total_stories: (data.stories || []).length,
179
+ });
180
+
181
+ for (const story of data.stories || []) {
182
+ const certified = story.certified || {};
183
+ const quality = story.quality || {};
184
+ const dimensions = quality.dimensions || {};
185
+
186
+ // acceptance_criteria: support both array (auto-generator) and object (hu-reviewer)
187
+ let acText = null;
188
+ if (Array.isArray(story.acceptance_criteria)) {
189
+ acText = JSON.stringify(story.acceptance_criteria);
190
+ } else if (story.acceptance_criteria?.criteria) {
191
+ acText = JSON.stringify(story.acceptance_criteria.criteria);
192
+ }
193
+
194
+ upsertStory({
195
+ id: story.id,
196
+ project_id: projectId,
197
+ session_id: sessionId,
198
+ status: story.status || 'pending',
199
+ title: extractTitle(story),
200
+ original_text: story.original?.text || certified.text || null,
201
+ certified_as: certified.as || null,
202
+ certified_want: certified.want || (certified.text ? certified.text.slice(0, 500) : null),
203
+ certified_so_that: certified.so_that || null,
204
+ quality_total: quality.total ?? null,
205
+ quality_d1: dimensions.d1 ?? null,
206
+ quality_d2: dimensions.d2 ?? null,
207
+ quality_d3: dimensions.d3 ?? null,
208
+ quality_d4: dimensions.d4 ?? null,
209
+ quality_d5: dimensions.d5 ?? null,
210
+ quality_d6: dimensions.d6 ?? null,
211
+ antipatterns: quality.antipatterns ? JSON.stringify(quality.antipatterns) : null,
212
+ ac_format: story.acceptance_criteria?.format || (Array.isArray(story.acceptance_criteria) ? 'list' : null),
213
+ acceptance_criteria: acText,
214
+ created_at: story.created_at,
215
+ updated_at: story.updated_at,
216
+ certified_at: story.certified_at || null,
217
+ });
218
+
219
+ // Sync context requests
220
+ for (const ctx of story.context_requests || []) {
221
+ insertContextRequest({
222
+ story_id: story.id,
223
+ fields_needed: ctx.fields_needed ? JSON.stringify(ctx.fields_needed) : null,
224
+ question: ctx.question || null,
225
+ answer: ctx.answer || null,
226
+ requested_at: ctx.requested_at || null,
227
+ answered_at: ctx.answered_at || null,
228
+ });
229
+ }
230
+ }
231
+
232
+ console.log(`[sync] Synced stories from ${basename(join(filePath, '..'))}: ${(data.stories || []).length} stories`);
233
+ publishEvent({ type: 'batch', projectId, path: filePath });
234
+ } catch (err) {
235
+ console.error(`[sync] Error syncing story file ${filePath}:`, err.message);
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Extracts a short title from a story.
241
+ * @param {object} story
242
+ * @returns {string}
243
+ */
244
+ function extractTitle(story) {
245
+ // Auto-generated HUs have story.title directly
246
+ if (story.title && story.title !== story.id) return story.title.slice(0, 120);
247
+ // HU-reviewer format: certified.want
248
+ if (story.certified?.want) return story.certified.want.slice(0, 120);
249
+ // Fallback: original text or id
250
+ const text = story.original?.text || '';
251
+ return text.slice(0, 120) || story.id;
252
+ }
253
+
254
+ /**
255
+ * Parses a session.json file and syncs to SQLite.
256
+ * @param {string} filePath - Absolute path to session.json
257
+ */
258
+ function syncSessionFile(filePath) {
259
+ try {
260
+ const raw = readFileSync(filePath, 'utf-8');
261
+ const data = JSON.parse(raw);
262
+ const sessionId = data.id || basename(join(filePath, '..'));
263
+
264
+ if (skipIfTombstoned('session', sessionId, dirname(filePath))) return;
265
+
266
+ // Resolve which project this session belongs to:
267
+ // 1. If a plan / HU batch already created `auto-<sessionId>`, reuse it
268
+ // so plan-based runs group with their HU batch.
269
+ // 2. Otherwise fall back to the session's declared `project_id`.
270
+ // 3. Otherwise → REJECT. Orphan sessions (no project_id and no batch)
271
+ // USED to be promoted to a fake "Orphan sessions" project, which
272
+ // violated the user's invariant: "Si se crea proyecto y se crean
273
+ // hus se asocian a proyecto. ES IMPOSIBLE QUE [no exista esa
274
+ // asociación]". Now we skip them entirely. The fix for the source
275
+ // (kj run always stamps project_id derived from cwd) lives in
276
+ // PR-F; this is the safety net so a leftover orphan can never
277
+ // again appear as a phantom project on the board.
278
+ const autoProjectId = `auto-${sessionId}`;
279
+ const db = getDb();
280
+ const existingBatch = db.prepare('SELECT id FROM projects WHERE id = ?').get(autoProjectId);
281
+ let projectId;
282
+ if (existingBatch) {
283
+ projectId = autoProjectId;
284
+ } else if (data.project_id) {
285
+ projectId = data.project_id;
286
+ } else {
287
+ // Orphan: log + skip. Never promote to "default".
288
+ console.warn(`[sync] Session ${sessionId} has no project_id and no matching batch — skipping (orphan). File: ${filePath}`);
289
+ return;
290
+ }
291
+
292
+ // Ensure the project row exists. Skip when an auto-batch owns it already
293
+ // (that row was written by syncStoryFile with richer metadata and we do
294
+ // not want to overwrite name/total_stories).
295
+ if (!existingBatch) {
296
+ upsertProject({
297
+ id: projectId,
298
+ name: projectId,
299
+ last_activity: data.updated_at || data.created_at || new Date().toISOString(),
300
+ total_stories: 0,
301
+ });
302
+ }
303
+
304
+ // Calculate iterations from checkpoints
305
+ const checkpoints = data.checkpoints || [];
306
+ const maxIteration = checkpoints.reduce((max, cp) => Math.max(max, cp.iteration || 0), 0);
307
+
308
+ // Calculate duration from first and last checkpoint
309
+ let durationMs = null;
310
+ if (data.created_at && data.updated_at) {
311
+ durationMs = new Date(data.updated_at).getTime() - new Date(data.created_at).getTime();
312
+ }
313
+
314
+ // Collect completed stages
315
+ const stagesSet = new Set();
316
+ for (const cp of checkpoints) {
317
+ if (cp.stage && !cp.stage.includes('attempt') && !cp.stage.includes('checkpoint')) {
318
+ stagesSet.add(cp.stage);
319
+ }
320
+ }
321
+
322
+ // Project row was created above (unless an auto-batch owns it); the
323
+ // session always has a project to attach to now.
324
+
325
+ upsertSession({
326
+ id: sessionId,
327
+ project_id: projectId,
328
+ task: data.task || null,
329
+ status: data.status || 'unknown',
330
+ created_at: data.created_at,
331
+ updated_at: data.updated_at,
332
+ iterations: maxIteration,
333
+ duration_ms: durationMs,
334
+ approved: data.status === 'approved' ? 1 : 0,
335
+ commits: data.commits ? JSON.stringify(data.commits) : null,
336
+ stages_completed: JSON.stringify([...stagesSet]),
337
+ checkpoints: JSON.stringify(checkpoints),
338
+ llm_calls: data.llm_calls ? JSON.stringify(data.llm_calls) : null,
339
+ config_snapshot: data.config_snapshot ? JSON.stringify(data.config_snapshot) : null,
340
+ budget: data.budget ? JSON.stringify(data.budget) : null,
341
+ });
342
+
343
+ console.log(`[sync] Synced session ${sessionId}: status=${data.status}, iterations=${maxIteration}`);
344
+ publishEvent({ type: 'session', projectId, sessionId });
345
+ } catch (err) {
346
+ console.error(`[sync] Error syncing session file ${filePath}:`, err.message);
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Performs a full scan of all existing JSON files.
352
+ */
353
+ /**
354
+ * Sync a v2 plan file (with embedded HUs) to SQLite.
355
+ * Plans are the authoritative source — replaces hu-stories for plan-based runs.
356
+ */
357
+ export function syncPlanFile(filePath) {
358
+ try {
359
+ const raw = readFileSync(filePath, 'utf-8');
360
+ const data = JSON.parse(raw);
361
+ // KJC-BUG-0124 (issue #1277): a strict `version !== 2` silently hid
362
+ // every plan whose version drifted — the board showed 0 stories with no
363
+ // trace, and users concluded their work was lost. Tolerant now: any
364
+ // v2+ plan syncs, with a LOUD warn on version drift (fired before the
365
+ // empty check so drift is visible even on empty plans). Pre-v2 plans
366
+ // keep their historical silent skip (they never synced and carry no
367
+ // hus[]), and an empty v2 plan has nothing to sync — skipping it
368
+ // loses nothing.
369
+ if (typeof data.version !== 'number' || data.version < 2) return;
370
+ if (data.version > 2) {
371
+ console.warn(`[sync] ${filePath}: plan version ${data.version} (expected 2) — syncing anyway; re-save with current kj to normalize`);
372
+ }
373
+ if (!Array.isArray(data.hus) || data.hus.length === 0) return;
374
+
375
+ // Bug fix: pre-patch, `projectId = data.planId` — every plan was a separate
376
+ // "project" on the board, so 2 464 plans across the same repo produced
377
+ // 2 464 boxes in the UI instead of one. Now we derive the project from
378
+ // the stamped `projectDir` so every plan for the same source tree groups
379
+ // under a single board entry. Legacy plans without `projectDir` still
380
+ // fall back to `planId` (conservative — they at least continue to show
381
+ // up, just each on its own card).
382
+ const projectId = data.projectDir
383
+ ? deriveProjectIdFromDir(data.projectDir)
384
+ : data.planId;
385
+
386
+ if (skipIfTombstoned('plan', data.planId, filePath)) return;
387
+ // KJC-BUG-0055 (supersedes KJC-BUG-0050): gating por timestamp.
388
+ // El "fix" anterior quitaba el tombstone del proyecto en CUALQUIER
389
+ // llegada de plan — eso resucitaba proyectos borrados cuando un plan
390
+ // anterior reaparecía (DB nueva + plan-files viejos en disco) o
391
+ // cuando un fullScan posterior los re-encontraba. Comportamiento:
392
+ // • plan.updatedAt/createdAt > tombstone.deleted_at → el usuario
393
+ // ha generado el plan DESPUÉS de borrar el proyecto → revivir.
394
+ // • plan.updatedAt/createdAt <= tombstone.deleted_at → plan stale
395
+ // pre-existente al borrado → respetar el tombstone y borrar el
396
+ // fichero en disco para que no vuelva a re-importarse.
397
+ const projectTomb = getTombstone('project', projectId);
398
+ if (projectTomb) {
399
+ const planTime = data.updatedAt || data.createdAt || null;
400
+ const planTs = planTime ? Date.parse(planTime) : NaN;
401
+ const tombTs = Date.parse(projectTomb.deleted_at);
402
+ if (Number.isFinite(planTs) && planTs > tombTs) {
403
+ console.log(`[sync] Project ${projectId} resucitado: plan ${data.planId} es más reciente (${planTime}) que el tombstone (${projectTomb.deleted_at}).`);
404
+ removeTombstone('project', projectId);
405
+ } else {
406
+ console.log(`[sync] Plan ${data.planId} ignorado: proyecto ${projectId} tombstoned (${projectTomb.deleted_at}); borrando ${filePath}.`);
407
+ try { rmSync(filePath, { force: true }); } catch { /* best effort */ }
408
+ return;
409
+ }
410
+ }
411
+ // Human-friendly project name resolution order (most-specific first):
412
+ // 1. plan.name explicitly set by the user / planner ("Linux Assistant
413
+ // Orchestrator" — preserved across renames done from the board).
414
+ // 2. titleCaseBasename(projectDir) — derived from the cwd of the
415
+ // original kj plan invocation. NOTE: this is the SUBCARPETA of the
416
+ // projectDir, NOT necessarily where the work happens. If the user
417
+ // ran `kj plan` from inside `…/karajan-code/packages/hu-board/`
418
+ // with a SPEC about "Linux Assistant", the basename here would
419
+ // misleadingly say "Hu Board" — that's why plan.name wins.
420
+ // 3. slugToTitle of the task / projectId as a last resort.
421
+ const projectName = data.name
422
+ || (data.projectDir ? titleCaseBasename(basename(data.projectDir)) : null)
423
+ || slugToTitle(data.task || '')
424
+ || projectId;
425
+
426
+ // KJC-PRP-0002 PR3: stamp `is_shared` from the path (authoritative — works
427
+ // even for legacy plan files without the `shared:true` envelope marker).
428
+ // OR with existing value so a single shared plan promotes the project,
429
+ // while unsharing the LAST shared plan still leaves the flag set (we
430
+ // clear it lazily only when the user explicitly tombstones / re-creates).
431
+ upsertProject({
432
+ id: projectId,
433
+ name: projectName,
434
+ last_activity: data.updatedAt || data.createdAt || new Date().toISOString(),
435
+ total_stories: data.hus.length,
436
+ is_shared: isSharedPath(filePath) || data.shared === true ? 1 : undefined,
437
+ });
438
+
439
+ for (let i = 0; i < data.hus.length; i += 1) {
440
+ const hu = data.hus[i];
441
+ const acList = Array.isArray(hu.acceptance_criteria) ? hu.acceptance_criteria : [];
442
+ // Only count tests that carry real content. `kj plan` stamps a
443
+ // placeholder like `["npx vitest run ..."]` on every HU — those
444
+ // DO count as 1 test each. But empty strings / null entries
445
+ // (which show up in half-baked plans) must not inflate the
446
+ // badge to "🧪 1 test" on a HU that effectively has none.
447
+ // Drop empty entries but accept BOTH legacy shapes and the
448
+ // v2.7.5 structured form. The original anti-junk filter only
449
+ // knew the legacy shape (`name|title|description|given|gherkin|
450
+ // scope`) and silently nuked every test produced by the
451
+ // tests-synthesizer (which emits `{type, content, file?}`),
452
+ // leaving every HU flagged "missing test contract" on the
453
+ // board even though the plan JSON had 4 tests apiece.
454
+ const testList = (Array.isArray(hu.acceptance_tests) ? hu.acceptance_tests : [])
455
+ .filter((t) => {
456
+ if (!t) return false;
457
+ if (typeof t === 'string') return t.trim().length > 0;
458
+ if (typeof t !== 'object') return false;
459
+ // v2.7.5 structured form — primary shape going forward.
460
+ if (typeof t.content === 'string' && t.content.trim().length > 0) return true;
461
+ // Legacy shapes kept for back-compat with hand-edited plans.
462
+ return Boolean(t.name || t.title || t.description || t.given || t.gherkin || t.scope);
463
+ });
464
+ const blockedByList = Array.isArray(hu.blocked_by) ? hu.blocked_by : [];
465
+ const acText = acList.length > 0 ? JSON.stringify(acList) : null;
466
+
467
+ upsertStory({
468
+ id: `${projectId}::${hu.id}`,
469
+ project_id: projectId,
470
+ session_id: projectId,
471
+ status: hu.status || 'pending',
472
+ title: hu.title || hu.id,
473
+ original_text: hu.scope || null,
474
+ certified_as: null,
475
+ certified_want: hu.scope || null,
476
+ certified_so_that: null,
477
+ quality_total: null,
478
+ quality_d1: null, quality_d2: null, quality_d3: null,
479
+ quality_d4: null, quality_d5: null, quality_d6: null,
480
+ antipatterns: null,
481
+ ac_format: acText ? 'list' : null,
482
+ acceptance_criteria: acText,
483
+ created_at: hu.createdAt,
484
+ updated_at: hu.updatedAt,
485
+ certified_at: null,
486
+ // Stamp plan_id so the PATCH / mark-ready endpoints can locate
487
+ // the source plan JSON without walking the plans dir.
488
+ plan_id: data.planId,
489
+ // Denormalised counters + deps so the card renders "3 ACs ·
490
+ // 2 tests · waits for lao-001" without every client parsing JSON.
491
+ ac_count: acList.length,
492
+ test_count: testList.length,
493
+ blocked_by: blockedByList.length > 0 ? JSON.stringify(blockedByList) : null,
494
+ // Full test list goes to SQLite too so the modal can show
495
+ // "🧪 2 tests" next to the list. Pre-patch the card showed
496
+ // test_count but the modal had nothing to render.
497
+ acceptance_tests: testList.length > 0 ? JSON.stringify(testList) : null,
498
+ // Index in plan.hus[] — used by getStoriesByProject to order
499
+ // cards topologically (roots first, dependents last).
500
+ plan_order: i,
501
+ // Spec mapping (PR C): "implements §5.3" trace pointer.
502
+ spec_section: typeof hu.spec_section === 'string' && hu.spec_section.trim() ? hu.spec_section.trim() : null,
503
+ // PR3: per-HU outcome (iterations, duration, commits,
504
+ // blockers, summary). Stored as a JSON string in SQLite —
505
+ // the front parses on read. Null until hu-sub-pipeline
506
+ // stamps it at the end of each HU.
507
+ outcome: hu.outcome ? JSON.stringify(hu.outcome) : null,
508
+ // KJC-TSK-0394: result (pass|fail|partial|null) ortogonal al status.
509
+ // Distinto de `outcome` (blob JSON) — esto es el enum corto para
510
+ // badge ✓/✗ en la card.
511
+ result: hu.result || null,
512
+ // KJC-TSK-0406: coder_model y reviewer_model por HU. Reflejan
513
+ // qué modelo se va a usar (o se usó) en este HU. El modal del
514
+ // board los muestra y permite cambiarlos antes de run.
515
+ coder_model: hu.coder_model || null,
516
+ reviewer_model: hu.reviewer_model || null,
517
+ coder_provider: hu.coder_provider || null,
518
+ reviewer_provider: hu.reviewer_provider || null,
519
+ // KJC-PRP-0002 PR6: optional handle of the owner of this HU
520
+ // (free-form string). Only surfaced in UI when project is_shared,
521
+ // but stored unconditionally so values survive share/unshare flips.
522
+ assignee: typeof hu.assignee === 'string' && hu.assignee.trim() ? hu.assignee.trim() : null,
523
+ });
524
+ }
525
+
526
+ console.log(`[sync] Plan ${projectId}: ${data.hus.length} HUs (${projectName})`);
527
+ publishEvent({ type: 'plan', projectId, planId: data.planId });
528
+ } catch (err) {
529
+ console.error(`[sync] Failed to sync plan ${filePath}: ${err.message}`);
530
+ }
531
+ }
532
+
533
+ export function fullScan() {
534
+ const kjHome = getKjHome();
535
+ const storiesDir = join(kjHome, 'hu-stories');
536
+ const sessionsDir = join(kjHome, 'sessions');
537
+
538
+ // Clear existing data for a clean rebuild
539
+ const db = getDb();
540
+ db.exec('DELETE FROM context_requests');
541
+ db.exec('DELETE FROM stories');
542
+ db.exec('DELETE FROM sessions');
543
+ db.exec('DELETE FROM projects');
544
+
545
+ // KJC-BUG-0055: garbage-collect on-disk story/session artefacts whose
546
+ // ids carry an active tombstone. Without this, a tombstone is useless
547
+ // against a stray ~/.karajan/hu-stories/<id>/ that survived a manual
548
+ // DB wipe — the next pass would re-import it as a ghost project.
549
+ // (Plan dirs are NOT pre-purged here: a tombstoned project may hold a
550
+ // newer plan from a legit projectDir re-use; `syncPlanFile`'s
551
+ // timestamp gate decides per-file.)
552
+ for (const [type, root] of [['story', storiesDir], ['session', sessionsDir]]) {
553
+ if (!existsSync(root)) continue;
554
+ for (const name of readdirSync(root)) {
555
+ if (!isTombstoned(type, name)) continue;
556
+ try { rmSync(join(root, name), { recursive: true, force: true }); } catch { /* best effort */ }
557
+ }
558
+ }
559
+
560
+ // Scan stories
561
+ if (existsSync(storiesDir)) {
562
+ const dirs = readdirSync(storiesDir);
563
+ for (const dir of dirs) {
564
+ const batchPath = join(storiesDir, dir, 'batch.json');
565
+ if (existsSync(batchPath)) {
566
+ syncStoryFile(batchPath);
567
+ }
568
+ }
569
+ }
570
+
571
+ // Scan sessions
572
+ if (existsSync(sessionsDir)) {
573
+ const dirs = readdirSync(sessionsDir);
574
+ for (const dir of dirs) {
575
+ const sessionPath = join(sessionsDir, dir, 'session.json');
576
+ if (existsSync(sessionPath)) {
577
+ syncSessionFile(sessionPath);
578
+ }
579
+ }
580
+ }
581
+
582
+ // KJC-PRP-0002 PR5: order plans scans by conflict policy. `syncPlanFile`
583
+ // upserts by planId, so whichever pass runs LAST wins. Default
584
+ // shared-wins matches what teams expect (the committed copy is canonical);
585
+ // local-wins is the escape hatch for a solo dev still iterating before
586
+ // publishing. Same projectDir scan, only the order changes.
587
+ const scanPerUserPlans = () => {
588
+ const kjDir = process.env.KJ_PLANS_DIR || join(homedir(), '.karajan', 'plans');
589
+ const legacyKjDir = process.env.KJ_PLANS_DIR ? null : join(homedir(), '.kj', 'plans');
590
+ for (const root of [kjDir, legacyKjDir]) {
591
+ if (!root || !existsSync(root)) continue;
592
+ const projectDirs = readdirSync(root);
593
+ for (const projDir of projectDirs) {
594
+ const projPath = join(root, projDir);
595
+ try {
596
+ const files = readdirSync(projPath);
597
+ for (const file of files) {
598
+ if (file.startsWith('plan-') && file.endsWith('.json')) {
599
+ syncPlanFile(join(projPath, file));
600
+ }
601
+ }
602
+ } catch { /* skip */ }
603
+ }
604
+ }
605
+ };
606
+
607
+ if (getSharedConflictPolicy() === 'local-wins') {
608
+ scanSharedPlans();
609
+ scanPerUserPlans();
610
+ } else {
611
+ scanPerUserPlans();
612
+ scanSharedPlans();
613
+ }
614
+
615
+ console.log('[sync] Full scan completed');
616
+ }
617
+
618
+ /**
619
+ * Scans `<projectDir>/.karajan-shared/plans/` for every known projectDir
620
+ * and feeds each plan into `syncPlanFile`. Discovery sources:
621
+ * 1. `process.cwd()` — covers the canonical case of running `kj board start`
622
+ * from a project root with a freshly cloned `.karajan-shared/` and zero
623
+ * local plans yet.
624
+ * 2. `KJ_SHARED_PROJECT_DIRS` env var (colon-separated paths) — escape
625
+ * hatch for users running the board outside the project root or
626
+ * tracking several repos with one board instance.
627
+ */
628
+ function scanSharedPlans() {
629
+ const dirs = new Set([process.cwd()]);
630
+ const extra = process.env.KJ_SHARED_PROJECT_DIRS;
631
+ if (extra) {
632
+ for (const p of extra.split(':')) if (p) dirs.add(p);
633
+ }
634
+ for (const projectDir of dirs) {
635
+ const sharedDir = getSharedPlansDir(projectDir);
636
+ if (!existsSync(sharedDir)) continue;
637
+ try {
638
+ const files = readdirSync(sharedDir);
639
+ for (const file of files) {
640
+ if (file.startsWith('plan-') && file.endsWith('.json')) {
641
+ syncPlanFile(join(sharedDir, file));
642
+ }
643
+ }
644
+ } catch { /* skip */ }
645
+ }
646
+ }
647
+
648
+ /**
649
+ * Starts the file watcher for live sync.
650
+ * @returns {import('chokidar').FSWatcher}
651
+ */
652
+ export function startWatcher() {
653
+ const kjHome = getKjHome();
654
+ const storiesGlob = join(kjHome, 'hu-stories', '*', 'batch.json');
655
+ const sessionsGlob = join(kjHome, 'sessions', '*', 'session.json');
656
+ const promptsGlob = join(kjHome, 'prompts', '*.json');
657
+ // Plans live under KJ_PLANS_DIR or `~/.karajan/plans/` after
658
+ // KJC-PCS-0047 PR 3 consolidated the home directories. Without this
659
+ // glob, HU statuses flipped to coding/done by `kj run --plan` never
660
+ // reach the board until the user clicks 🔄 manually. The legacy
661
+ // `~/.kj/plans/` root is also watched so users running the board
662
+ // before their first post-upgrade `kj` command (the trigger of the
663
+ // auto-migrator) keep seeing live updates.
664
+ const plansRoot = process.env.KJ_PLANS_DIR || join(homedir(), '.karajan', 'plans');
665
+ const legacyPlansRoot = process.env.KJ_PLANS_DIR ? null : join(homedir(), '.kj', 'plans');
666
+ const plansGlob = join(plansRoot, '*', 'plan-*.json');
667
+ const legacyPlansGlob = legacyPlansRoot ? join(legacyPlansRoot, '*', 'plan-*.json') : null;
668
+
669
+ const watchTargets = [storiesGlob, sessionsGlob, plansGlob, promptsGlob];
670
+ if (legacyPlansGlob) watchTargets.push(legacyPlansGlob);
671
+ // KJC-PRP-0002 PR2: live-watch shared plans for the board's cwd + any
672
+ // dir listed in KJ_SHARED_PROJECT_DIRS. Without this, `kj plan share`
673
+ // updates would only land via the next fullScan / manual 🔄.
674
+ const sharedDirs = new Set([process.cwd()]);
675
+ const extraShared = process.env.KJ_SHARED_PROJECT_DIRS;
676
+ if (extraShared) {
677
+ for (const p of extraShared.split(':')) if (p) sharedDirs.add(p);
678
+ }
679
+ for (const d of sharedDirs) {
680
+ watchTargets.push(join(d, SHARED_DIR_NAME, 'plans', 'plan-*.json'));
681
+ }
682
+ const watcher = watch(watchTargets, {
683
+ ignoreInitial: true,
684
+ awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 },
685
+ });
686
+
687
+ const syncByPath = (p, eventName) => {
688
+ if (p.includes('hu-stories')) syncStoryFile(p);
689
+ else if (p.includes('sessions')) syncSessionFile(p);
690
+ else if (p.includes(`${plansRoot}${plansRoot.endsWith('/') ? '' : '/'}`) || p.includes('/plans/')) {
691
+ // Catches both per-user `~/.karajan/plans/<slug>/plan-*.json` and
692
+ // team-shared `<projectDir>/.karajan-shared/plans/plan-*.json` — the
693
+ // path-segment match `/plans/` is the same for both.
694
+ syncPlanFile(p);
695
+ } else if (p.includes('/prompts/')) {
696
+ // Prompt files are RPC packets the runner writes when it needs
697
+ // an answer (Solomon escalation, etc.) — see board-prompt-bridge.js.
698
+ // The board's API exposes a /api/prompts list; here we just emit
699
+ // a bus event so connected SSE clients pop the modal immediately
700
+ // instead of waiting on the next render cycle.
701
+ if (p.endsWith('.answer.json')) return; // ignore our own writes
702
+ try {
703
+ const raw = readFileSync(p, 'utf-8');
704
+ const data = JSON.parse(raw);
705
+ publishEvent({ type: 'prompt', event: eventName, promptId: data.promptId, sessionId: data.sessionId, question: data.question });
706
+ } catch { /* malformed prompt file — ignore */ }
707
+ }
708
+ };
709
+
710
+ watcher.on('add', (p) => {
711
+ console.log(`[watcher] New file: ${p}`);
712
+ syncByPath(p, 'add');
713
+ });
714
+
715
+ watcher.on('change', (p) => {
716
+ console.log(`[watcher] Changed: ${p}`);
717
+ syncByPath(p, 'change');
718
+ });
719
+
720
+ // The runner deletes prompt files once the answer arrives. Surface
721
+ // those deletions as `prompt:resolved` events so already-open modals
722
+ // can close themselves without polling.
723
+ watcher.on('unlink', (p) => {
724
+ if (p.includes('/prompts/') && p.endsWith('.json') && !p.endsWith('.answer.json')) {
725
+ const m = /([^/]+)\.json$/.exec(p);
726
+ if (m) publishEvent({ type: 'prompt-resolved', promptId: m[1] });
727
+ }
728
+ });
729
+
730
+ console.log(`[watcher] Watching ${kjHome} + ${plansRoot} for changes`);
731
+ return watcher;
732
+ }