karajan-code 4.70.4 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (390) hide show
  1. package/README.md +2 -2
  2. package/package.json +6 -14
  3. package/packages/hu-board/package.json +1 -1
  4. package/packages/hu-board/public/app.js +4 -18
  5. package/packages/hu-board/public/index.html +5 -2
  6. package/packages/hu-board/public/styles.css +26 -0
  7. package/packages/hu-board/public/utils/api.js +0 -74
  8. package/packages/hu-board/public/utils/board-tour.js +62 -0
  9. package/packages/hu-board/public/utils/board-view.js +49 -144
  10. package/packages/hu-board/public/utils/command-launcher.js +8 -70
  11. package/packages/hu-board/public/utils/formatters.js +10 -0
  12. package/packages/hu-board/public/utils/governance-view.js +13 -1
  13. package/packages/hu-board/public/utils/hu-actions.js +17 -68
  14. package/packages/hu-board/public/utils/init-listeners.js +7 -8
  15. package/packages/hu-board/public/utils/log-panel.js +3 -16
  16. package/packages/hu-board/public/utils/maggle-mode.js +2 -12
  17. package/packages/hu-board/public/utils/modals.js +7 -0
  18. package/packages/hu-board/public/utils/project-picker-view.js +1 -1
  19. package/packages/hu-board/public/utils/server-push.js +3 -22
  20. package/packages/hu-board/public/utils/shared-board-notice.js +33 -0
  21. package/packages/hu-board/public/utils/story-detail-view.js +5 -19
  22. package/packages/hu-board/src/cleanup-zombies.js +0 -1
  23. package/packages/hu-board/src/command-runner.js +36 -25
  24. package/packages/hu-board/src/db-connection.js +282 -0
  25. package/packages/hu-board/src/db-paths.js +113 -0
  26. package/packages/hu-board/src/db-queries.js +188 -0
  27. package/packages/hu-board/src/db-tombstones.js +51 -0
  28. package/packages/hu-board/src/db.js +28 -619
  29. package/packages/hu-board/src/ephemeral-cleaner.js +2 -3
  30. package/packages/hu-board/src/hu-zombie-reaper.js +5 -1
  31. package/packages/hu-board/src/plan-mutations.js +3 -153
  32. package/packages/hu-board/src/plans-service.js +55 -0
  33. package/packages/hu-board/src/routes/api/commands.js +122 -0
  34. package/packages/hu-board/src/routes/api/plans.js +55 -0
  35. package/packages/hu-board/src/routes/api/projects.js +267 -0
  36. package/packages/hu-board/src/routes/api/prompts.js +135 -0
  37. package/packages/hu-board/src/routes/api/sessions.js +95 -0
  38. package/packages/hu-board/src/routes/api/stories.js +183 -0
  39. package/packages/hu-board/src/routes/api/system.js +241 -0
  40. package/packages/hu-board/src/routes/api/tombstones.js +38 -0
  41. package/packages/hu-board/src/routes/api.js +24 -1371
  42. package/packages/hu-board/src/routes/governance.js +23 -1
  43. package/packages/hu-board/src/routes/lanes.js +40 -0
  44. package/packages/hu-board/src/routes/rag.js +4 -3
  45. package/packages/hu-board/src/server.js +20 -34
  46. package/packages/hu-board/src/sync-files.js +318 -0
  47. package/packages/hu-board/src/sync-project.js +99 -0
  48. package/packages/hu-board/src/sync.js +22 -566
  49. package/packages/hu-board/src/zombie-reaper.js +1 -1
  50. package/scripts/dual-publish.mjs +19 -3
  51. package/scripts/postinstall.js +15 -2
  52. package/scripts/verify-pack-mcp.mjs +11 -7
  53. package/scripts/verify-pack.mjs +4 -4
  54. package/src/audit/audit-history.js +3 -2
  55. package/src/audit/dead-exports.js +68 -59
  56. package/src/audit/deterministic-summary.js +33 -13
  57. package/src/audit/npm-signatures.js +41 -0
  58. package/src/audit/sbom.js +35 -0
  59. package/src/checks/agent-token.js +112 -0
  60. package/src/checks/dir-setup.js +34 -1
  61. package/src/checks/native-build.js +21 -39
  62. package/src/checks/runner.js +128 -142
  63. package/src/cli/advanced-commands.js +6 -6
  64. package/src/cli/register-meta.js +42 -82
  65. package/src/cli/register-pipeline.js +8 -143
  66. package/src/cli/register-plan.js +9 -100
  67. package/src/cli/register-roles-skills.js +1 -1
  68. package/src/cli/retired-commands.js +49 -0
  69. package/src/cli.js +18 -5
  70. package/src/commands/agent-run.js +40 -26
  71. package/src/commands/audit.js +127 -124
  72. package/src/commands/board.js +5 -9
  73. package/src/commands/check.js +29 -16
  74. package/src/commands/doctor.js +2 -0
  75. package/src/commands/env.js +176 -150
  76. package/src/commands/go.js +71 -53
  77. package/src/commands/harden.js +90 -81
  78. package/src/commands/hu-start.js +52 -0
  79. package/src/commands/hu.js +288 -221
  80. package/src/commands/identity.js +111 -78
  81. package/src/commands/init-atril.js +47 -0
  82. package/src/commands/init.js +11 -40
  83. package/src/commands/install-tools.js +70 -85
  84. package/src/commands/main-guard.js +78 -0
  85. package/src/commands/policy.js +226 -184
  86. package/src/commands/qmd.js +27 -21
  87. package/src/commands/rag.js +121 -55
  88. package/src/commands/review-gate.js +421 -341
  89. package/src/commands/skills.js +4 -1
  90. package/src/commands/start.js +4 -5
  91. package/src/commands/steward.js +96 -47
  92. package/src/commands/tournament.js +46 -37
  93. package/src/commands/worktree.js +107 -26
  94. package/src/config/defaults.js +1 -0
  95. package/src/config/loader.js +34 -17
  96. package/src/config/role-resolver.js +1 -0
  97. package/src/config/schema.js +1 -0
  98. package/src/config/secrets.js +36 -0
  99. package/src/config.js +1 -0
  100. package/src/environment/adr.js +16 -2
  101. package/src/git/karajan-label.js +20 -8
  102. package/src/guards/main-guard.js +54 -0
  103. package/src/harden/hook-templates.js +16 -1
  104. package/src/harden/sentinel/posttooluse.mjs +18 -6
  105. package/src/harden/sentinel/pretooluse-sentinel.mjs +64 -36
  106. package/src/harden/sentinel/sentinel-lib.mjs +129 -26
  107. package/src/harden/sentinel/sentinel-shell.mjs +5 -16
  108. package/src/harden/sentinel/sessionstart.mjs +3 -2
  109. package/src/harden/sentinel/stop.mjs +5 -4
  110. package/src/harden/workflow-templates.js +1 -1
  111. package/src/hu/board-lookup.js +29 -0
  112. package/src/hu/gh-identity.js +23 -0
  113. package/src/hu/github-board.js +4 -2
  114. package/src/hu/lane-prune.js +83 -13
  115. package/src/hu/lane-setup.js +65 -0
  116. package/src/hu/lanes.js +66 -0
  117. package/src/hu/team-board-mirror.js +3 -2
  118. package/src/infrastructure/command-runner.js +1 -1
  119. package/src/infrastructure/file-system-service.js +1 -1
  120. package/src/mcp/handlers/direct-handlers.js +2 -292
  121. package/src/mcp/handlers/hu-handlers.js +1 -35
  122. package/src/mcp/handlers/management-handlers.js +2 -42
  123. package/src/mcp/handlers/rag-handler.js +9 -1
  124. package/src/mcp/server-handlers.js +16 -22
  125. package/src/mcp/tools.js +1 -196
  126. package/src/plan/lane-plans.js +54 -0
  127. package/src/plan/plan-store.js +7 -4
  128. package/src/policy/add.js +26 -12
  129. package/src/policy/adr-number-check.js +31 -0
  130. package/src/prompts/audit.js +299 -339
  131. package/src/prompts/card-context.js +10 -5
  132. package/src/prompts/reviewer.js +5 -2
  133. package/src/rag/auto-update.js +71 -44
  134. package/src/rag/bootstrap-index.js +46 -54
  135. package/src/rag/indexer.js +9 -4
  136. package/src/rag/migrate.js +1 -1
  137. package/src/rag/onnx-fallback.js +2 -2
  138. package/src/rag/pid-lease.js +181 -0
  139. package/src/rag/project-store.js +2 -12
  140. package/src/review/card-first.js +57 -46
  141. package/src/review/gate-gitignore.js +23 -4
  142. package/src/review/gate-hook.js +12 -4
  143. package/src/review/one-shot-review.js +45 -35
  144. package/src/review/publish-pr.js +14 -2
  145. package/src/review/repo-docs.js +73 -0
  146. package/src/review/repo-stack.js +59 -0
  147. package/src/review/reviewer-fallback.js +19 -4
  148. package/src/review/seal-commit.js +29 -0
  149. package/src/review/sonar-pregate.js +59 -44
  150. package/src/review/third-party-review.js +90 -16
  151. package/src/review/third-party-size.js +43 -0
  152. package/src/review/third-party-skills.js +54 -0
  153. package/src/review/verify-pr.js +1 -24
  154. package/src/roles/audit-role.js +106 -125
  155. package/src/skills/atril-cli.js +19 -1
  156. package/src/skills/atril-skills.js +5 -4
  157. package/src/skills/skills-search.js +72 -0
  158. package/src/skills/translate.js +62 -0
  159. package/src/sonar/scanner.js +116 -101
  160. package/src/steward/invariants.js +5 -4
  161. package/src/steward/phantom-sweep.js +66 -0
  162. package/src/tournament/judge.js +60 -53
  163. package/src/utils/gh-account.js +37 -0
  164. package/src/utils/git.js +0 -13
  165. package/src/utils/home-migration.js +7 -1
  166. package/src/utils/main-checkout.js +53 -0
  167. package/src/utils/qmd-collection.js +3 -1
  168. package/src/utils/welcome.js +3 -3
  169. package/templates/{skills → atril/commands}/kj-architect.md +5 -0
  170. package/templates/{skills → atril/commands}/kj-audit.md +5 -0
  171. package/templates/{skills → atril/commands}/kj-board.md +5 -0
  172. package/templates/{skills → atril/commands}/kj-code.md +5 -0
  173. package/templates/{skills → atril/commands}/kj-csv-transform.md +5 -0
  174. package/templates/{skills → atril/commands}/kj-data-report.md +5 -0
  175. package/templates/{skills → atril/commands}/kj-discover.md +5 -0
  176. package/templates/{skills → atril/commands}/kj-rag-query.md +5 -0
  177. package/templates/{skills → atril/commands}/kj-review.md +5 -0
  178. package/templates/{skills → atril/commands}/kj-security.md +5 -0
  179. package/templates/{skills → atril/commands}/kj-sonar.md +5 -0
  180. package/templates/{skills → atril/commands}/kj-sql-analysis.md +5 -0
  181. package/templates/{skills → atril/commands}/kj-test.md +5 -0
  182. package/packages/hu-board/public/utils/run-launcher.js +0 -169
  183. package/packages/hu-board/src/run-tracker.js +0 -121
  184. package/src/activity-log.js +0 -59
  185. package/src/agents/host-agent.js +0 -38
  186. package/src/agents/model-registry.js +0 -142
  187. package/src/audit/plan-adherence.js +0 -224
  188. package/src/autonomy/arbiter-role.js +0 -71
  189. package/src/autonomy/arbiter.js +0 -22
  190. package/src/autonomy/decider.js +0 -18
  191. package/src/autonomy/decision-resolver.js +0 -58
  192. package/src/autonomy/outcome-report.js +0 -55
  193. package/src/autonomy/stage-ask.js +0 -30
  194. package/src/brain/decision-tracker.js +0 -125
  195. package/src/brain/decisor.js +0 -243
  196. package/src/brain/solomon-consult.js +0 -171
  197. package/src/brain/standby-scheduler.js +0 -8
  198. package/src/budget/comparison.js +0 -96
  199. package/src/canvas/parse-md.js +0 -238
  200. package/src/canvas/schema.js +0 -101
  201. package/src/canvas/to-flat.js +0 -85
  202. package/src/canvas/validate.js +0 -163
  203. package/src/ci/dispatch.js +0 -99
  204. package/src/ci/index.js +0 -3
  205. package/src/ci/pr-diff.js +0 -26
  206. package/src/cli/register-standby.js +0 -26
  207. package/src/commands/architect.js +0 -111
  208. package/src/commands/autorun.js +0 -49
  209. package/src/commands/researcher.js +0 -54
  210. package/src/commands/resume.js +0 -72
  211. package/src/commands/review.js +0 -89
  212. package/src/commands/run.js +0 -185
  213. package/src/commands/standby.js +0 -117
  214. package/src/commands/status.js +0 -36
  215. package/src/commands/triage.js +0 -78
  216. package/src/commands/undo.js +0 -62
  217. package/src/domains/domain-loader.js +0 -245
  218. package/src/domains/domain-registry.js +0 -140
  219. package/src/domains/domain-synthesizer.js +0 -129
  220. package/src/git/automation.js +0 -433
  221. package/src/git/coder-commit-guard.js +0 -40
  222. package/src/git/hu-automation.js +0 -243
  223. package/src/git/hu-snapshot.js +0 -10
  224. package/src/git/symlink-targets.js +0 -57
  225. package/src/golden/asserter.js +0 -92
  226. package/src/golden/loader.js +0 -52
  227. package/src/golden/runner.js +0 -92
  228. package/src/golden/schema.js +0 -41
  229. package/src/guards/duplicate-members.js +0 -86
  230. package/src/guards/intent-guard.js +0 -161
  231. package/src/guards/perf-guard.js +0 -126
  232. package/src/guards/policy-guard.js +0 -90
  233. package/src/guards/policy-resolver.js +0 -85
  234. package/src/guards/secret-redactor.js +0 -71
  235. package/src/hu/acceptance-runner.js +0 -138
  236. package/src/hu/lazy-planner.js +0 -109
  237. package/src/hu/parallel-executor.js +0 -94
  238. package/src/hu/routing-metrics.js +0 -17
  239. package/src/hu/split-validator.js +0 -143
  240. package/src/hu/splitting-detector.js +0 -158
  241. package/src/hu/splitting-generator.js +0 -175
  242. package/src/hu/store.js +0 -394
  243. package/src/infrastructure/mocks.js +0 -182
  244. package/src/mcp/handlers/run-handler.js +0 -318
  245. package/src/mcp/sovereignty-guard.js +0 -113
  246. package/src/mcp/tool-arg-normalizers.js +0 -16
  247. package/src/mutate/reviewer-signal.js +0 -76
  248. package/src/orchestrator/agent-fallback.js +0 -81
  249. package/src/orchestrator/brain-coordinator.js +0 -186
  250. package/src/orchestrator/ci-integration.js +0 -139
  251. package/src/orchestrator/config-init.js +0 -426
  252. package/src/orchestrator/direct-actions.js +0 -217
  253. package/src/orchestrator/drivers/error-recovery.js +0 -287
  254. package/src/orchestrator/drivers/init-context.js +0 -238
  255. package/src/orchestrator/drivers/iteration-loop.js +0 -382
  256. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +0 -69
  257. package/src/orchestrator/drivers/iteration-phases/guards.js +0 -91
  258. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +0 -29
  259. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +0 -125
  260. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +0 -62
  261. package/src/orchestrator/drivers/post-loop.js +0 -404
  262. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +0 -137
  263. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +0 -47
  264. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +0 -84
  265. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +0 -393
  266. package/src/orchestrator/drivers/pre-loop.js +0 -524
  267. package/src/orchestrator/drivers/run-hu-batch.js +0 -468
  268. package/src/orchestrator/feedback-enrichment.js +0 -140
  269. package/src/orchestrator/feedback-queue.js +0 -151
  270. package/src/orchestrator/flow-control.js +0 -170
  271. package/src/orchestrator/flow-runner.js +0 -365
  272. package/src/orchestrator/fs-leak-detector.js +0 -228
  273. package/src/orchestrator/hu-scheduler.js +0 -114
  274. package/src/orchestrator/hu-sub-pipeline.js +0 -642
  275. package/src/orchestrator/iteration-gate.js +0 -73
  276. package/src/orchestrator/iteration-stages.js +0 -9
  277. package/src/orchestrator/parallel-limiter.js +0 -74
  278. package/src/orchestrator/pipeline-context.js +0 -72
  279. package/src/orchestrator/post-loop-stages.js +0 -468
  280. package/src/orchestrator/pre-loop-stages.js +0 -11
  281. package/src/orchestrator/preflight-checks.js +0 -487
  282. package/src/orchestrator/repair/repair-detector.js +0 -117
  283. package/src/orchestrator/repair/repair-runner.js +0 -142
  284. package/src/orchestrator/reviewer-fallback.js +0 -45
  285. package/src/orchestrator/role-output-compressor.js +0 -189
  286. package/src/orchestrator/session-journal.js +0 -308
  287. package/src/orchestrator/solomon-escalation.js +0 -331
  288. package/src/orchestrator/solomon-rules.js +0 -212
  289. package/src/orchestrator/stages/acceptance-coverage-stage.js +0 -47
  290. package/src/orchestrator/stages/architect-stage.js +0 -172
  291. package/src/orchestrator/stages/coder-stage.js +0 -506
  292. package/src/orchestrator/stages/domain-curator-stage.js +0 -82
  293. package/src/orchestrator/stages/format-stage.js +0 -61
  294. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +0 -127
  295. package/src/orchestrator/stages/hu-reviewer-stage.js +0 -419
  296. package/src/orchestrator/stages/perf-stage.js +0 -140
  297. package/src/orchestrator/stages/planner-stage.js +0 -85
  298. package/src/orchestrator/stages/rag-context-stage.js +0 -80
  299. package/src/orchestrator/stages/rag-preload-decisor.js +0 -58
  300. package/src/orchestrator/stages/research-stage.js +0 -69
  301. package/src/orchestrator/stages/reviewer-stage.js +0 -431
  302. package/src/orchestrator/stages/sonar-stage.js +0 -439
  303. package/src/orchestrator/stages/stage-classes.js +0 -84
  304. package/src/orchestrator/stages/stage-executor.js +0 -155
  305. package/src/orchestrator/stages/suite-green-stage.js +0 -62
  306. package/src/orchestrator/stages/tdd-discipline-stage.js +0 -56
  307. package/src/orchestrator/stages/tool-judge-stage.js +0 -53
  308. package/src/orchestrator/stages/triage-stage.js +0 -172
  309. package/src/orchestrator/standby.js +0 -71
  310. package/src/orchestrator/verification-gate.js +0 -183
  311. package/src/orchestrator.js +0 -22
  312. package/src/plan/adr-generator.js +0 -170
  313. package/src/plan/adr-loader.js +0 -60
  314. package/src/plan/plan-executor.js +0 -100
  315. package/src/plan/plan-validation.js +0 -4
  316. package/src/prompts/acceptance-coverage.js +0 -32
  317. package/src/prompts/arbiter.js +0 -38
  318. package/src/prompts/architect.js +0 -103
  319. package/src/prompts/hu-reviewer.js +0 -244
  320. package/src/prompts/tool-correctness-judge.js +0 -31
  321. package/src/prompts/triage.js +0 -57
  322. package/src/rag/embedder.js +0 -2
  323. package/src/rag/embedders/_cloud-base.js +0 -2
  324. package/src/rag/embedders/cohere.js +0 -2
  325. package/src/rag/embedders/mistral.js +0 -2
  326. package/src/rag/embedders/onnx.js +0 -2
  327. package/src/rag/embedders/openai.js +0 -2
  328. package/src/rag/embedders/voyage.js +0 -2
  329. package/src/rag/rerank.js +0 -4
  330. package/src/rag/where-parser.js +0 -4
  331. package/src/repeat-detector.js +0 -88
  332. package/src/review/diff-generator.js +0 -112
  333. package/src/review/git-stash-helper.js +0 -104
  334. package/src/review/schema.js +0 -31
  335. package/src/review/scope-filter.js +0 -152
  336. package/src/review/snapshot-diff.js +0 -122
  337. package/src/review/tdd-discipline.js +0 -90
  338. package/src/review/tdd-policy.js +0 -69
  339. package/src/roles/acceptance-coverage-role.js +0 -78
  340. package/src/roles/architect-role.js +0 -77
  341. package/src/roles/coder-role.js +0 -113
  342. package/src/roles/commiter-role.js +0 -113
  343. package/src/roles/domain-curator-role.js +0 -134
  344. package/src/roles/hu-reviewer-role.js +0 -64
  345. package/src/roles/impeccable-role.js +0 -85
  346. package/src/roles/index.js +0 -18
  347. package/src/roles/karajan-brain-role.js +0 -134
  348. package/src/roles/perf-role.js +0 -122
  349. package/src/roles/planner-role.js +0 -151
  350. package/src/roles/refactorer-role.js +0 -30
  351. package/src/roles/repairer-role.js +0 -181
  352. package/src/roles/researcher-role.js +0 -70
  353. package/src/roles/reviewer-role.js +0 -115
  354. package/src/roles/security-role.js +0 -73
  355. package/src/roles/solomon-role.js +0 -271
  356. package/src/roles/sonar-role.js +0 -72
  357. package/src/roles/tester-role.js +0 -235
  358. package/src/roles/tool-correctness-judge-role.js +0 -78
  359. package/src/roles/triage-role.js +0 -99
  360. package/src/session/journal/decisions-writer.js +0 -200
  361. package/src/session/journal/iteration-logger.js +0 -194
  362. package/src/session/journal/summary-writer.js +0 -320
  363. package/src/session/journal/tree-writer.js +0 -164
  364. package/src/session/journal/write-all.js +0 -89
  365. package/src/session-cleanup.js +0 -116
  366. package/src/skills/semantic-detector.js +0 -167
  367. package/src/skills/skill-detector.js +0 -385
  368. package/src/sonar/cloud-scanner.js +0 -78
  369. package/src/tracker-bootstrap.js +0 -44
  370. package/src/types/index.js +0 -38
  371. package/src/types/policy.js +0 -43
  372. package/src/types/solomon.js +0 -87
  373. package/src/utils/async-lock.js +0 -31
  374. package/src/utils/budget.js +0 -229
  375. package/src/utils/cwd-confirm.js +0 -39
  376. package/src/utils/display/event-handlers.js +0 -403
  377. package/src/utils/display/formatters.js +0 -132
  378. package/src/utils/display/header.js +0 -47
  379. package/src/utils/display/session-summary.js +0 -118
  380. package/src/utils/display/solomon.js +0 -42
  381. package/src/utils/pricing.js +0 -29
  382. package/src/utils/quota-simulator.js +0 -56
  383. package/src/utils/role-assigner.js +0 -137
  384. package/src/utils/rtk-wrapper.js +0 -87
  385. package/src/utils/run-registry.js +0 -9
  386. package/src/utils/status-dashboard.js +0 -254
  387. package/src/utils/tool-call-extractor.js +0 -56
  388. package/src/webperf/devtools-detect.js +0 -112
  389. package/src/webperf/webperf-role-integration.js +0 -66
  390. package/templates/skills/kj-run.md +0 -73
