karajan-code 4.70.3 → 5.0.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 (388) 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/ai-surface.js +9 -7
  61. package/src/checks/config-files.js +8 -7
  62. package/src/checks/dir-setup.js +34 -1
  63. package/src/checks/native-build.js +21 -39
  64. package/src/checks/runner.js +128 -142
  65. package/src/cli/advanced-commands.js +6 -6
  66. package/src/cli/register-meta.js +42 -82
  67. package/src/cli/register-pipeline.js +8 -140
  68. package/src/cli/register-plan.js +9 -100
  69. package/src/cli/retired-commands.js +49 -0
  70. package/src/cli.js +18 -5
  71. package/src/commands/agent-run.js +40 -26
  72. package/src/commands/audit.js +127 -124
  73. package/src/commands/board.js +5 -9
  74. package/src/commands/check.js +29 -16
  75. package/src/commands/doctor.js +2 -0
  76. package/src/commands/env.js +176 -148
  77. package/src/commands/go.js +76 -55
  78. package/src/commands/harden.js +105 -86
  79. package/src/commands/hu-start.js +52 -0
  80. package/src/commands/hu.js +288 -221
  81. package/src/commands/identity.js +111 -78
  82. package/src/commands/init-atril.js +47 -0
  83. package/src/commands/init.js +11 -40
  84. package/src/commands/install-tools.js +70 -85
  85. package/src/commands/main-guard.js +78 -0
  86. package/src/commands/policy.js +226 -184
  87. package/src/commands/qmd.js +27 -21
  88. package/src/commands/rag.js +75 -48
  89. package/src/commands/review-gate.js +422 -339
  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 +94 -25
  94. package/src/config/loader.js +34 -17
  95. package/src/config/overrides.js +2 -1
  96. package/src/config/secrets.js +36 -0
  97. package/src/config.js +1 -0
  98. package/src/environment/adr.js +16 -2
  99. package/src/environment/board-access.js +11 -9
  100. package/src/git/karajan-label.js +20 -8
  101. package/src/guards/main-guard.js +54 -0
  102. package/src/harden/branch-protection.js +36 -3
  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 +81 -35
  106. package/src/harden/sentinel/sentinel-lib.mjs +149 -23
  107. package/src/harden/sentinel/sentinel-shell.mjs +64 -0
  108. package/src/harden/sentinel/sessionstart.mjs +3 -2
  109. package/src/harden/sentinel/stop.mjs +5 -4
  110. package/src/harden/workflow-engine.js +12 -1
  111. package/src/harden/workflow-templates.js +1 -1
  112. package/src/hu/board-lookup.js +29 -0
  113. package/src/hu/gh-identity.js +23 -0
  114. package/src/hu/github-board.js +4 -2
  115. package/src/hu/lane-prune.js +15 -7
  116. package/src/hu/lane-setup.js +65 -0
  117. package/src/hu/lanes.js +66 -0
  118. package/src/hu/team-board-mirror.js +3 -2
  119. package/src/infrastructure/command-runner.js +1 -1
  120. package/src/infrastructure/file-system-service.js +1 -1
  121. package/src/mcp/handlers/direct-handlers.js +2 -292
  122. package/src/mcp/handlers/hu-handlers.js +44 -55
  123. package/src/mcp/handlers/management-handlers.js +2 -42
  124. package/src/mcp/server-handlers.js +16 -22
  125. package/src/mcp/tools.js +5 -200
  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 +56 -43
  134. package/src/rag/bootstrap-index.js +28 -54
  135. package/src/rag/migrate.js +1 -1
  136. package/src/rag/onnx-fallback.js +2 -2
  137. package/src/rag/pid-lease.js +70 -0
  138. package/src/rag/project-store.js +2 -12
  139. package/src/review/card-first.js +57 -46
  140. package/src/review/dual-review.js +5 -4
  141. package/src/review/gate-gitignore.js +23 -4
  142. package/src/review/gate-hook.js +47 -0
  143. package/src/review/one-shot-review.js +51 -37
  144. package/src/review/publish-pr.js +18 -3
  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 +34 -3
  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 +2 -26
  154. package/src/roles/audit-role.js +106 -125
  155. package/src/skills/atril-skills.js +5 -4
  156. package/src/sonar/scanner.js +116 -101
  157. package/src/steward/invariants.js +5 -4
  158. package/src/steward/phantom-sweep.js +66 -0
  159. package/src/tournament/judge.js +60 -53
  160. package/src/utils/agent-home.js +33 -0
  161. package/src/utils/gh-account.js +37 -0
  162. package/src/utils/git.js +0 -13
  163. package/src/utils/home-migration.js +7 -1
  164. package/src/utils/main-checkout.js +53 -0
  165. package/src/utils/qmd-collection.js +3 -1
  166. package/src/utils/welcome.js +3 -3
  167. package/templates/{skills → atril/commands}/kj-architect.md +5 -0
  168. package/templates/{skills → atril/commands}/kj-audit.md +5 -0
  169. package/templates/{skills → atril/commands}/kj-board.md +5 -0
  170. package/templates/{skills → atril/commands}/kj-code.md +5 -0
  171. package/templates/{skills → atril/commands}/kj-csv-transform.md +5 -0
  172. package/templates/{skills → atril/commands}/kj-data-report.md +5 -0
  173. package/templates/{skills → atril/commands}/kj-discover.md +5 -0
  174. package/templates/{skills → atril/commands}/kj-rag-query.md +5 -0
  175. package/templates/{skills → atril/commands}/kj-review.md +5 -0
  176. package/templates/{skills → atril/commands}/kj-security.md +5 -0
  177. package/templates/{skills → atril/commands}/kj-sonar.md +5 -0
  178. package/templates/{skills → atril/commands}/kj-sql-analysis.md +5 -0
  179. package/templates/{skills → atril/commands}/kj-test.md +5 -0
  180. package/packages/hu-board/public/utils/run-launcher.js +0 -169
  181. package/packages/hu-board/src/run-tracker.js +0 -121
  182. package/src/activity-log.js +0 -59
  183. package/src/agents/host-agent.js +0 -38
  184. package/src/agents/model-registry.js +0 -142
  185. package/src/audit/plan-adherence.js +0 -224
  186. package/src/autonomy/arbiter-role.js +0 -71
  187. package/src/autonomy/arbiter.js +0 -22
  188. package/src/autonomy/decider.js +0 -18
  189. package/src/autonomy/decision-resolver.js +0 -58
  190. package/src/autonomy/outcome-report.js +0 -55
  191. package/src/autonomy/stage-ask.js +0 -30
  192. package/src/brain/decision-tracker.js +0 -125
  193. package/src/brain/decisor.js +0 -243
  194. package/src/brain/solomon-consult.js +0 -171
  195. package/src/brain/standby-scheduler.js +0 -8
  196. package/src/budget/comparison.js +0 -96
  197. package/src/canvas/parse-md.js +0 -238
  198. package/src/canvas/schema.js +0 -101
  199. package/src/canvas/to-flat.js +0 -85
  200. package/src/canvas/validate.js +0 -163
  201. package/src/ci/dispatch.js +0 -99
  202. package/src/ci/index.js +0 -3
  203. package/src/ci/pr-diff.js +0 -26
  204. package/src/cli/register-standby.js +0 -26
  205. package/src/commands/architect.js +0 -111
  206. package/src/commands/autorun.js +0 -49
  207. package/src/commands/researcher.js +0 -54
  208. package/src/commands/resume.js +0 -72
  209. package/src/commands/review.js +0 -89
  210. package/src/commands/run.js +0 -185
  211. package/src/commands/standby.js +0 -117
  212. package/src/commands/status.js +0 -36
  213. package/src/commands/triage.js +0 -78
  214. package/src/commands/undo.js +0 -62
  215. package/src/domains/domain-loader.js +0 -245
  216. package/src/domains/domain-registry.js +0 -140
  217. package/src/domains/domain-synthesizer.js +0 -129
  218. package/src/git/automation.js +0 -433
  219. package/src/git/coder-commit-guard.js +0 -40
  220. package/src/git/hu-automation.js +0 -243
  221. package/src/git/hu-snapshot.js +0 -10
  222. package/src/git/symlink-targets.js +0 -57
  223. package/src/golden/asserter.js +0 -92
  224. package/src/golden/loader.js +0 -52
  225. package/src/golden/runner.js +0 -92
  226. package/src/golden/schema.js +0 -41
  227. package/src/guards/duplicate-members.js +0 -86
  228. package/src/guards/intent-guard.js +0 -161
  229. package/src/guards/perf-guard.js +0 -126
  230. package/src/guards/policy-guard.js +0 -90
  231. package/src/guards/policy-resolver.js +0 -85
  232. package/src/guards/secret-redactor.js +0 -71
  233. package/src/hu/acceptance-runner.js +0 -138
  234. package/src/hu/lazy-planner.js +0 -109
  235. package/src/hu/parallel-executor.js +0 -94
  236. package/src/hu/routing-metrics.js +0 -17
  237. package/src/hu/split-validator.js +0 -143
  238. package/src/hu/splitting-detector.js +0 -158
  239. package/src/hu/splitting-generator.js +0 -175
  240. package/src/hu/store.js +0 -394
  241. package/src/infrastructure/mocks.js +0 -182
  242. package/src/mcp/handlers/run-handler.js +0 -318
  243. package/src/mcp/sovereignty-guard.js +0 -113
  244. package/src/mcp/tool-arg-normalizers.js +0 -16
  245. package/src/mutate/reviewer-signal.js +0 -76
  246. package/src/orchestrator/agent-fallback.js +0 -81
  247. package/src/orchestrator/brain-coordinator.js +0 -186
  248. package/src/orchestrator/ci-integration.js +0 -139
  249. package/src/orchestrator/config-init.js +0 -426
  250. package/src/orchestrator/direct-actions.js +0 -217
  251. package/src/orchestrator/drivers/error-recovery.js +0 -287
  252. package/src/orchestrator/drivers/init-context.js +0 -238
  253. package/src/orchestrator/drivers/iteration-loop.js +0 -382
  254. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +0 -69
  255. package/src/orchestrator/drivers/iteration-phases/guards.js +0 -91
  256. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +0 -29
  257. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +0 -125
  258. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +0 -62
  259. package/src/orchestrator/drivers/post-loop.js +0 -404
  260. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +0 -137
  261. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +0 -47
  262. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +0 -84
  263. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +0 -393
  264. package/src/orchestrator/drivers/pre-loop.js +0 -524
  265. package/src/orchestrator/drivers/run-hu-batch.js +0 -468
  266. package/src/orchestrator/feedback-enrichment.js +0 -140
  267. package/src/orchestrator/feedback-queue.js +0 -151
  268. package/src/orchestrator/flow-control.js +0 -170
  269. package/src/orchestrator/flow-runner.js +0 -365
  270. package/src/orchestrator/fs-leak-detector.js +0 -228
  271. package/src/orchestrator/hu-scheduler.js +0 -114
  272. package/src/orchestrator/hu-sub-pipeline.js +0 -642
  273. package/src/orchestrator/iteration-gate.js +0 -73
  274. package/src/orchestrator/iteration-stages.js +0 -9
  275. package/src/orchestrator/parallel-limiter.js +0 -74
  276. package/src/orchestrator/pipeline-context.js +0 -72
  277. package/src/orchestrator/post-loop-stages.js +0 -468
  278. package/src/orchestrator/pre-loop-stages.js +0 -11
  279. package/src/orchestrator/preflight-checks.js +0 -487
  280. package/src/orchestrator/repair/repair-detector.js +0 -117
  281. package/src/orchestrator/repair/repair-runner.js +0 -142
  282. package/src/orchestrator/reviewer-fallback.js +0 -45
  283. package/src/orchestrator/role-output-compressor.js +0 -189
  284. package/src/orchestrator/session-journal.js +0 -308
  285. package/src/orchestrator/solomon-escalation.js +0 -331
  286. package/src/orchestrator/solomon-rules.js +0 -212
  287. package/src/orchestrator/stages/acceptance-coverage-stage.js +0 -47
  288. package/src/orchestrator/stages/architect-stage.js +0 -172
  289. package/src/orchestrator/stages/coder-stage.js +0 -506
  290. package/src/orchestrator/stages/domain-curator-stage.js +0 -82
  291. package/src/orchestrator/stages/format-stage.js +0 -61
  292. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +0 -127
  293. package/src/orchestrator/stages/hu-reviewer-stage.js +0 -419
  294. package/src/orchestrator/stages/perf-stage.js +0 -140
  295. package/src/orchestrator/stages/planner-stage.js +0 -85
  296. package/src/orchestrator/stages/rag-context-stage.js +0 -80
  297. package/src/orchestrator/stages/rag-preload-decisor.js +0 -58
  298. package/src/orchestrator/stages/research-stage.js +0 -69
  299. package/src/orchestrator/stages/reviewer-stage.js +0 -431
  300. package/src/orchestrator/stages/sonar-stage.js +0 -439
  301. package/src/orchestrator/stages/stage-classes.js +0 -84
  302. package/src/orchestrator/stages/stage-executor.js +0 -155
  303. package/src/orchestrator/stages/suite-green-stage.js +0 -62
  304. package/src/orchestrator/stages/tdd-discipline-stage.js +0 -56
  305. package/src/orchestrator/stages/tool-judge-stage.js +0 -53
  306. package/src/orchestrator/stages/triage-stage.js +0 -172
  307. package/src/orchestrator/standby.js +0 -71
  308. package/src/orchestrator/verification-gate.js +0 -183
  309. package/src/orchestrator.js +0 -22
  310. package/src/plan/adr-generator.js +0 -170
  311. package/src/plan/adr-loader.js +0 -60
  312. package/src/plan/plan-executor.js +0 -100
  313. package/src/plan/plan-validation.js +0 -4
  314. package/src/prompts/acceptance-coverage.js +0 -32
  315. package/src/prompts/arbiter.js +0 -38
  316. package/src/prompts/architect.js +0 -103
  317. package/src/prompts/hu-reviewer.js +0 -244
  318. package/src/prompts/tool-correctness-judge.js +0 -31
  319. package/src/prompts/triage.js +0 -57
  320. package/src/rag/embedder.js +0 -2
  321. package/src/rag/embedders/_cloud-base.js +0 -2
  322. package/src/rag/embedders/cohere.js +0 -2
  323. package/src/rag/embedders/mistral.js +0 -2
  324. package/src/rag/embedders/onnx.js +0 -2
  325. package/src/rag/embedders/openai.js +0 -2
  326. package/src/rag/embedders/voyage.js +0 -2
  327. package/src/rag/rerank.js +0 -4
  328. package/src/rag/where-parser.js +0 -4
  329. package/src/repeat-detector.js +0 -88
  330. package/src/review/diff-generator.js +0 -112
  331. package/src/review/git-stash-helper.js +0 -104
  332. package/src/review/schema.js +0 -31
  333. package/src/review/scope-filter.js +0 -152
  334. package/src/review/snapshot-diff.js +0 -122
  335. package/src/review/tdd-discipline.js +0 -90
  336. package/src/review/tdd-policy.js +0 -69
  337. package/src/roles/acceptance-coverage-role.js +0 -78
  338. package/src/roles/architect-role.js +0 -77
  339. package/src/roles/coder-role.js +0 -113
  340. package/src/roles/commiter-role.js +0 -113
  341. package/src/roles/domain-curator-role.js +0 -134
  342. package/src/roles/hu-reviewer-role.js +0 -64
  343. package/src/roles/impeccable-role.js +0 -85
  344. package/src/roles/index.js +0 -18
  345. package/src/roles/karajan-brain-role.js +0 -134
  346. package/src/roles/perf-role.js +0 -122
  347. package/src/roles/planner-role.js +0 -151
  348. package/src/roles/refactorer-role.js +0 -30
  349. package/src/roles/repairer-role.js +0 -181
  350. package/src/roles/researcher-role.js +0 -70
  351. package/src/roles/reviewer-role.js +0 -115
  352. package/src/roles/security-role.js +0 -73
  353. package/src/roles/solomon-role.js +0 -271
  354. package/src/roles/sonar-role.js +0 -72
  355. package/src/roles/tester-role.js +0 -235
  356. package/src/roles/tool-correctness-judge-role.js +0 -78
  357. package/src/roles/triage-role.js +0 -99
  358. package/src/session/journal/decisions-writer.js +0 -200
  359. package/src/session/journal/iteration-logger.js +0 -194
  360. package/src/session/journal/summary-writer.js +0 -320
  361. package/src/session/journal/tree-writer.js +0 -164
  362. package/src/session/journal/write-all.js +0 -89
  363. package/src/session-cleanup.js +0 -116
  364. package/src/skills/semantic-detector.js +0 -167
  365. package/src/skills/skill-detector.js +0 -385
  366. package/src/sonar/cloud-scanner.js +0 -78
  367. package/src/tracker-bootstrap.js +0 -44
  368. package/src/types/index.js +0 -38
  369. package/src/types/policy.js +0 -43
  370. package/src/types/solomon.js +0 -87
  371. package/src/utils/async-lock.js +0 -31
  372. package/src/utils/budget.js +0 -229
  373. package/src/utils/cwd-confirm.js +0 -39
  374. package/src/utils/display/event-handlers.js +0 -403
  375. package/src/utils/display/formatters.js +0 -132
  376. package/src/utils/display/header.js +0 -47
  377. package/src/utils/display/session-summary.js +0 -118
  378. package/src/utils/display/solomon.js +0 -42
  379. package/src/utils/pricing.js +0 -29
  380. package/src/utils/quota-simulator.js +0 -56
  381. package/src/utils/role-assigner.js +0 -137
  382. package/src/utils/rtk-wrapper.js +0 -87
  383. package/src/utils/run-registry.js +0 -9
  384. package/src/utils/status-dashboard.js +0 -254
  385. package/src/utils/tool-call-extractor.js +0 -56
  386. package/src/webperf/devtools-detect.js +0 -112
  387. package/src/webperf/webperf-role-integration.js +0 -66
  388. package/templates/skills/kj-run.md +0 -73