@@ -1,426 +0,0 @@
1
- /**
2
- * Configuration initialization and pipeline setup helpers.
3
- * Extracted from orchestrator.js — pure functions, no orchestration state.
4
- */
5
- import fs from "node:fs/promises";
6
- import path from "node:path";
7
- import { computeBaseRef, setSnapshot } from "../review/diff-generator.js";
8
- import { ensureContractBlockPresent, excludeLocalArtifacts } from "../review/gate-gitignore.js";
9
- import { revParse } from "../utils/git.js";
10
- import { buildCoderPrompt } from "../prompts/coder.js";
11
- import { buildReviewerPrompt } from "../prompts/reviewer.js";
12
- import { resolveRole } from "../config.js";
13
- import { emitProgress, makeEvent } from "../utils/events.js";
14
- import { getTemplatesRoot } from "../utils/templates-root.js";
15
- import { BudgetTracker, extractUsageMetrics } from "../utils/budget.js";
16
- import { DEFAULTS } from "../config/defaults.js";
17
- import { computeKjComparison } from "../budget/comparison.js";
18
- import { resolveRoleMdPath, loadFirstExisting } from "../roles/base-role.js";
19
- import { projectSlug } from "../plan/plan-store.js";
20
- import { applyPolicies } from "../guards/policy-resolver.js";
21
- import { resolveReviewProfile } from "../review/profiles.js";
22
- import { createSession } from "../session/store.js";
23
- import { setResolvedPolicies } from "../session/mutators.js";
24
- import { exists, ensureDir } from "../utils/fs.js";
25
-
26
- /**
27
- * Auto-initialize .karajan/ in projectDir if missing.
28
- * Copies essential templates (coder-rules, review-rules) without running the full wizard.
29
- * Called by the orchestrator before the pipeline starts.
30
- */
31
- export async function autoInit(projectDir, logger) {
32
- // Ensure git repo exists — without git, diff/reviewer/commit won't work.
33
- //
34
- // KJC-BUG-0060: the previous guard `!exists(projectDir/.git)` was insufficient
35
- // when projectDir is a subdirectory inside an existing repo (common in dogfooding
36
- // scenarios where kj-linked runs from the karajan-code source tree). In that case
37
- // `git commit --allow-empty` would resolve upward to the parent repo and land an
38
- // empty "initial commit" on the user's main branch. Use `git rev-parse
39
- // --is-inside-work-tree` instead — it follows the same upward traversal that git
40
- // does naturally, so a subdir inside a repo correctly reports `true` and we don't
41
- // touch anything. We also drop the `git commit --allow-empty -m "initial commit"`
42
- // step: a root commit is not required for any downstream stage (diff, review,
43
- // coder, sonar) and the empty-commit zombie history was the user-visible symptom.
44
- const { execFileSync } = await import("node:child_process");
45
- try {
46
- execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: projectDir, stdio: "pipe" });
47
- // Already inside a git work tree (own .git or parent's). Nothing to do.
48
- } catch {
49
- // Not inside any repo — create a fresh one. No empty seed commit.
50
- try {
51
- execFileSync("git", ["init"], { cwd: projectDir, stdio: "pipe" });
52
- logger.info("Initialized git repository (no seed commit)");
53
- } catch (err) {
54
- logger.warn(`Failed to init git repo: ${err.message}`);
55
- }
56
- }
57
-
58
- // KJC-BUG-0213 (issue #1734): el .gitignore es del equipo y viaja con el repo.
59
- // Ahi solo entra el contrato v4, que el equipo entero hereda; los artefactos
60
- // de una ejecucion son nuestros y locales, asi que van a .git/info/exclude.
61
- // Lo que kj metia antes (.env, *.log, .DS_Store) era opinion sobre un
62
- // proyecto ajeno, y aparecia en el diff de otra persona sin haberlo pedido.
63
- try {
64
- const contract = await ensureContractBlockPresent(projectDir);
65
- if (contract.changed) logger.info("El contrato v4 se ha anadido a .gitignore (lo hereda quien clone)");
66
- const local = await excludeLocalArtifacts(projectDir);
67
- if (local.changed) logger.info(`Artefactos de kj excluidos en .git/info/exclude: ${local.added.join(", ")}`);
68
- } catch (err) {
69
- logger.warn(`Failed to prepare .gitignore: ${err.message}`);
70
- }
71
-
72
- const karajanDir = path.join(projectDir, ".karajan");
73
- if (await exists(karajanDir)) return;
74
-
75
- logger.info("No .karajan/ found — auto-initializing project scaffolding");
76
- await ensureDir(karajanDir);
77
-
78
- const templatesDir = getTemplatesRoot();
79
-
80
- const filesToCopy = [
81
- { src: "coder-rules.md", dest: "coder-rules.md" },
82
- { src: "review-rules.md", dest: "review-rules.md" }
83
- ];
84
-
85
- for (const { src, dest } of filesToCopy) {
86
- const srcPath = path.join(templatesDir, src);
87
- const destPath = path.join(karajanDir, dest);
88
- try {
89
- if (!(await exists(destPath))) {
90
- const content = await fs.readFile(srcPath, "utf8");
91
- await fs.writeFile(destPath, content, "utf8");
92
- logger.info(` Created .karajan/${dest}`);
93
- }
94
- } catch (err) {
95
- logger.warn(` Failed to copy ${src}: ${err.message}`);
96
- }
97
- }
98
-
99
- // Copy role templates directory
100
- const rolesTemplateDir = path.join(templatesDir, "roles");
101
- const rolesDestDir = path.join(karajanDir, "roles");
102
- try {
103
- if (await exists(rolesTemplateDir)) {
104
- await ensureDir(rolesDestDir);
105
- const roleFiles = await fs.readdir(rolesTemplateDir);
106
- for (const rf of roleFiles) {
107
- if (!rf.endsWith(".md")) continue;
108
- const destFile = path.join(rolesDestDir, rf);
109
- if (!(await exists(destFile))) {
110
- await fs.copyFile(path.join(rolesTemplateDir, rf), destFile);
111
- }
112
- }
113
- logger.info(` Copied ${roleFiles.filter(f => f.endsWith(".md")).length} role templates to .karajan/roles/`);
114
- }
115
- } catch (err) {
116
- logger.warn(` Failed to copy role templates: ${err.message}`);
117
- }
118
- }
119
-
120
- // KJC-BUG-0213 (issue #1734): aqui vivia updateGitignoreForStack, que deducia
121
- // el stack buscando palabras como "ts" o "js" en el texto del plan y de la
122
- // tarea. Cualquier frase las contiene, asi que un proyecto Laravel acabo con
123
- // reglas de TypeScript en SU .gitignore. Un acierto por casualidad no vale para
124
- // escribir en un fichero que no es nuestro: la funcion se retira entera.
125
-
126
- /**
127
- * Load product context from well-known file locations.
128
- */
129
- export async function loadProductContext(projectDir) {
130
- const base = projectDir || process.cwd();
131
- const candidates = [
132
- path.join(base, ".karajan", "context.md"),
133
- path.join(base, "product-vision.md")
134
- ];
135
- for (const file of candidates) {
136
- try {
137
- const content = await fs.readFile(file, "utf8");
138
- return { content, source: file };
139
- } catch { /* not found, try next */ }
140
- }
141
- return { content: null, source: null };
142
- }
143
-
144
- export function resolvePipelineFlags(config) {
145
- return {
146
- plannerEnabled: Boolean(config.pipeline?.planner?.enabled),
147
- refactorerEnabled: Boolean(config.pipeline?.refactorer?.enabled),
148
- researcherEnabled: Boolean(config.pipeline?.researcher?.enabled),
149
- testerEnabled: Boolean(config.pipeline?.tester?.enabled),
150
- securityEnabled: Boolean(config.pipeline?.security?.enabled),
151
- impeccableEnabled: Boolean(config.pipeline?.impeccable?.enabled),
152
- perfEnabled: Boolean(config.pipeline?.perf?.enabled),
153
- toolJudgeEnabled: Boolean(config.pipeline?.tool_judge?.enabled),
154
- reviewerEnabled: config.pipeline?.reviewer?.enabled !== false,
155
- discoverEnabled: Boolean(config.pipeline?.discover?.enabled),
156
- architectEnabled: Boolean(config.pipeline?.architect?.enabled),
157
- huReviewerEnabled: Boolean(config.pipeline?.hu_reviewer?.enabled),
158
- };
159
- }
160
-
161
- export async function handleDryRun({ task, config, flags, emitter, pipelineFlags }) {
162
- const { plannerEnabled, refactorerEnabled, researcherEnabled, testerEnabled, securityEnabled, impeccableEnabled, perfEnabled, reviewerEnabled, discoverEnabled, architectEnabled, huReviewerEnabled } = pipelineFlags;
163
- const plannerRole = resolveRole(config, "planner");
164
- const coderRole = resolveRole(config, "coder");
165
- const reviewerRole = resolveRole(config, "reviewer");
166
- const refactorerRole = resolveRole(config, "refactorer");
167
- const triageEnabled = true;
168
-
169
- const dryRunPolicies = applyPolicies({
170
- taskType: flags.taskType || config.taskType || null,
171
- policies: config.policies,
172
- });
173
- const projectDir = config.projectDir || process.cwd();
174
- const { rules: reviewRules } = await resolveReviewProfile({ mode: config.review_mode, projectDir });
175
- const coderRules = await loadFirstExisting(resolveRoleMdPath("coder", projectDir));
176
- const coderPrompt = await buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology, serenaEnabled: Boolean(config.serena?.enabled), rtkAvailable: Boolean(config.rtk?.available), productContext: config.productContext || null });
177
- const reviewerPrompt = await buildReviewerPrompt({ task, diff: "(dry-run: no diff)", reviewRules, mode: config.review_mode, serenaEnabled: Boolean(config.serena?.enabled), rtkAvailable: Boolean(config.rtk?.available), productContext: config.productContext || null });
178
-
179
- const summary = {
180
- dry_run: true,
181
- task,
182
- policies: dryRunPolicies,
183
- roles: { planner: plannerRole, coder: coderRole, reviewer: reviewerRole, refactorer: refactorerRole },
184
- pipeline: {
185
- discover_enabled: discoverEnabled,
186
- architect_enabled: architectEnabled,
187
- triage_enabled: triageEnabled,
188
- planner_enabled: plannerEnabled,
189
- refactorer_enabled: refactorerEnabled,
190
- sonar_enabled: Boolean(config.sonarqube?.enabled),
191
- reviewer_enabled: reviewerEnabled,
192
- researcher_enabled: researcherEnabled,
193
- tester_enabled: testerEnabled,
194
- security_enabled: securityEnabled,
195
- impeccable_enabled: impeccableEnabled,
196
- perf_enabled: perfEnabled,
197
- solomon_enabled: Boolean(config.pipeline?.solomon?.enabled),
198
- hu_reviewer_enabled: huReviewerEnabled
199
- },
200
- limits: {
201
- max_iterations: config.max_iterations,
202
- max_iteration_minutes: config.session?.max_iteration_minutes,
203
- max_total_minutes: config.session?.max_total_minutes,
204
- max_sonar_retries: config.session?.max_sonar_retries,
205
- max_reviewer_retries: config.session?.max_reviewer_retries,
206
- max_tester_retries: config.session?.max_tester_retries,
207
- max_security_retries: config.session?.max_security_retries
208
- },
209
- prompts: { coder: coderPrompt, reviewer: reviewerPrompt },
210
- git: config.git
211
- };
212
-
213
- emitProgress(
214
- emitter,
215
- makeEvent("dry-run:summary", { sessionId: null, iteration: 0, stage: "dry-run", startedAt: Date.now() }, {
216
- message: "Dry-run complete — no changes made",
217
- detail: summary
218
- })
219
- );
220
-
221
- return summary;
222
- }
223
-
224
- export function createBudgetManager({ config, emitter, eventBase, getCompressionStats = null }) {
225
- const budgetTracker = new BudgetTracker({ pricing: config?.budget?.pricing });
226
- // KJC-BUG-0114: Number(null) === 0, so a config that reached us with
227
- // max_budget_usd: null produced a phantom "$X / $0.00" ceiling (and a
228
- // permanent warn state). null/undefined fall back to the shipped
229
- // default ceiling; an explicit 0 means "no ceiling".
230
- const rawBudget = config?.max_budget_usd;
231
- const budgetLimit = rawBudget == null ? DEFAULTS.max_budget_usd : Number(rawBudget);
232
- const hasBudgetLimit = Number.isFinite(budgetLimit) && budgetLimit > 0;
233
- const warnThresholdPct = Number(config?.budget?.warn_threshold_pct ?? 80);
234
- let stageCounter = 0;
235
-
236
- function budgetSummary() {
237
- const s = budgetTracker.summary();
238
- s.trace = budgetTracker.trace();
239
- // Attach the "With KJ vs Without KJ" comparison when compression data
240
- // is available (TSK-0274). hasCompression: false means no savings to
241
- // report; the display falls back to the plain cost line.
242
- if (typeof getCompressionStats === "function") {
243
- try {
244
- const stats = getCompressionStats() || {};
245
- s.kj_comparison = computeKjComparison({
246
- realTokens: s.total_tokens,
247
- realCost: s.total_cost_usd,
248
- rtkSavings: stats.rtkSavings,
249
- brainCtx: stats.brainCtx,
250
- });
251
- } catch { /* best-effort — omit comparison on failure */ }
252
- }
253
- return s;
254
- }
255
-
256
- function trackBudget({ role, provider, model, result, duration_ms, promptSize }) {
257
- const enrichedResult = promptSize && result ? { ...result, promptSize } : result;
258
- const metrics = extractUsageMetrics(enrichedResult, model);
259
- budgetTracker.record({ role, provider, ...metrics, duration_ms, stage_index: stageCounter++ });
260
-
261
- if (!hasBudgetLimit) return;
262
- const totalCost = budgetTracker.total().cost_usd;
263
- const pctUsed = budgetLimit === 0 ? 100 : (totalCost / budgetLimit) * 100;
264
- const warnOrOk = pctUsed >= warnThresholdPct ? "paused" : "ok";
265
- const status = totalCost > budgetLimit ? "fail" : warnOrOk;
266
- emitProgress(
267
- emitter,
268
- makeEvent("budget:update", { ...eventBase, stage: role }, {
269
- status,
270
- message: `Budget: $${totalCost.toFixed(2)} / $${budgetLimit.toFixed(2)}`,
271
- detail: {
272
- ...budgetSummary(),
273
- max_budget_usd: budgetLimit,
274
- warn_threshold_pct: warnThresholdPct,
275
- pct_used: Number(pctUsed.toFixed(2)),
276
- remaining_usd: budgetTracker.remaining(budgetLimit),
277
- executorType: "system"
278
- }
279
- })
280
- );
281
- }
282
-
283
- return { budgetTracker, budgetLimit, budgetSummary, trackBudget };
284
- }
285
-
286
- export async function initializeSession({ task, config, flags, pgTaskId, pgProject }) {
287
- const baseRef = await computeBaseRef({ baseBranch: config.base_branch, baseRef: flags.baseRef || null });
288
-
289
- if (baseRef === "__snapshot__") {
290
- // TSK-0340: `setSnapshot` now static; `takeSnapshot` kept dynamic because
291
- // snapshot-diff is ~600 LOC of dependency-free fs walking that most
292
- // runs don't need (only the greenfield/no-git path reaches here).
293
- const { takeSnapshot } = await import("../review/snapshot-diff.js");
294
- const snapshot = await takeSnapshot(config.projectDir || process.cwd());
295
- setSnapshot(snapshot);
296
- }
297
-
298
- // PR-F: stamp project_id on every session derived from the project
299
- // directory (slug = same algorithm the board uses to bucket plans).
300
- // Without this, sessions created by `kj run` from a terminal land
301
- // on disk with `project_id: null`. The board sync sees an orphan
302
- // and (used to) promote it to a phantom "Orphan sessions" project.
303
- // Now that PR-B blocks orphans on the board side, we must also fix
304
- // the source so the session can attach to a real project.
305
- const projectDir = config.projectDir || process.cwd();
306
- const project_id = projectSlug(projectDir);
307
-
308
- // Capture HEAD at the very start of the run, separately from base_ref.
309
- // session_start_sha was being conflated with base_ref (used for `git diff`)
310
- // — when the user's repo had a single commit and no remote main, base_ref
311
- // fell through to git's empty-tree SHA. The post-loop journal then read
312
- // session_start_sha to enumerate "commits made by this run" and got back
313
- // the entire history, including pre-existing commits. Capturing actual
314
- // HEAD here makes "what did the run produce?" deterministic and correct.
315
- // Falls back to baseRef when HEAD doesn't exist (zero-commits repo); in
316
- // that case "everything since baseRef" still equals "everything the run
317
- // produced" because there was no prior history.
318
- let headAtStart;
319
- try {
320
- headAtStart = (await revParse("HEAD")) || baseRef;
321
- } catch { headAtStart = baseRef; }
322
-
323
- const sessionInit = {
324
- task,
325
- project_id,
326
- config_snapshot: config,
327
- base_ref: baseRef,
328
- session_start_sha: baseRef,
329
- head_at_start: headAtStart,
330
- last_reviewer_feedback: null,
331
- repeated_issue_count: 0,
332
- sonar_retry_count: 0,
333
- reviewer_retry_count: 0,
334
- standby_retry_count: 0,
335
- last_sonar_issue_signature: null,
336
- sonar_repeat_count: 0,
337
- last_reviewer_issue_signature: null,
338
- reviewer_repeat_count: 0,
339
- deferred_issues: []
340
- };
341
- if (pgTaskId) sessionInit.pg_task_id = pgTaskId;
342
- if (pgProject) sessionInit.pg_project_id = pgProject;
343
- return createSession(sessionInit);
344
- }
345
-
346
- export function applyTriageOverrides(pipelineFlags, roleOverrides) {
347
- const keys = ["plannerEnabled", "researcherEnabled", "architectEnabled", "refactorerEnabled", "reviewerEnabled", "testerEnabled", "securityEnabled", "impeccableEnabled"];
348
- for (const key of keys) {
349
- if (roleOverrides[key] !== undefined) {
350
- pipelineFlags[key] = roleOverrides[key];
351
- }
352
- }
353
- }
354
-
355
- const SIMPLE_LEVELS = new Set(["trivial", "simple"]);
356
-
357
- export function applyAutoSimplify({ pipelineFlags, triageLevel, config, flags, logger, emitter, eventBase }) {
358
- if (!config.pipeline?.auto_simplify) return false;
359
- if (!triageLevel || !SIMPLE_LEVELS.has(triageLevel)) return false;
360
- if (flags.mode) return false;
361
- if (flags.enableReviewer !== undefined || flags.enableTester !== undefined) return false;
362
-
363
- // KJC-BUG-0297 (#1993): a simple task still gets its cross-AI review. With
364
- // the reviewer off, the gate's stub read as an approval and the pipeline
365
- // stamped a verdict nobody gave and committed it. Only the tester is lighter.
366
- pipelineFlags.testerEnabled = false;
367
-
368
- const disabledRoles = ["tester"];
369
- logger.info(`Simple task (${triageLevel}) — lightweight pipeline (disabled: ${disabledRoles.join(", ")})`);
370
- emitProgress(
371
- emitter,
372
- makeEvent("pipeline:simplify", { ...eventBase, stage: "triage" }, {
373
- message: `Simple task (${triageLevel}) — lightweight pipeline`,
374
- detail: { level: triageLevel, disabledRoles }
375
- })
376
- );
377
- return true;
378
- }
379
-
380
- export function applyFlagOverrides(pipelineFlags, flags) {
381
- if (flags.enablePlanner !== undefined) pipelineFlags.plannerEnabled = Boolean(flags.enablePlanner);
382
- if (flags.enableResearcher !== undefined) pipelineFlags.researcherEnabled = Boolean(flags.enableResearcher);
383
- if (flags.enableArchitect !== undefined) pipelineFlags.architectEnabled = Boolean(flags.enableArchitect);
384
- if (flags.enableRefactorer !== undefined) pipelineFlags.refactorerEnabled = Boolean(flags.enableRefactorer);
385
- if (flags.enableReviewer !== undefined) pipelineFlags.reviewerEnabled = Boolean(flags.enableReviewer);
386
- if (flags.enableTester !== undefined) pipelineFlags.testerEnabled = Boolean(flags.enableTester);
387
- if (flags.enableSecurity !== undefined) pipelineFlags.securityEnabled = Boolean(flags.enableSecurity);
388
- if (flags.enableImpeccable !== undefined) pipelineFlags.impeccableEnabled = Boolean(flags.enableImpeccable);
389
-
390
- if (flags.design) {
391
- pipelineFlags.impeccableEnabled = true;
392
- pipelineFlags.impeccableMode = "refactoring";
393
- }
394
- }
395
-
396
- export function resolvePipelinePolicies({ flags, config, stageResults, emitter, eventBase, session, pipelineFlags }) {
397
- const resolvedPolicies = applyPolicies({
398
- taskType: flags.taskType || config.taskType || stageResults.triage?.taskType || stageResults.intent?.taskType || null,
399
- policies: config.policies,
400
- });
401
- setResolvedPolicies(session, resolvedPolicies);
402
-
403
- let updatedConfig = config;
404
- if (!resolvedPolicies.tdd) {
405
- updatedConfig = { ...updatedConfig, development: { ...updatedConfig.development, methodology: "standard", require_test_changes: false } };
406
- }
407
- if (!resolvedPolicies.sonar) {
408
- updatedConfig = { ...updatedConfig, sonarqube: { ...updatedConfig.sonarqube, enabled: false } };
409
- }
410
- if (!resolvedPolicies.reviewer) {
411
- pipelineFlags.reviewerEnabled = false;
412
- }
413
- if (resolvedPolicies.coderRequired === false) {
414
- pipelineFlags.coderRequired = false;
415
- }
416
-
417
- emitProgress(
418
- emitter,
419
- makeEvent("policies:resolved", eventBase, {
420
- message: `Policies resolved for taskType="${resolvedPolicies.taskType}"`,
421
- detail: resolvedPolicies
422
- })
423
- );
424
-
425
- return updatedConfig;
426
- }
@@ -1,217 +0,0 @@
1
- // Direct actions: commands Karajan Brain can execute without invoking a full role.
2
- // Keeps the action catalog small, auditable, and safe.
3
-
4
- import { execFileSync } from "node:child_process";
5
- import fs from "node:fs/promises";
6
- import path from "node:path";
7
-
8
- /**
9
- * @typedef {Object} DirectAction
10
- * @property {string} type - action type identifier
11
- * @property {Object} params - action-specific parameters
12
- */
13
-
14
- /**
15
- * @typedef {Object} ActionResult
16
- * @property {boolean} ok
17
- * @property {string} [output]
18
- * @property {string} [error]
19
- * @property {string} action
20
- */
21
-
22
- const ALLOWED_COMMANDS = [
23
- "npm install", "npm ci", "pnpm install", "yarn install",
24
- "pip install -r requirements.txt", "poetry install",
25
- "go mod download", "cargo fetch",
26
- "bundle install", "composer install", "dotnet restore"
27
- ];
28
-
29
- // KJC-BUG-0099: a dependency install must not be arbitrary code execution —
30
- // lifecycle hooks (postinstall & friends) run whatever the dependency ships.
31
- // The allow-list above names the INTENT; execution appends the ecosystem's
32
- // script-skipping flag. Ecosystems without package hooks run untouched.
33
- const HARDEN_FLAGS = {
34
- npm: ["--ignore-scripts"],
35
- pnpm: ["--ignore-scripts"],
36
- yarn: ["--ignore-scripts"],
37
- composer: ["--no-scripts", "--no-plugins"],
38
- };
39
-
40
- /**
41
- * True when `target` resolves to a path inside `base` (or is `base` itself).
42
- * Uses path.relative instead of a string prefix check: a naive
43
- * `resolved.startsWith(base)` lets a sibling with a shared prefix slip through
44
- * (base `/repo/app` would accept `/repo/app2`). Also rejects absolute paths
45
- * pointing outside the project.
46
- */
47
- function isInsideBase(target, base) {
48
- const rel = path.relative(base, target);
49
- return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
50
- }
51
-
52
- /**
53
- * Validate that a shell command is in the allow-list.
54
- * Prevents arbitrary command execution.
55
- */
56
- function isCommandAllowed(cmd) {
57
- if (!cmd || typeof cmd !== "string") return false;
58
- const tokens = cmd.trim().split(/\s+/);
59
- return ALLOWED_COMMANDS.some(allowed => {
60
- const allowedTokens = allowed.split(/\s+/);
61
- return allowedTokens.every((t, i) => tokens[i] === t) && tokens.length === allowedTokens.length;
62
- });
63
- }
64
-
65
- /**
66
- * Execute an allowed shell command (install deps, etc.).
67
- */
68
- async function runCommand({ cmd, cwd }) {
69
- if (!isCommandAllowed(cmd)) {
70
- return { ok: false, error: `Command not in allow-list: ${cmd}`, action: "run_command" };
71
- }
72
-
73
- try {
74
- // Audit follow-up: was using execSync, which routes through /bin/sh.
75
- // Even though `cmd` is allow-listed, defense-in-depth: switched to
76
- // execFileSync with a tokenised arg array so no shell expansion at all.
77
- const [program, ...args] = cmd.trim().split(/\s+/);
78
- const output = execFileSync(program, [...args, ...(HARDEN_FLAGS[program] ?? [])], {
79
- cwd: cwd || process.cwd(),
80
- encoding: "utf8",
81
- stdio: ["pipe", "pipe", "pipe"],
82
- timeout: 120_000
83
- });
84
- return { ok: true, output: output.trim(), action: "run_command" };
85
- } catch (err) {
86
- return { ok: false, error: err.message, action: "run_command" };
87
- }
88
- }
89
-
90
- /**
91
- * Create a file with given content (only if it doesn't exist).
92
- */
93
- async function createFile({ filePath, content, cwd, overwrite = false }) {
94
- try {
95
- const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd || process.cwd(), filePath);
96
-
97
- // Path traversal guard
98
- const resolved = path.resolve(fullPath);
99
- const base = path.resolve(cwd || process.cwd());
100
- if (!isInsideBase(resolved, base)) {
101
- return { ok: false, error: "Path traversal denied", action: "create_file" };
102
- }
103
-
104
- try {
105
- await fs.access(fullPath);
106
- if (!overwrite) {
107
- return { ok: false, error: `File already exists: ${filePath}`, action: "create_file" };
108
- }
109
- } catch { /* file doesn't exist, proceed */ }
110
-
111
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
112
- await fs.writeFile(fullPath, content || "", "utf8");
113
- return { ok: true, output: `Created ${filePath}`, action: "create_file" };
114
- } catch (err) {
115
- return { ok: false, error: err.message, action: "create_file" };
116
- }
117
- }
118
-
119
- /**
120
- * Append entries to .gitignore (creates if missing, skips duplicates).
121
- */
122
- async function updateGitignore({ entries, cwd }) {
123
- if (!Array.isArray(entries) || entries.length === 0) {
124
- return { ok: false, error: "No entries provided", action: "update_gitignore" };
125
- }
126
-
127
- try {
128
- const gitignorePath = path.join(cwd || process.cwd(), ".gitignore");
129
- let content = "";
130
- try {
131
- content = await fs.readFile(gitignorePath, "utf8");
132
- } catch { /* will create */ }
133
-
134
- const missing = entries.filter(e => !content.includes(e));
135
- if (missing.length === 0) {
136
- return { ok: true, output: "All entries already present", action: "update_gitignore" };
137
- }
138
-
139
- const append = (content && !content.endsWith("\n") ? "\n" : "") + missing.join("\n") + "\n";
140
- await fs.appendFile(gitignorePath, append, "utf8");
141
- return { ok: true, output: `Added: ${missing.join(", ")}`, action: "update_gitignore" };
142
- } catch (err) {
143
- return { ok: false, error: err.message, action: "update_gitignore" };
144
- }
145
- }
146
-
147
- /**
148
- * Stage files with git add.
149
- */
150
- async function gitAdd({ files, cwd }) {
151
- if (!Array.isArray(files) || files.length === 0) {
152
- return { ok: false, error: "No files to add", action: "git_add" };
153
- }
154
-
155
- try {
156
- // Reject shell metacharacters and anything that resolves outside the project.
157
- const base = path.resolve(cwd || process.cwd());
158
- for (const f of files) {
159
- if (typeof f !== "string" || /[;&|`$]/.test(f) || !isInsideBase(path.resolve(base, f), base)) {
160
- return { ok: false, error: `Invalid file path: ${f}`, action: "git_add" };
161
- }
162
- }
163
- execFileSync("git", ["add", ...files], {
164
- cwd: cwd || process.cwd(),
165
- encoding: "utf8",
166
- stdio: ["pipe", "pipe", "pipe"]
167
- });
168
- return { ok: true, output: `Staged ${files.length} file(s)`, action: "git_add" };
169
- } catch (err) {
170
- return { ok: false, error: err.message, action: "git_add" };
171
- }
172
- }
173
-
174
- const ACTION_HANDLERS = {
175
- run_command: runCommand,
176
- create_file: createFile,
177
- update_gitignore: updateGitignore,
178
- git_add: gitAdd
179
- };
180
-
181
- /**
182
- * Execute a single direct action.
183
- */
184
- export async function executeAction(action, { cwd } = {}) {
185
- if (!action || typeof action !== "object") {
186
- return { ok: false, error: "Invalid action", action: "unknown" };
187
- }
188
-
189
- const handler = ACTION_HANDLERS[action.type];
190
- if (!handler) {
191
- return { ok: false, error: `Unknown action type: ${action.type}`, action: action.type };
192
- }
193
-
194
- return handler({ ...action.params, cwd: cwd || action.params?.cwd });
195
- }
196
-
197
- /**
198
- * Execute a list of actions in sequence. Stops on first failure unless continueOnError.
199
- */
200
- export async function executeActions(actions, { cwd, continueOnError = false } = {}) {
201
- const results = [];
202
- for (const action of actions || []) {
203
- const result = await executeAction(action, { cwd });
204
- results.push(result);
205
- if (!result.ok && !continueOnError) break;
206
- }
207
- return results;
208
- }
209
-
210
- /**
211
- * Get list of allowed action types.
212
- */
213
- export function getAllowedActionTypes() {
214
- return Object.keys(ACTION_HANDLERS);
215
- }
216
-
217
- export { isCommandAllowed };