@@ -1,129 +0,0 @@
1
- /**
2
- * Domain context synthesizer.
3
- * Filters relevant sections from domain files based on task + hints,
4
- * and compacts the output to stay within a token budget.
5
- */
6
-
7
- const DEFAULT_MAX_TOKENS = 4000;
8
- const CHARS_PER_TOKEN = 4;
9
-
10
- /**
11
- * Synthesize a domain context string from selected domains,
12
- * filtered by relevance to the task and domain hints.
13
- *
14
- * @param {Object} params
15
- * @param {string} params.task — task description
16
- * @param {string[]} params.domainHints — keywords from triage
17
- * @param {import('./domain-loader.js').DomainFile[]|null} params.selectedDomains
18
- * @param {number} [params.maxTokens=4000] — token budget (0 = unlimited)
19
- * @returns {string} synthesized markdown context or empty string
20
- */
21
- export function synthesizeDomainContext({ task, domainHints = [], selectedDomains, maxTokens = DEFAULT_MAX_TOKENS }) {
22
- if (!selectedDomains?.length) return "";
23
-
24
- const keywords = buildKeywords(task, domainHints);
25
- const domainBlocks = [];
26
-
27
- for (const domain of selectedDomains) {
28
- const sections = domain.sections || [];
29
- if (!sections.length) continue;
30
-
31
- // Score all sections but include them all (domain was explicitly selected).
32
- // Sort by relevance so highest-scoring sections survive token truncation.
33
- const scored = sections
34
- .map(section => ({
35
- section,
36
- score: scoreSection(section, keywords)
37
- }))
38
- .sort((a, b) => b.score - a.score);
39
-
40
- const selected = scored.map(s => s.section);
41
-
42
- const sectionMd = selected
43
- .map(s => `#### ${s.heading}\n${s.content}`)
44
- .join("\n\n");
45
-
46
- domainBlocks.push(`### ${domain.name}\n\n${sectionMd}`);
47
- }
48
-
49
- if (!domainBlocks.length) return "";
50
-
51
- let result = domainBlocks.join("\n\n---\n\n");
52
-
53
- // Truncate by token budget if needed
54
- if (maxTokens > 0) {
55
- const maxChars = maxTokens * CHARS_PER_TOKEN;
56
- if (result.length > maxChars) {
57
- result = truncateBySection(result, maxChars);
58
- }
59
- }
60
-
61
- return result;
62
- }
63
-
64
- /**
65
- * Build a set of lowercase keywords from task text and domain hints.
66
- * @param {string} task
67
- * @param {string[]} hints
68
- * @returns {string[]}
69
- */
70
- function buildKeywords(task, hints) {
71
- const words = new Set();
72
-
73
- // Add domain hints
74
- for (const h of hints) {
75
- words.add(h.toLowerCase());
76
- }
77
-
78
- // Extract meaningful words from task (>3 chars, skip stop words)
79
- const stopWords = new Set(["the", "this", "that", "with", "from", "have", "will", "should", "could", "would", "been", "being", "their", "there", "where", "when", "what", "which", "about", "into", "some", "than", "then", "them", "they", "also", "each", "more", "most", "other"]);
80
-
81
- const taskWords = task
82
- .toLowerCase()
83
- .replaceAll(/[^a-z0-9\s-]/g, " ")
84
- .split(/\s+/)
85
- .filter(w => w.length > 3 && !stopWords.has(w));
86
-
87
- for (const w of taskWords) {
88
- words.add(w);
89
- }
90
-
91
- return Array.from(words);
92
- }
93
-
94
- /**
95
- * Score a section by keyword overlap.
96
- * @param {import('./domain-loader.js').DomainSection} section
97
- * @param {string[]} keywords
98
- * @returns {number}
99
- */
100
- function scoreSection(section, keywords) {
101
- const text = `${section.heading} ${section.content}`.toLowerCase();
102
- let score = 0;
103
- for (const kw of keywords) {
104
- if (text.includes(kw)) score++;
105
- }
106
- return score;
107
- }
108
-
109
- /**
110
- * Truncate result by dropping trailing sections to fit within maxChars.
111
- * Preserves complete sections rather than cutting mid-sentence.
112
- * @param {string} text
113
- * @param {number} maxChars
114
- * @returns {string}
115
- */
116
- function truncateBySection(text, maxChars) {
117
- // Split by section markers (#### or ###)
118
- const parts = text.split(/(?=^#{3,4}\s)/m);
119
- let result = "";
120
-
121
- for (const part of parts) {
122
- if (result.length + part.length > maxChars && result.length > 0) {
123
- break;
124
- }
125
- result += part;
126
- }
127
-
128
- return result.trim();
129
- }
@@ -1,433 +0,0 @@
1
- /**
2
- * Git automation helpers for the pipeline.
3
- * Extracted from orchestrator.js for testability and reuse.
4
- */
5
-
6
- import { addCheckpoint } from "../session/store.js";
7
- import { setGitPreexisting } from "../session/mutators.js";
8
- import { pipelineSonarBlock, stampStagedVerdict } from "../review/verdict-store.js";
9
- import { branchSize } from "../commands/pr-size.js";
10
- import { pushRemedy } from "./hu-automation.js";
11
- import { resolveRunIdentity } from "./run-identity.js";
12
- import { getRunContext } from "../utils/run-context.js";
13
- import {
14
- ensureGitRepo,
15
- currentBranch,
16
- hasCommits,
17
- fetchBase,
18
- syncBaseBranch,
19
- ensureBranchUpToDateWithBase,
20
- createBranch,
21
- checkoutBranch,
22
- buildBranchName,
23
- commitAll,
24
- pushBranch,
25
- createPullRequest,
26
- listPendingPaths,
27
- hasRemote
28
- } from "../utils/git.js";
29
-
30
- /**
31
- * Karajan writes / updates several internal directories during a pipeline
32
- * run (the workspace scaffold + the session journal). When they're the
33
- * ONLY thing pending at finalization, the post-loop commit shouldn't
34
- * inherit the user task's `feat:` message — it should be a clear
35
- * `chore: scaffold karajan workspace` so `git log --oneline` reads:
36
- *
37
- * 2d770aa docs: add JSDoc comment to index.js
38
- * abc1234 chore: scaffold karajan workspace
39
- * 8a95ef0 initial
40
- *
41
- * A path counts as scaffolding when it lives at one of these prefixes
42
- * OR is the project's root `.gitignore` (we extend it to include
43
- * stack-specific entries during triage).
44
- */
45
- const KARAJAN_SCAFFOLD_PREFIXES = [
46
- ".karajan/",
47
- ".reviews/",
48
- ".kj/",
49
- ".agent/",
50
- ];
51
-
52
- function isKarajanScaffoldPath(p) {
53
- if (p === ".gitignore") return true;
54
- return KARAJAN_SCAFFOLD_PREFIXES.some((prefix) => p.startsWith(prefix));
55
- }
56
-
57
- /**
58
- * @param {string[]} paths
59
- * @returns {boolean} true when every pending path is a Karajan internal
60
- */
61
- export function pendingPathsAreOnlyKarajanScaffold(paths) {
62
- if (!Array.isArray(paths) || paths.length === 0) return false;
63
- return paths.every(isKarajanScaffoldPath);
64
- }
65
-
66
- /**
67
- * Map the taskType (as classified by triage) to the matching
68
- * Conventional Commits prefix. When triage hasn't run or the type is
69
- * unknown we default to `feat:` (the pre-fix behaviour).
70
- */
71
- const TASK_TYPE_TO_PREFIX = {
72
- doc: "docs",
73
- "add-tests": "test",
74
- refactor: "refactor",
75
- infra: "chore",
76
- sw: "feat",
77
- };
78
-
79
- function prefixForTaskType(taskType) {
80
- return TASK_TYPE_TO_PREFIX[taskType] || "feat";
81
- }
82
-
83
- export function commitMessageFromTask(task, taskType = null) {
84
- // KJC-BUG-0413 (#2565): the header comes from the first paragraph, without
85
- // its leading Markdown marks; the whole task made "feat: # Roadmap ... Context: ...".
86
- const firstParagraph = String(task || "").trim().split(/\n[ \t]*\r?\n/)[0];
87
- const clean = firstParagraph
88
- .replace(/^[#>*\-\s]+/, "")
89
- .replaceAll(/\s+/g, " ")
90
- .trim();
91
- const prefix = prefixForTaskType(taskType);
92
- // KJC-BUG-0294 (#1993): cut on a word, never mid-word (a title with no blank keeps the hard cut).
93
- const cut = clean.length > 72 ? clean.slice(0, 72).replace(/\s+\S*$/, "") : clean;
94
- return `${prefix}: ${cut || "karajan update"}`;
95
- }
96
-
97
- /**
98
- * Build the message used by `finalizeGitAutomation` for the post-loop
99
- * commit. Two paths:
100
- * - When the only pending changes are Karajan scaffold files
101
- * (.gitignore, .karajan/, .reviews/, ...), use a fixed
102
- * `chore: scaffold karajan workspace` message. This avoids the
103
- * "duplicate feat:" git log seen when triage extends .gitignore
104
- * for a freshly detected stack and the coder already committed
105
- * the user-visible change.
106
- * - Otherwise, fall back to `commitMessageFromTask(task, taskType)`,
107
- * respecting the type for the prefix.
108
- */
109
- export function buildFinalizeCommitMessage({ task, taskType, pendingPaths }) {
110
- if (pendingPathsAreOnlyKarajanScaffold(pendingPaths)) {
111
- return "chore: scaffold karajan workspace";
112
- }
113
- return commitMessageFromTask(task, taskType);
114
- }
115
-
116
- // KJC-BUG-0249 (#1896): a branch named after the card is the same on a second
117
- // run of that card, so one already there is taken, not recreated.
118
- async function checkoutRunBranch(created) {
119
- try {
120
- await createBranch(created);
121
- } catch (err) {
122
- if (!/already exists/i.test(err.message)) throw err;
123
- await checkoutBranch(created);
124
- }
125
- }
126
-
127
- export async function prepareGitAutomation({ config, task, logger, session, preexisting: resumedBaseline = null }) {
128
- const enabled = config.git.auto_commit || config.git.auto_push || config.git.auto_pr;
129
- if (!enabled) {
130
- // KJC-TSK-1237: the format stage needs the baseline also when kj does not
131
- // commit; outside git there is none, and that stage says so.
132
- let baseline = resumedBaseline;
133
- if (!baseline) {
134
- try {
135
- baseline = await listPendingPaths();
136
- } catch {
137
- baseline = null;
138
- }
139
- }
140
- if (session && Array.isArray(baseline)) setGitPreexisting(session, baseline);
141
- return { enabled: false };
142
- }
143
-
144
- if (!(await ensureGitRepo())) {
145
- throw new Error("Git automation requested but current directory is not a git repository");
146
- }
147
-
148
- // KJC-BUG-0246 (#1893): one identity for every commit of the run, resolved
149
- // once: the clone's declaration, else the repository's config, else the
150
- // global one. From the global config it is said, because another session
151
- // may switch it mid-run. Nothing resolvable: git refuses the commit loudly.
152
- const identity = resolveRunIdentity({ projectDir: config.projectDir || process.cwd() });
153
- const runContext = getRunContext();
154
- if (identity && runContext) runContext.gitIdentity = identity;
155
- if (identity) {
156
- logger.info(`Git identity for this run: ${identity.name} <${identity.email}> (${identity.source})`);
157
- if (identity.source === "global") logger.warn("The identity comes from the global git config, which another session may switch mid-run: declare this clone's with kj identity set, or set user.email in this repository");
158
- } else {
159
- logger.warn("No git identity resolvable (kj identity set, or git config user.name and user.email): the first commit will fail");
160
- }
161
-
162
- const baseBranch = config.base_branch;
163
- const autoRebase = config.git.auto_rebase !== false;
164
- // KJC-BUG-0294 (#1993): what is dirty or untracked NOW is not the run's; the
165
- // pipeline commits only what it changes. Both names of a pending rename are
166
- // kept. Fails closed: with no answer from git the run stops here rather than
167
- // commit the person's pending changes later.
168
- // KJC-BUG-0412 (#2564): a resumed run keeps the baseline of the session it
169
- // resumes; the tree now also holds that session's own work.
170
- const preexisting = resumedBaseline ?? await listPendingPaths();
171
- if (session) setGitPreexisting(session, preexisting);
172
- const repoHasCommits = await hasCommits();
173
-
174
- // New repo without commits: create branch directly (no fetch/sync possible)
175
- if (!repoHasCommits) {
176
- const created = buildBranchName(config.git.branch_prefix || "feat/", task, session?.pg_task_id || null);
177
- // git checkout -b works even without commits (creates orphan-like branch on first commit)
178
- await createBranch(created);
179
- logger.info(`New repo — created working branch: ${created}`);
180
- await addCheckpoint(session, { stage: "git-prep", branch: created, created: true, newRepo: true });
181
- return { enabled: true, branch: created, baseBranch, autoRebase, preexisting };
182
- }
183
-
184
- await fetchBase(baseBranch).catch(() => {
185
- // No remote — skip fetch (new project without push)
186
- logger.info("No remote configured — skipping fetch");
187
- });
188
-
189
- let branch;
190
- try {
191
- branch = await currentBranch();
192
- } catch {
193
- // HEAD exists but branch detection failed — unusual, treat as main
194
- branch = baseBranch;
195
- }
196
-
197
- if (branch === baseBranch) {
198
- await syncBaseBranch({ baseBranch, autoRebase }).catch(() => {
199
- // No remote tracking — skip sync for new projects
200
- });
201
- const created = buildBranchName(config.git.branch_prefix || "feat/", task, session?.pg_task_id || null);
202
- await checkoutRunBranch(created);
203
- branch = created;
204
- logger.info(`Created working branch: ${branch}`);
205
- await addCheckpoint(session, { stage: "git-prep", branch, created: true });
206
- } else {
207
- await ensureBranchUpToDateWithBase({ branch, baseBranch, autoRebase }).catch(() => {
208
- // No remote tracking — skip rebase for new projects
209
- logger.info("No remote tracking — skipping rebase check");
210
- });
211
- await addCheckpoint(session, { stage: "git-prep", branch, created: false });
212
- }
213
-
214
- return { enabled: true, branch, baseBranch, autoRebase, preexisting };
215
- }
216
-
217
- export function buildPrBody({ task: _task, stageResults }) {
218
- const sections = ["Created by Karajan Code."];
219
-
220
- const approach = stageResults?.planner?.approach;
221
- if (approach) {
222
- sections.push("", "## Approach", approach);
223
- }
224
-
225
- const steps = stageResults?.planner?.steps;
226
- if (steps?.length) {
227
- sections.push("", "## Steps");
228
- for (let i = 0; i < steps.length; i++) {
229
- sections.push(`${i + 1}. ${steps[i]}`);
230
- }
231
- }
232
-
233
- const triageSubtasks = stageResults?.triage?.subtasks;
234
- const shouldDecompose = stageResults?.triage?.shouldDecompose;
235
- const pendingSubtasks = shouldDecompose && triageSubtasks?.length > 1 ? triageSubtasks.slice(1) : [];
236
- if (pendingSubtasks.length > 0) {
237
- sections.push("", "## Pending subtasks", "This PR addresses part of a larger task. The following subtasks were identified but not included:");
238
- for (const subtask of pendingSubtasks) {
239
- sections.push(`- [ ] ${subtask}`);
240
- }
241
- }
242
-
243
- return sections.join("\n");
244
- }
245
-
246
- /**
247
- * Create an early PR after the first coder iteration (Karajan CI flow).
248
- * Commits, pushes, and creates a PR before the reviewer runs.
249
- * Returns { prNumber, prUrl, commits } or null if nothing to commit.
250
- */
251
- export async function earlyPrCreation({ gitCtx, task, logger, session, stageResults = null }) {
252
- if (!gitCtx?.enabled) return null;
253
-
254
- const commitMsg = commitMessageFromTask(task);
255
- const commitResult = await commitAll(commitMsg, null, { exclude: gitCtx.preexisting || [] });
256
- if (!commitResult.committed) {
257
- logger.info("earlyPrCreation: no changes to commit");
258
- return null;
259
- }
260
-
261
- const commits = commitResult.commit ? [commitResult.commit] : [];
262
- await addCheckpoint(session, { stage: "ci-commit", committed: true });
263
-
264
- await pushBranch(gitCtx.branch);
265
- await addCheckpoint(session, { stage: "ci-push", branch: gitCtx.branch });
266
- logger.info(`Pushed branch for early PR: ${gitCtx.branch}`);
267
-
268
- const body = buildPrBody({ task, stageResults });
269
- const prUrl = await createPullRequest({
270
- baseBranch: gitCtx.baseBranch,
271
- branch: gitCtx.branch,
272
- title: commitMessageFromTask(task),
273
- body
274
- });
275
- await addCheckpoint(session, { stage: "ci-pr", branch: gitCtx.branch, pr: prUrl });
276
- logger.info(`Early PR created: ${prUrl}`);
277
-
278
- // Extract PR number from URL (e.g. https://github.com/owner/repo/pull/42)
279
- const prNumber = Number.parseInt(prUrl.split("/").pop(), 10) || null;
280
- return { prNumber, prUrl, commits };
281
- }
282
-
283
- /**
284
- * Incremental push after each coder iteration (Karajan CI flow).
285
- * Commits and pushes without creating a new PR.
286
- */
287
- export async function incrementalPush({ gitCtx, task, logger, session }) {
288
- if (!gitCtx?.enabled) return null;
289
-
290
- const commitMsg = commitMessageFromTask(task);
291
- const commitResult = await commitAll(commitMsg, null, { exclude: gitCtx.preexisting || [] });
292
- if (!commitResult.committed) {
293
- logger.info("incrementalPush: no changes to commit");
294
- return null;
295
- }
296
-
297
- const commits = commitResult.commit ? [commitResult.commit] : [];
298
- await addCheckpoint(session, { stage: "ci-incremental-commit", committed: true });
299
-
300
- await pushBranch(gitCtx.branch);
301
- await addCheckpoint(session, { stage: "ci-incremental-push", branch: gitCtx.branch });
302
- logger.info(`Incremental push: ${gitCtx.branch}`);
303
-
304
- return { commits };
305
- }
306
-
307
- const DEFAULT_PR_SIZE_LIMIT = 200;
308
-
309
- export async function finalizeGitAutomation({ config, gitCtx, task, logger, session, stageResults = null, review = null, measureSize = branchSize }) {
310
- if (!gitCtx?.enabled) return { git: "disabled", commits: [] };
311
- // KJC-BUG-0297 (#1993): the pipeline commits only what a reviewer approved. An
312
- // approval that reviewed nothing (reviewer disabled) stamps no verdict and
313
- // commits nothing: the changes stay in the tree, and the log says why.
314
- if (review && review.reviewed === false) {
315
- logger.warn("No review, no commit: the reviewer was disabled, so the pipeline commits nothing; the changes stay in the tree for a review (kj review --staged)");
316
- await addCheckpoint(session, { stage: "git-commit", committed: false, skipped: "unreviewed" });
317
- return { committed: false, branch: gitCtx.branch, prUrl: null, pr: null, commits: [], unreviewed: true };
318
- }
319
-
320
- // Take a snapshot of pending paths BEFORE staging so we can decide
321
- // whether what's about to be committed is just Karajan scaffolding
322
- // (in which case we use `chore: scaffold karajan workspace`) or a
323
- // real, user-visible change (in which case we honour the task's
324
- // taskType for the Conventional Commits prefix).
325
- const taskType = stageResults?.triage?.taskType ?? null;
326
- let pendingPaths = [];
327
- if (config.git.auto_commit) {
328
- try {
329
- pendingPaths = await listPendingPaths();
330
- } catch { /* best-effort — fall back to default message */ }
331
- }
332
-
333
- const commitMsg =
334
- config.git.commit_message
335
- || buildFinalizeCommitMessage({ task, taskType, pendingPaths });
336
-
337
- let committed = false;
338
- const commits = [];
339
- // KJC-BUG-0299 (#1993): the PR budget is applied BEFORE the commit, with the
340
- // CI gate's own count (kj pr-size), over what this commit will contain. A
341
- // step planned at 150 lines once committed 960. Where the branch cannot be
342
- // measured (a fresh repo with no base yet) kj says so and goes on.
343
- if (config.git.auto_commit) {
344
- const limit = Number(config.git.pr_size_limit) > 0 ? Number(config.git.pr_size_limit) : DEFAULT_PR_SIZE_LIMIT;
345
- let size = null;
346
- try {
347
- size = await measureSize({ projectDir: config.projectDir || process.cwd(), base: gitCtx.baseBranch, exclude: gitCtx.preexisting || [] });
348
- } catch (err) {
349
- logger.warn(`pr-size: the branch could not be measured (${err.message}); the budget is not checked for this commit`);
350
- }
351
- // KJC-BUG-0434: net, as the CI gate counts it.
352
- if (size && size.net > limit) {
353
- logger.warn(`pr-size: ${size.net} net line(s) against ${gitCtx.baseBranch}, over the budget of ${limit}: nothing committed. Split the step (several commits, several HUs); the changes stay in the tree`);
354
- await addCheckpoint(session, { stage: "git-commit", committed: false, skipped: "over-budget", added: size.added, net: size.net, limit });
355
- return { committed: false, branch: gitCtx.branch, prUrl: null, pr: null, commits: [], overBudget: { added: size.added, net: size.net, limit } };
356
- }
357
- // ENV-F1: this path only runs after the pipeline's reviewer approved,
358
- // so stamp that verdict for the staged diff — the v4 pre-commit gate
359
- // (when the repo opted in) accepts the pipeline's own commit.
360
- const commitResult = await commitAll(commitMsg, null, {
361
- exclude: gitCtx.preexisting || [],
362
- beforeCommit: ({ paths } = {}) => stampStagedVerdict({
363
- projectDir: config?.projectDir || process.cwd(),
364
- paths,
365
- reviewer: config?.reviewer || "pipeline-reviewer",
366
- summary: `kj run session ${session?.id || ""}: reviewer approved`.trim(),
367
- // KJC-TSK-0838: the sonar stage result travels with the stamp.
368
- sonar: pipelineSonarBlock(stageResults?.sonar),
369
- }),
370
- });
371
- committed = commitResult.committed;
372
- if (commitResult.commit) {
373
- commits.push(commitResult.commit);
374
- }
375
- // KJC-BUG-0294: what was pending before the run stays out of the commit and is named;
376
- // if the run also touched one of those files, its edit is in the tree, not in the commit.
377
- if (commitResult.leftOut?.length) {
378
- logger.warn(`Left out of the commit, pending before the run (review and commit them yourself): ${commitResult.leftOut.join(", ")}`);
379
- }
380
- await addCheckpoint(session, { stage: "git-commit", committed, leftOut: commitResult.leftOut || [] });
381
- logger.info(committed ? "Committed changes" : "No changes to commit");
382
- }
383
-
384
- // KJC-BUG-0112: without an `origin` remote (the quickstart scenario:
385
- // fresh folder + git init) push/PR automation has nothing to talk to —
386
- // fetchBase used to throw here and escalate to Solomon on EVERY
387
- // iteration, burning the whole budget on an expected condition.
388
- const remoteAvailable = (config.git.auto_push || config.git.auto_pr)
389
- ? await hasRemote()
390
- : false;
391
- if ((config.git.auto_push || config.git.auto_pr) && !remoteAvailable) {
392
- logger.info("No remote configured — skipping push/PR automation");
393
- await addCheckpoint(session, { stage: "git-push", skipped: "no-remote" });
394
- }
395
-
396
- if (remoteAvailable && (config.git.auto_push || config.git.auto_pr)) {
397
- await fetchBase(gitCtx.baseBranch);
398
- await ensureBranchUpToDateWithBase({
399
- branch: gitCtx.branch,
400
- baseBranch: gitCtx.baseBranch,
401
- autoRebase: gitCtx.autoRebase
402
- });
403
- await addCheckpoint(session, { stage: "git-rebase-check", branch: gitCtx.branch });
404
- }
405
-
406
- if (remoteAvailable && (config.git.auto_push || config.git.auto_pr)) {
407
- // KJC-BUG-0248 (#1895): a refused push stops the run loudly, with its remedy.
408
- try {
409
- await pushBranch(gitCtx.branch);
410
- } catch (err) {
411
- throw new Error(`push of ${gitCtx.branch} failed: ${err.message}. ${pushRemedy(err.message, gitCtx.branch)}`, { cause: err });
412
- }
413
- await addCheckpoint(session, { stage: "git-push", branch: gitCtx.branch });
414
- logger.info(`Pushed branch: ${gitCtx.branch}`);
415
- }
416
-
417
- let prUrl = session.ci_pr_url || null;
418
- if (remoteAvailable && config.git.auto_pr && !prUrl) {
419
- const body = buildPrBody({ task, stageResults });
420
- prUrl = await createPullRequest({
421
- baseBranch: gitCtx.baseBranch,
422
- branch: gitCtx.branch,
423
- title: commitMessageFromTask(task),
424
- body
425
- });
426
- await addCheckpoint(session, { stage: "git-pr", branch: gitCtx.branch, pr: prUrl });
427
- logger.info("Pull request created");
428
- } else if (prUrl) {
429
- logger.info(`PR already exists (CI flow): ${prUrl}`);
430
- }
431
-
432
- return { committed, branch: gitCtx.branch, prUrl, pr: prUrl, commits };
433
- }
@@ -1,40 +0,0 @@
1
- /**
2
- * KJC-BUG-0285 (#1982): the coder commits nothing; the pipeline does, once the
3
- * review approves. With --auto-commit the coder ran git commit inside its
4
- * iteration, before the reviewer, and the rejected commit stayed on the branch.
5
- * The stage reads HEAD before the coder runs and, when HEAD moved on top of it,
6
- * undoes those commits SOFTLY: the changes stay in the tree and the index for
7
- * the review. A rewritten branch (HEAD no longer descends from the start) is
8
- * left alone. Plain git, no injected runner: it never throws, it says.
9
- */
10
- import { spawnSync } from "node:child_process";
11
-
12
- const git = (args, cwd) => spawnSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
13
-
14
- /** @returns {string|null} the sha HEAD points at, or null outside a repo with commits */
15
- export function headSha(cwd = process.cwd()) {
16
- try {
17
- const res = git(["rev-parse", "HEAD"], cwd);
18
- return res.status === 0 ? res.stdout.trim() : null;
19
- } catch {
20
- return null;
21
- }
22
- }
23
-
24
- /** @returns {{undone: number, reason?: string}} */
25
- export function undoCommitsSince(sha, cwd = process.cwd()) {
26
- if (!sha) return { undone: 0, reason: "no starting commit" };
27
- try {
28
- const head = headSha(cwd);
29
- if (!head || head === sha) return { undone: 0 };
30
- if (git(["merge-base", "--is-ancestor", sha, "HEAD"], cwd).status !== 0) {
31
- return { undone: 0, reason: "HEAD no longer descends from the iteration's start" };
32
- }
33
- const undone = Number(git(["rev-list", "--count", `${sha}..HEAD`], cwd).stdout.trim());
34
- const reset = git(["reset", "--soft", sha], cwd);
35
- if (reset.status !== 0) return { undone: 0, reason: `git reset --soft failed: ${reset.stderr.trim()}` };
36
- return { undone };
37
- } catch (err) {
38
- return { undone: 0, reason: err.message };
39
- }
40
- }