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.
- package/README.md +2 -2
- package/package.json +6 -14
- package/packages/hu-board/package.json +1 -1
- package/packages/hu-board/public/app.js +4 -18
- package/packages/hu-board/public/index.html +5 -2
- package/packages/hu-board/public/styles.css +26 -0
- package/packages/hu-board/public/utils/api.js +0 -74
- package/packages/hu-board/public/utils/board-tour.js +62 -0
- package/packages/hu-board/public/utils/board-view.js +49 -144
- package/packages/hu-board/public/utils/command-launcher.js +8 -70
- package/packages/hu-board/public/utils/formatters.js +10 -0
- package/packages/hu-board/public/utils/governance-view.js +13 -1
- package/packages/hu-board/public/utils/hu-actions.js +17 -68
- package/packages/hu-board/public/utils/init-listeners.js +7 -8
- package/packages/hu-board/public/utils/log-panel.js +3 -16
- package/packages/hu-board/public/utils/maggle-mode.js +2 -12
- package/packages/hu-board/public/utils/modals.js +7 -0
- package/packages/hu-board/public/utils/project-picker-view.js +1 -1
- package/packages/hu-board/public/utils/server-push.js +3 -22
- package/packages/hu-board/public/utils/shared-board-notice.js +33 -0
- package/packages/hu-board/public/utils/story-detail-view.js +5 -19
- package/packages/hu-board/src/cleanup-zombies.js +0 -1
- package/packages/hu-board/src/command-runner.js +36 -25
- package/packages/hu-board/src/db-connection.js +282 -0
- package/packages/hu-board/src/db-paths.js +113 -0
- package/packages/hu-board/src/db-queries.js +188 -0
- package/packages/hu-board/src/db-tombstones.js +51 -0
- package/packages/hu-board/src/db.js +28 -619
- package/packages/hu-board/src/ephemeral-cleaner.js +2 -3
- package/packages/hu-board/src/hu-zombie-reaper.js +5 -1
- package/packages/hu-board/src/plan-mutations.js +3 -153
- package/packages/hu-board/src/plans-service.js +55 -0
- package/packages/hu-board/src/routes/api/commands.js +122 -0
- package/packages/hu-board/src/routes/api/plans.js +55 -0
- package/packages/hu-board/src/routes/api/projects.js +267 -0
- package/packages/hu-board/src/routes/api/prompts.js +135 -0
- package/packages/hu-board/src/routes/api/sessions.js +95 -0
- package/packages/hu-board/src/routes/api/stories.js +183 -0
- package/packages/hu-board/src/routes/api/system.js +241 -0
- package/packages/hu-board/src/routes/api/tombstones.js +38 -0
- package/packages/hu-board/src/routes/api.js +24 -1371
- package/packages/hu-board/src/routes/governance.js +23 -1
- package/packages/hu-board/src/routes/lanes.js +40 -0
- package/packages/hu-board/src/routes/rag.js +4 -3
- package/packages/hu-board/src/server.js +20 -34
- package/packages/hu-board/src/sync-files.js +318 -0
- package/packages/hu-board/src/sync-project.js +99 -0
- package/packages/hu-board/src/sync.js +22 -566
- package/packages/hu-board/src/zombie-reaper.js +1 -1
- package/scripts/dual-publish.mjs +19 -3
- package/scripts/postinstall.js +15 -2
- package/scripts/verify-pack-mcp.mjs +11 -7
- package/scripts/verify-pack.mjs +4 -4
- package/src/audit/audit-history.js +3 -2
- package/src/audit/dead-exports.js +68 -59
- package/src/audit/deterministic-summary.js +33 -13
- package/src/audit/npm-signatures.js +41 -0
- package/src/audit/sbom.js +35 -0
- package/src/checks/agent-token.js +112 -0
- package/src/checks/ai-surface.js +9 -7
- package/src/checks/config-files.js +8 -7
- package/src/checks/dir-setup.js +34 -1
- package/src/checks/native-build.js +21 -39
- package/src/checks/runner.js +128 -142
- package/src/cli/advanced-commands.js +6 -6
- package/src/cli/register-meta.js +42 -82
- package/src/cli/register-pipeline.js +8 -140
- package/src/cli/register-plan.js +9 -100
- package/src/cli/retired-commands.js +49 -0
- package/src/cli.js +18 -5
- package/src/commands/agent-run.js +40 -26
- package/src/commands/audit.js +127 -124
- package/src/commands/board.js +5 -9
- package/src/commands/check.js +29 -16
- package/src/commands/doctor.js +2 -0
- package/src/commands/env.js +176 -148
- package/src/commands/go.js +76 -55
- package/src/commands/harden.js +105 -86
- package/src/commands/hu-start.js +52 -0
- package/src/commands/hu.js +288 -221
- package/src/commands/identity.js +111 -78
- package/src/commands/init-atril.js +47 -0
- package/src/commands/init.js +11 -40
- package/src/commands/install-tools.js +70 -85
- package/src/commands/main-guard.js +78 -0
- package/src/commands/policy.js +226 -184
- package/src/commands/qmd.js +27 -21
- package/src/commands/rag.js +75 -48
- package/src/commands/review-gate.js +422 -339
- package/src/commands/start.js +4 -5
- package/src/commands/steward.js +96 -47
- package/src/commands/tournament.js +46 -37
- package/src/commands/worktree.js +94 -25
- package/src/config/loader.js +34 -17
- package/src/config/overrides.js +2 -1
- package/src/config/secrets.js +36 -0
- package/src/config.js +1 -0
- package/src/environment/adr.js +16 -2
- package/src/environment/board-access.js +11 -9
- package/src/git/karajan-label.js +20 -8
- package/src/guards/main-guard.js +54 -0
- package/src/harden/branch-protection.js +36 -3
- package/src/harden/hook-templates.js +16 -1
- package/src/harden/sentinel/posttooluse.mjs +18 -6
- package/src/harden/sentinel/pretooluse-sentinel.mjs +81 -35
- package/src/harden/sentinel/sentinel-lib.mjs +149 -23
- package/src/harden/sentinel/sentinel-shell.mjs +64 -0
- package/src/harden/sentinel/sessionstart.mjs +3 -2
- package/src/harden/sentinel/stop.mjs +5 -4
- package/src/harden/workflow-engine.js +12 -1
- package/src/harden/workflow-templates.js +1 -1
- package/src/hu/board-lookup.js +29 -0
- package/src/hu/gh-identity.js +23 -0
- package/src/hu/github-board.js +4 -2
- package/src/hu/lane-prune.js +15 -7
- package/src/hu/lane-setup.js +65 -0
- package/src/hu/lanes.js +66 -0
- package/src/hu/team-board-mirror.js +3 -2
- package/src/infrastructure/command-runner.js +1 -1
- package/src/infrastructure/file-system-service.js +1 -1
- package/src/mcp/handlers/direct-handlers.js +2 -292
- package/src/mcp/handlers/hu-handlers.js +44 -55
- package/src/mcp/handlers/management-handlers.js +2 -42
- package/src/mcp/server-handlers.js +16 -22
- package/src/mcp/tools.js +5 -200
- package/src/plan/lane-plans.js +54 -0
- package/src/plan/plan-store.js +7 -4
- package/src/policy/add.js +26 -12
- package/src/policy/adr-number-check.js +31 -0
- package/src/prompts/audit.js +299 -339
- package/src/prompts/card-context.js +10 -5
- package/src/prompts/reviewer.js +5 -2
- package/src/rag/auto-update.js +56 -43
- package/src/rag/bootstrap-index.js +28 -54
- package/src/rag/migrate.js +1 -1
- package/src/rag/onnx-fallback.js +2 -2
- package/src/rag/pid-lease.js +70 -0
- package/src/rag/project-store.js +2 -12
- package/src/review/card-first.js +57 -46
- package/src/review/dual-review.js +5 -4
- package/src/review/gate-gitignore.js +23 -4
- package/src/review/gate-hook.js +47 -0
- package/src/review/one-shot-review.js +51 -37
- package/src/review/publish-pr.js +18 -3
- package/src/review/repo-docs.js +73 -0
- package/src/review/repo-stack.js +59 -0
- package/src/review/reviewer-fallback.js +34 -3
- package/src/review/seal-commit.js +29 -0
- package/src/review/sonar-pregate.js +59 -44
- package/src/review/third-party-review.js +90 -16
- package/src/review/third-party-size.js +43 -0
- package/src/review/third-party-skills.js +54 -0
- package/src/review/verify-pr.js +2 -26
- package/src/roles/audit-role.js +106 -125
- package/src/skills/atril-skills.js +5 -4
- package/src/sonar/scanner.js +116 -101
- package/src/steward/invariants.js +5 -4
- package/src/steward/phantom-sweep.js +66 -0
- package/src/tournament/judge.js +60 -53
- package/src/utils/agent-home.js +33 -0
- package/src/utils/gh-account.js +37 -0
- package/src/utils/git.js +0 -13
- package/src/utils/home-migration.js +7 -1
- package/src/utils/main-checkout.js +53 -0
- package/src/utils/qmd-collection.js +3 -1
- package/src/utils/welcome.js +3 -3
- package/templates/{skills → atril/commands}/kj-architect.md +5 -0
- package/templates/{skills → atril/commands}/kj-audit.md +5 -0
- package/templates/{skills → atril/commands}/kj-board.md +5 -0
- package/templates/{skills → atril/commands}/kj-code.md +5 -0
- package/templates/{skills → atril/commands}/kj-csv-transform.md +5 -0
- package/templates/{skills → atril/commands}/kj-data-report.md +5 -0
- package/templates/{skills → atril/commands}/kj-discover.md +5 -0
- package/templates/{skills → atril/commands}/kj-rag-query.md +5 -0
- package/templates/{skills → atril/commands}/kj-review.md +5 -0
- package/templates/{skills → atril/commands}/kj-security.md +5 -0
- package/templates/{skills → atril/commands}/kj-sonar.md +5 -0
- package/templates/{skills → atril/commands}/kj-sql-analysis.md +5 -0
- package/templates/{skills → atril/commands}/kj-test.md +5 -0
- package/packages/hu-board/public/utils/run-launcher.js +0 -169
- package/packages/hu-board/src/run-tracker.js +0 -121
- package/src/activity-log.js +0 -59
- package/src/agents/host-agent.js +0 -38
- package/src/agents/model-registry.js +0 -142
- package/src/audit/plan-adherence.js +0 -224
- package/src/autonomy/arbiter-role.js +0 -71
- package/src/autonomy/arbiter.js +0 -22
- package/src/autonomy/decider.js +0 -18
- package/src/autonomy/decision-resolver.js +0 -58
- package/src/autonomy/outcome-report.js +0 -55
- package/src/autonomy/stage-ask.js +0 -30
- package/src/brain/decision-tracker.js +0 -125
- package/src/brain/decisor.js +0 -243
- package/src/brain/solomon-consult.js +0 -171
- package/src/brain/standby-scheduler.js +0 -8
- package/src/budget/comparison.js +0 -96
- package/src/canvas/parse-md.js +0 -238
- package/src/canvas/schema.js +0 -101
- package/src/canvas/to-flat.js +0 -85
- package/src/canvas/validate.js +0 -163
- package/src/ci/dispatch.js +0 -99
- package/src/ci/index.js +0 -3
- package/src/ci/pr-diff.js +0 -26
- package/src/cli/register-standby.js +0 -26
- package/src/commands/architect.js +0 -111
- package/src/commands/autorun.js +0 -49
- package/src/commands/researcher.js +0 -54
- package/src/commands/resume.js +0 -72
- package/src/commands/review.js +0 -89
- package/src/commands/run.js +0 -185
- package/src/commands/standby.js +0 -117
- package/src/commands/status.js +0 -36
- package/src/commands/triage.js +0 -78
- package/src/commands/undo.js +0 -62
- package/src/domains/domain-loader.js +0 -245
- package/src/domains/domain-registry.js +0 -140
- package/src/domains/domain-synthesizer.js +0 -129
- package/src/git/automation.js +0 -433
- package/src/git/coder-commit-guard.js +0 -40
- package/src/git/hu-automation.js +0 -243
- package/src/git/hu-snapshot.js +0 -10
- package/src/git/symlink-targets.js +0 -57
- package/src/golden/asserter.js +0 -92
- package/src/golden/loader.js +0 -52
- package/src/golden/runner.js +0 -92
- package/src/golden/schema.js +0 -41
- package/src/guards/duplicate-members.js +0 -86
- package/src/guards/intent-guard.js +0 -161
- package/src/guards/perf-guard.js +0 -126
- package/src/guards/policy-guard.js +0 -90
- package/src/guards/policy-resolver.js +0 -85
- package/src/guards/secret-redactor.js +0 -71
- package/src/hu/acceptance-runner.js +0 -138
- package/src/hu/lazy-planner.js +0 -109
- package/src/hu/parallel-executor.js +0 -94
- package/src/hu/routing-metrics.js +0 -17
- package/src/hu/split-validator.js +0 -143
- package/src/hu/splitting-detector.js +0 -158
- package/src/hu/splitting-generator.js +0 -175
- package/src/hu/store.js +0 -394
- package/src/infrastructure/mocks.js +0 -182
- package/src/mcp/handlers/run-handler.js +0 -318
- package/src/mcp/sovereignty-guard.js +0 -113
- package/src/mcp/tool-arg-normalizers.js +0 -16
- package/src/mutate/reviewer-signal.js +0 -76
- package/src/orchestrator/agent-fallback.js +0 -81
- package/src/orchestrator/brain-coordinator.js +0 -186
- package/src/orchestrator/ci-integration.js +0 -139
- package/src/orchestrator/config-init.js +0 -426
- package/src/orchestrator/direct-actions.js +0 -217
- package/src/orchestrator/drivers/error-recovery.js +0 -287
- package/src/orchestrator/drivers/init-context.js +0 -238
- package/src/orchestrator/drivers/iteration-loop.js +0 -382
- package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +0 -69
- package/src/orchestrator/drivers/iteration-phases/guards.js +0 -91
- package/src/orchestrator/drivers/iteration-phases/handle-approved.js +0 -29
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +0 -125
- package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +0 -62
- package/src/orchestrator/drivers/post-loop.js +0 -404
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +0 -137
- package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +0 -47
- package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +0 -84
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +0 -393
- package/src/orchestrator/drivers/pre-loop.js +0 -524
- package/src/orchestrator/drivers/run-hu-batch.js +0 -468
- package/src/orchestrator/feedback-enrichment.js +0 -140
- package/src/orchestrator/feedback-queue.js +0 -151
- package/src/orchestrator/flow-control.js +0 -170
- package/src/orchestrator/flow-runner.js +0 -365
- package/src/orchestrator/fs-leak-detector.js +0 -228
- package/src/orchestrator/hu-scheduler.js +0 -114
- package/src/orchestrator/hu-sub-pipeline.js +0 -642
- package/src/orchestrator/iteration-gate.js +0 -73
- package/src/orchestrator/iteration-stages.js +0 -9
- package/src/orchestrator/parallel-limiter.js +0 -74
- package/src/orchestrator/pipeline-context.js +0 -72
- package/src/orchestrator/post-loop-stages.js +0 -468
- package/src/orchestrator/pre-loop-stages.js +0 -11
- package/src/orchestrator/preflight-checks.js +0 -487
- package/src/orchestrator/repair/repair-detector.js +0 -117
- package/src/orchestrator/repair/repair-runner.js +0 -142
- package/src/orchestrator/reviewer-fallback.js +0 -45
- package/src/orchestrator/role-output-compressor.js +0 -189
- package/src/orchestrator/session-journal.js +0 -308
- package/src/orchestrator/solomon-escalation.js +0 -331
- package/src/orchestrator/solomon-rules.js +0 -212
- package/src/orchestrator/stages/acceptance-coverage-stage.js +0 -47
- package/src/orchestrator/stages/architect-stage.js +0 -172
- package/src/orchestrator/stages/coder-stage.js +0 -506
- package/src/orchestrator/stages/domain-curator-stage.js +0 -82
- package/src/orchestrator/stages/format-stage.js +0 -61
- package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +0 -127
- package/src/orchestrator/stages/hu-reviewer-stage.js +0 -419
- package/src/orchestrator/stages/perf-stage.js +0 -140
- package/src/orchestrator/stages/planner-stage.js +0 -85
- package/src/orchestrator/stages/rag-context-stage.js +0 -80
- package/src/orchestrator/stages/rag-preload-decisor.js +0 -58
- package/src/orchestrator/stages/research-stage.js +0 -69
- package/src/orchestrator/stages/reviewer-stage.js +0 -431
- package/src/orchestrator/stages/sonar-stage.js +0 -439
- package/src/orchestrator/stages/stage-classes.js +0 -84
- package/src/orchestrator/stages/stage-executor.js +0 -155
- package/src/orchestrator/stages/suite-green-stage.js +0 -62
- package/src/orchestrator/stages/tdd-discipline-stage.js +0 -56
- package/src/orchestrator/stages/tool-judge-stage.js +0 -53
- package/src/orchestrator/stages/triage-stage.js +0 -172
- package/src/orchestrator/standby.js +0 -71
- package/src/orchestrator/verification-gate.js +0 -183
- package/src/orchestrator.js +0 -22
- package/src/plan/adr-generator.js +0 -170
- package/src/plan/adr-loader.js +0 -60
- package/src/plan/plan-executor.js +0 -100
- package/src/plan/plan-validation.js +0 -4
- package/src/prompts/acceptance-coverage.js +0 -32
- package/src/prompts/arbiter.js +0 -38
- package/src/prompts/architect.js +0 -103
- package/src/prompts/hu-reviewer.js +0 -244
- package/src/prompts/tool-correctness-judge.js +0 -31
- package/src/prompts/triage.js +0 -57
- package/src/rag/embedder.js +0 -2
- package/src/rag/embedders/_cloud-base.js +0 -2
- package/src/rag/embedders/cohere.js +0 -2
- package/src/rag/embedders/mistral.js +0 -2
- package/src/rag/embedders/onnx.js +0 -2
- package/src/rag/embedders/openai.js +0 -2
- package/src/rag/embedders/voyage.js +0 -2
- package/src/rag/rerank.js +0 -4
- package/src/rag/where-parser.js +0 -4
- package/src/repeat-detector.js +0 -88
- package/src/review/diff-generator.js +0 -112
- package/src/review/git-stash-helper.js +0 -104
- package/src/review/schema.js +0 -31
- package/src/review/scope-filter.js +0 -152
- package/src/review/snapshot-diff.js +0 -122
- package/src/review/tdd-discipline.js +0 -90
- package/src/review/tdd-policy.js +0 -69
- package/src/roles/acceptance-coverage-role.js +0 -78
- package/src/roles/architect-role.js +0 -77
- package/src/roles/coder-role.js +0 -113
- package/src/roles/commiter-role.js +0 -113
- package/src/roles/domain-curator-role.js +0 -134
- package/src/roles/hu-reviewer-role.js +0 -64
- package/src/roles/impeccable-role.js +0 -85
- package/src/roles/index.js +0 -18
- package/src/roles/karajan-brain-role.js +0 -134
- package/src/roles/perf-role.js +0 -122
- package/src/roles/planner-role.js +0 -151
- package/src/roles/refactorer-role.js +0 -30
- package/src/roles/repairer-role.js +0 -181
- package/src/roles/researcher-role.js +0 -70
- package/src/roles/reviewer-role.js +0 -115
- package/src/roles/security-role.js +0 -73
- package/src/roles/solomon-role.js +0 -271
- package/src/roles/sonar-role.js +0 -72
- package/src/roles/tester-role.js +0 -235
- package/src/roles/tool-correctness-judge-role.js +0 -78
- package/src/roles/triage-role.js +0 -99
- package/src/session/journal/decisions-writer.js +0 -200
- package/src/session/journal/iteration-logger.js +0 -194
- package/src/session/journal/summary-writer.js +0 -320
- package/src/session/journal/tree-writer.js +0 -164
- package/src/session/journal/write-all.js +0 -89
- package/src/session-cleanup.js +0 -116
- package/src/skills/semantic-detector.js +0 -167
- package/src/skills/skill-detector.js +0 -385
- package/src/sonar/cloud-scanner.js +0 -78
- package/src/tracker-bootstrap.js +0 -44
- package/src/types/index.js +0 -38
- package/src/types/policy.js +0 -43
- package/src/types/solomon.js +0 -87
- package/src/utils/async-lock.js +0 -31
- package/src/utils/budget.js +0 -229
- package/src/utils/cwd-confirm.js +0 -39
- package/src/utils/display/event-handlers.js +0 -403
- package/src/utils/display/formatters.js +0 -132
- package/src/utils/display/header.js +0 -47
- package/src/utils/display/session-summary.js +0 -118
- package/src/utils/display/solomon.js +0 -42
- package/src/utils/pricing.js +0 -29
- package/src/utils/quota-simulator.js +0 -56
- package/src/utils/role-assigner.js +0 -137
- package/src/utils/rtk-wrapper.js +0 -87
- package/src/utils/run-registry.js +0 -9
- package/src/utils/status-dashboard.js +0 -254
- package/src/utils/tool-call-extractor.js +0 -56
- package/src/webperf/devtools-detect.js +0 -112
- package/src/webperf/webperf-role-integration.js +0 -66
- package/templates/skills/kj-run.md +0 -73
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* and says so: the coder learns the card exists and where to ask for it,
|
|
13
13
|
* instead of receiving a silently empty context.
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
15
|
+
import { findBoardHu } from "../hu/board-lookup.js";
|
|
16
16
|
|
|
17
17
|
/** Ids minted by the HU Board; anything else belongs to an external board. */
|
|
18
18
|
const LOCAL_HU = /^HU-/i;
|
|
@@ -25,9 +25,14 @@ const externalSection = (ref) =>
|
|
|
25
25
|
"Treat the task below as the card's statement, and if something the card should answer is missing, ASK instead of guessing.",
|
|
26
26
|
].join("\n");
|
|
27
27
|
|
|
28
|
+
// KJC-BUG-0454: a board card keeps its criteria as a list; older HUs as one string.
|
|
29
|
+
const criteriaOf = (hu) => (Array.isArray(hu.acceptance_criteria) ? hu.acceptance_criteria.join("\n") : hu.acceptanceCriteria ?? "").trim();
|
|
30
|
+
const idOf = (hu) => hu.short_id || hu.id;
|
|
31
|
+
|
|
28
32
|
const localSection = (hu) => {
|
|
29
|
-
const lines = [`## Card ${hu
|
|
33
|
+
const lines = [`## Card ${idOf(hu)} — ${hu.title}`, ""];
|
|
30
34
|
if (hu.description?.trim()) lines.push(hu.description.trim(), "");
|
|
35
|
+
if (hu.scope?.trim()) lines.push(`Scope: ${hu.scope.trim()}`, "");
|
|
31
36
|
lines.push(`Status on the board: ${hu.status}. The card is the contract: done means its statement is literally true.`);
|
|
32
37
|
return lines.join("\n");
|
|
33
38
|
};
|
|
@@ -45,10 +50,10 @@ export async function resolveCardContext({ projectDir, ref, deps = {} }) {
|
|
|
45
50
|
}
|
|
46
51
|
// A missing local HU is an error, never an empty context: the caller asked
|
|
47
52
|
// for a card by id and kj either delivers it or says it does not exist.
|
|
48
|
-
const hu = await (deps.getHu ||
|
|
49
|
-
const criteria = hu
|
|
53
|
+
const hu = await (deps.getHu || findBoardHu)(projectDir, id);
|
|
54
|
+
const criteria = criteriaOf(hu);
|
|
50
55
|
return {
|
|
51
|
-
huId: hu
|
|
56
|
+
huId: idOf(hu),
|
|
52
57
|
title: hu.title,
|
|
53
58
|
section: localSection(hu),
|
|
54
59
|
acceptanceTests: criteria ? [{ type: "gherkin", content: criteria }] : null,
|
package/src/prompts/reviewer.js
CHANGED
|
@@ -35,7 +35,7 @@ const SERENA_INSTRUCTIONS = [
|
|
|
35
35
|
* on it; the per-review content (task context + git diff, up to 12 KB
|
|
36
36
|
* and always different) goes last.
|
|
37
37
|
*/
|
|
38
|
-
export async function buildReviewerPromptLayout({ task, diff, reviewRules, mode, serenaEnabled = false, rtkAvailable = false, productContext = null, domainContext = null, projectDir = null, language = "en", provider = null }) {
|
|
38
|
+
export async function buildReviewerPromptLayout({ task, diff, reviewRules, mode, serenaEnabled = false, rtkAvailable = false, productContext = null, domainContext = null, projectDir = null, language = "en", provider = null, roleSkillBlock = null }) {
|
|
39
39
|
const { body: clippedDiff, note: clipNote } = clipDiff(diff);
|
|
40
40
|
// KJC-BUG-0138: full diff on purpose — the moved-to side survives clipping.
|
|
41
41
|
const splitSignal = buildSplitSignal(diff);
|
|
@@ -44,7 +44,10 @@ export async function buildReviewerPromptLayout({ task, diff, reviewRules, mode,
|
|
|
44
44
|
const rtkSnippet = buildRtkInstructions({ rtkAvailable });
|
|
45
45
|
// KJC-TSK-1065: the reviewer gets the skills of its role and of the task, up to 3.
|
|
46
46
|
let skillSection = null;
|
|
47
|
-
|
|
47
|
+
// KJC-TSK-1272: a block the caller resolved (a third-party repo's stack) wins over the project's.
|
|
48
|
+
if (roleSkillBlock) {
|
|
49
|
+
skillSection = buildRoleSkillSection(roleSkillBlock, { provider }) || null;
|
|
50
|
+
} else if (projectDir) {
|
|
48
51
|
skillSection = buildRoleSkillSection(await roleSkills({ role: "reviewer", task, projectDir }), { provider }) || null;
|
|
49
52
|
}
|
|
50
53
|
|
package/src/rag/auto-update.js
CHANGED
|
@@ -31,6 +31,57 @@ const countSources = (db, slug, projectDir) => {
|
|
|
31
31
|
export const DEFAULT_MAX_DRIFT_FILES = 25;
|
|
32
32
|
const HOOK_SRC = resolve(fileURLToPath(import.meta.url), "../../../scripts/git-hooks/post-merge");
|
|
33
33
|
|
|
34
|
+
// KJC-TSK-1174 (RAG-A2): no index yet. It is built in the background, once; the session
|
|
35
|
+
// does not wait. A recent attempt that died (no Ollama) is not retried blindly.
|
|
36
|
+
function bootstrapInBackground({ projectDir, slug, head, logger }) {
|
|
37
|
+
const boot = startBootstrap(projectDir);
|
|
38
|
+
if (boot.busy) {
|
|
39
|
+
warnOnce(logger, `bootstrap-busy:${projectDir}`, `[rag] ${slug} has no index yet: another background index is running (${boot.busy.slug ?? "?"}, pid ${boot.busy.pid ?? "?"}); it is tried again on the next command`);
|
|
40
|
+
return { skipped: true, reason: "bootstrap-queued", holder: boot.busy, head };
|
|
41
|
+
}
|
|
42
|
+
if (boot.recent) {
|
|
43
|
+
warnOnce(logger, `bootstrap:${projectDir}`, `[rag] ${slug} has no index and the background indexing did not finish: run kj rag index --with-sources`);
|
|
44
|
+
return { skipped: true, reason: "bootstrap-failed", head };
|
|
45
|
+
}
|
|
46
|
+
warnOnce(logger, `bootstrap:${projectDir}`, `[rag] ${slug} has no index yet: indexing it in the background (pid ${boot.pid}); queries answer from it once it is done`);
|
|
47
|
+
return { skipped: true, reason: "bootstrapping", pid: boot.pid, head };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// KJC-TSK-1171 (RAG-B): an index built before the embedded header holds vectors a
|
|
51
|
+
// query by file or symbol name cannot reach. Small, it is rebuilt here; large, the
|
|
52
|
+
// query answers from it and says once how to rebuild.
|
|
53
|
+
async function rebuildOldSchema({ db, slug, projectDir, config, logger, head, maxFiles }, schema) {
|
|
54
|
+
const sources = countSources(db, slug, projectDir);
|
|
55
|
+
if (sources > maxFiles) {
|
|
56
|
+
warnOnce(logger, projectDir, `[rag] the index of ${slug} predates the embedded header (schema ${schema} < ${EMBED_SCHEMA}): answering from it as it is; rebuild it with kj rag index --with-sources`);
|
|
57
|
+
return { skipped: true, reason: "embed-schema-old", sources, head };
|
|
58
|
+
}
|
|
59
|
+
logger.info?.(`[rag] the index of ${slug} predates the embedded header (schema ${schema} < ${EMBED_SCHEMA}); rebuilding ${sources} source(s)`);
|
|
60
|
+
const totals = await indexProject(projectDir, { db, embedder: makeGovernedEmbedder(config), karajanHome: getKarajanHome(), logger, withSources: true, exclude: ragExclude(config) });
|
|
61
|
+
if (totals.head) setLastIndexedCommit(db, slug, totals.head);
|
|
62
|
+
return { ran: true, rebuilt: true, totals };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// KJC-TSK-1174 (RAG-A1): the drift is measured up to the files on disk, untracked ones
|
|
66
|
+
// included, so a query sees what the session is writing before it is committed.
|
|
67
|
+
// The dirty paths of the last refresh count too: reverted or deleted since, they hold stale chunks.
|
|
68
|
+
async function deltaUpdate({ db, slug, projectDir, config, logger, head, maxFiles }, since) {
|
|
69
|
+
const { stdout } = await execa("git", ["-C", projectDir, "diff", "--name-only", "-z", since]);
|
|
70
|
+
const dirty = [...stdout.split("\0").filter(Boolean), ...(await untrackedFiles(projectDir))];
|
|
71
|
+
const reconcile = readRefreshedPaths(projectDir).filter((p) => !dirty.includes(p));
|
|
72
|
+
const changed = dirty.length + reconcile.length;
|
|
73
|
+
if (changed === 0) return { skipped: true, head };
|
|
74
|
+
if (changed > maxFiles) {
|
|
75
|
+
logger.warn?.(`[rag] ${changed} file(s) changed since the last index (more than ${maxFiles}): answering from the index as it is; bring it up to date with kj rag index --since auto`);
|
|
76
|
+
return { skipped: true, reason: "drift-too-large", changed, head };
|
|
77
|
+
}
|
|
78
|
+
logger.info?.(`[rag] drift detected (${since.slice(0, 7)} → ${head.slice(0, 7)}, ${changed} file(s)); running delta update`);
|
|
79
|
+
const totals = await indexProjectDelta(projectDir, { db, embedder: makeGovernedEmbedder(config), since, logger, exclude: ragExclude(config), worktree: true, reconcile });
|
|
80
|
+
if (totals.head) setLastIndexedCommit(db, slug, totals.head);
|
|
81
|
+
writeRefreshedPaths(projectDir, dirty);
|
|
82
|
+
return { ran: true, totals };
|
|
83
|
+
}
|
|
84
|
+
|
|
34
85
|
export async function maybeAutoUpdate({ projectDir, config, logger = console, flags = {} } = {}) {
|
|
35
86
|
// commander turns `--no-rag-update` into flags.ragUpdate === false.
|
|
36
87
|
if (flags?.ragUpdate === false || config?.rag?.autoUpdate?.onRun === false) return { skipped: true };
|
|
@@ -42,53 +93,15 @@ export async function maybeAutoUpdate({ projectDir, config, logger = console, fl
|
|
|
42
93
|
const db = openProjectStore({ projectDir, dim: config?.rag?.embedder?.dim || 768 });
|
|
43
94
|
try {
|
|
44
95
|
const since = getLastIndexedCommit(db, slug);
|
|
45
|
-
if (!since) {
|
|
46
|
-
// KJC-TSK-1174 (RAG-A2): no index yet. It is built in the background, once; the session
|
|
47
|
-
// does not wait. A recent attempt that died (no Ollama) is not retried blindly.
|
|
48
|
-
const boot = startBootstrap(projectDir);
|
|
49
|
-
if (boot.recent) {
|
|
50
|
-
warnOnce(logger, `bootstrap:${projectDir}`, `[rag] ${slug} has no index and the background indexing did not finish: run kj rag index --with-sources`);
|
|
51
|
-
return { skipped: true, reason: "bootstrap-failed", head };
|
|
52
|
-
}
|
|
53
|
-
warnOnce(logger, `bootstrap:${projectDir}`, `[rag] ${slug} has no index yet: indexing it in the background (pid ${boot.pid}); queries answer from it once it is done`);
|
|
54
|
-
return { skipped: true, reason: "bootstrapping", pid: boot.pid, head };
|
|
55
|
-
}
|
|
96
|
+
if (!since) return bootstrapInBackground({ projectDir, slug, head, logger });
|
|
56
97
|
// KJC-BUG-0304: a drift of hundreds of files (an index weeks old) reindexed the
|
|
57
98
|
// repo for half an hour before the query answered. Past the bound the query
|
|
58
99
|
// answers from the index as it is, and says how to bring it up to date.
|
|
59
|
-
const
|
|
60
|
-
// KJC-TSK-1171 (RAG-B): an index built before the embedded header holds vectors a
|
|
61
|
-
// query by file or symbol name cannot reach. Small, it is rebuilt here; large, the
|
|
62
|
-
// query answers from it and says once how to rebuild.
|
|
100
|
+
const ctx = { db, slug, projectDir, config, logger, head, maxFiles: config?.rag?.autoUpdate?.maxFiles ?? DEFAULT_MAX_DRIFT_FILES };
|
|
63
101
|
const schema = readEmbedSchema(projectDir);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
warnOnce(logger, projectDir, `[rag] the index of ${slug} predates the embedded header (schema ${schema} < ${EMBED_SCHEMA}): answering from it as it is; rebuild it with kj rag index --with-sources`);
|
|
68
|
-
return { skipped: true, reason: "embed-schema-old", sources, head };
|
|
69
|
-
}
|
|
70
|
-
logger.info?.(`[rag] the index of ${slug} predates the embedded header (schema ${schema} < ${EMBED_SCHEMA}); rebuilding ${sources} source(s)`);
|
|
71
|
-
const totals = await indexProject(projectDir, { db, embedder: makeGovernedEmbedder(config), karajanHome: getKarajanHome(), logger, withSources: true, exclude: ragExclude(config) });
|
|
72
|
-
if (totals.head) setLastIndexedCommit(db, slug, totals.head);
|
|
73
|
-
return { ran: true, rebuilt: true, totals };
|
|
74
|
-
}
|
|
75
|
-
// KJC-TSK-1174 (RAG-A1): the drift is measured up to the files on disk, untracked ones
|
|
76
|
-
// included, so a query sees what the session is writing before it is committed.
|
|
77
|
-
// The dirty paths of the last refresh count too: reverted or deleted since, they hold stale chunks.
|
|
78
|
-
const { stdout } = await execa("git", ["-C", projectDir, "diff", "--name-only", "-z", since]);
|
|
79
|
-
const dirty = [...stdout.split("\0").filter(Boolean), ...(await untrackedFiles(projectDir))];
|
|
80
|
-
const reconcile = readRefreshedPaths(projectDir).filter((p) => !dirty.includes(p));
|
|
81
|
-
const changed = dirty.length + reconcile.length;
|
|
82
|
-
if (changed === 0) return { skipped: true, head };
|
|
83
|
-
if (changed > maxFiles) {
|
|
84
|
-
logger.warn?.(`[rag] ${changed} file(s) changed since the last index (more than ${maxFiles}): answering from the index as it is; bring it up to date with kj rag index --since auto`);
|
|
85
|
-
return { skipped: true, reason: "drift-too-large", changed, head };
|
|
86
|
-
}
|
|
87
|
-
logger.info?.(`[rag] drift detected (${since.slice(0, 7)} → ${head.slice(0, 7)}, ${changed} file(s)); running delta update`);
|
|
88
|
-
const totals = await indexProjectDelta(projectDir, { db, embedder: makeGovernedEmbedder(config), since, logger, exclude: ragExclude(config), worktree: true, reconcile });
|
|
89
|
-
if (totals.head) setLastIndexedCommit(db, slug, totals.head);
|
|
90
|
-
writeRefreshedPaths(projectDir, dirty);
|
|
91
|
-
return { ran: true, totals };
|
|
102
|
+
// Awaited here, inside the try: the catch must see their errors and the finally must not close the store under them.
|
|
103
|
+
if (schema < EMBED_SCHEMA) return await rebuildOldSchema(ctx, schema);
|
|
104
|
+
return await deltaUpdate(ctx, since);
|
|
92
105
|
} catch (err) {
|
|
93
106
|
// Said once per process: an embedder that is down would repeat it on every query otherwise.
|
|
94
107
|
warnOnce(logger, `failed:${projectDir}`, `[rag] auto-update failed (${err.message}); continuing without refresh`);
|
|
@@ -1,52 +1,28 @@
|
|
|
1
1
|
// KJC-TSK-1174 (RAG-A2): a project with no index gets one in the background, once, at the first
|
|
2
2
|
// command that needs it. The session goes on; queries answer from the index when it is done.
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
-
import { mkdirSync,
|
|
4
|
+
import { mkdirSync, rmdirSync, unlinkSync } from "node:fs";
|
|
5
5
|
import { dirname, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { projectDbPath } from "./project-store.js";
|
|
8
|
+
import { projectSlug } from "./vec-store.js";
|
|
9
|
+
import { GRACE_MS, MAX_RUN_MS, indexLeasePath, readLease, takeLock, unlessGone, withMutex, writeLease } from "./pid-lease.js";
|
|
8
10
|
|
|
9
11
|
const KJ_BIN = resolve(fileURLToPath(import.meta.url), "../../../bin/kj.js");
|
|
10
12
|
// An attempt that died (no Ollama, no model) is not repeated on every command: this long apart.
|
|
11
13
|
export const RETRY_MS = 10 * 60 * 1000;
|
|
12
|
-
|
|
13
|
-
export const GRACE_MS = 30 * 1000;
|
|
14
|
-
// An indexer is not trusted to run longer than this: past it, its pid may belong to someone else.
|
|
15
|
-
export const MAX_RUN_MS = 60 * 60 * 1000;
|
|
14
|
+
export { GRACE_MS, MAX_RUN_MS };
|
|
16
15
|
const markerPath = (projectDir) => `${projectDbPath(projectDir)}.bootstrap`;
|
|
17
16
|
|
|
18
|
-
const alive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } };
|
|
19
|
-
|
|
20
17
|
/** What the marker says of the last attempt: `{ pid, at, running }`, or null when none was ever made. */
|
|
21
|
-
const
|
|
22
|
-
try { return fn(); }
|
|
23
|
-
catch (err) {
|
|
24
|
-
if (err.code === "ENOENT") { return null; }
|
|
25
|
-
throw err;
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export function readBootstrap(projectDir, { now = Date.now() } = {}) {
|
|
30
|
-
const file = markerPath(projectDir);
|
|
31
|
-
// Read first, never check-then-read: another command may set the marker aside in between.
|
|
32
|
-
const raw = unlessGone(() => readFileSync(file, "utf8"));
|
|
33
|
-
if (raw === null) return null;
|
|
34
|
-
try {
|
|
35
|
-
const note = JSON.parse(raw);
|
|
36
|
-
return { ...note, running: alive(note.pid) && now - note.at < MAX_RUN_MS };
|
|
37
|
-
} catch {
|
|
38
|
-
// Another command is writing it this very moment: running for a moment; past the grace, a crash.
|
|
39
|
-
const stat = unlessGone(() => statSync(file));
|
|
40
|
-
return stat === null ? null : { pid: null, at: 0, running: now - stat.mtimeMs < GRACE_MS };
|
|
41
|
-
}
|
|
42
|
-
}
|
|
18
|
+
export const readBootstrap = (projectDir, o) => readLease(markerPath(projectDir), o);
|
|
43
19
|
|
|
44
20
|
/**
|
|
45
21
|
* Starts `kj rag index --with-sources` detached, or reuses the one running. Returns
|
|
46
22
|
* `{ started, pid }`, or `{ started: false, recent: true }` when the last attempt died less
|
|
47
23
|
* than RETRY_MS ago: the index is then asked for by hand instead of being retried blindly.
|
|
48
24
|
*/
|
|
49
|
-
export function startBootstrap(projectDir, { spawnFn = spawn, bin = KJ_BIN, now = Date.now() } = {}) {
|
|
25
|
+
export function startBootstrap(projectDir, { spawnFn = spawn, bin = KJ_BIN, now = Date.now(), lease = indexLeasePath() } = {}) {
|
|
50
26
|
const file = markerPath(projectDir);
|
|
51
27
|
mkdirSync(dirname(file), { recursive: true });
|
|
52
28
|
// One claimant at a time: the lock directory is created atomically, so of two commands arriving
|
|
@@ -58,29 +34,27 @@ export function startBootstrap(projectDir, { spawnFn = spawn, bin = KJ_BIN, now
|
|
|
58
34
|
const last = readBootstrap(projectDir, { now });
|
|
59
35
|
if (last?.running) return { started: false, pid: last.pid };
|
|
60
36
|
if (last && now - last.at < RETRY_MS) return { started: false, recent: true, pid: null };
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
37
|
+
// One indexer per machine: the lease is taken inside the project mutex, before the marker is
|
|
38
|
+
// written, so a queued project does not spend its RETRY_MS. Both mutexes yield, never wait.
|
|
39
|
+
const slug = projectSlug(projectDir);
|
|
40
|
+
const res = withMutex(lease, now, () => {
|
|
41
|
+
const holder = readLease(lease, { now });
|
|
42
|
+
if (holder?.running && holder.pid !== process.pid) return { started: false, busy: { projectDir: holder.projectDir, slug: holder.slug, pid: holder.pid } };
|
|
43
|
+
// Marker and lease land whole each time: first with the claimant's pid, so they read as
|
|
44
|
+
// running meanwhile, then with the indexer's.
|
|
45
|
+
const note = (pid) => { writeLease(file, { pid, at: now }); writeLease(lease, { pid, at: now, projectDir, slug }); };
|
|
46
|
+
note(process.pid);
|
|
47
|
+
try {
|
|
48
|
+
const child = spawnFn(process.execPath, [bin, "rag", "index", "--with-sources"], { cwd: projectDir, detached: true, stdio: "ignore" });
|
|
49
|
+
child.unref?.();
|
|
50
|
+
note(child.pid);
|
|
51
|
+
return { started: true, pid: child.pid };
|
|
52
|
+
} catch (err) {
|
|
53
|
+
// No indexer exists: neither the marker nor the lease may keep naming this (maybe long-lived) process.
|
|
54
|
+
unlessGone(() => unlinkSync(file)); unlessGone(() => unlinkSync(lease));
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return res.busy === true ? { started: false, busy: {} } : res;
|
|
69
59
|
} finally { unlessGone(() => rmdirSync(lock)); }
|
|
70
60
|
}
|
|
71
|
-
|
|
72
|
-
const takeLock = (lock, now) => {
|
|
73
|
-
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
74
|
-
try { mkdirSync(lock); return true; }
|
|
75
|
-
catch (err) {
|
|
76
|
-
if (err.code !== "EEXIST") throw err;
|
|
77
|
-
const held = unlessGone(() => statSync(lock));
|
|
78
|
-
if (held && now - held.mtimeMs < GRACE_MS) return false;
|
|
79
|
-
// The holder crashed. The lock is set aside under a unique name first: only the one caller
|
|
80
|
-
// whose rename succeeds removes it, so nobody removes a lock another caller has just taken.
|
|
81
|
-
const aside = `${lock}.${process.pid}.${now}.stale`;
|
|
82
|
-
if (unlessGone(() => renameSync(lock, aside)) !== null) rmdirSync(aside);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return false;
|
|
86
|
-
};
|
package/src/rag/migrate.js
CHANGED
|
@@ -10,7 +10,7 @@ import { existsSync } from "node:fs";
|
|
|
10
10
|
import { sep } from "node:path";
|
|
11
11
|
|
|
12
12
|
import { insertChunk, getLastIndexedCommit, setLastIndexedCommit } from "./vec-store.js";
|
|
13
|
-
import {
|
|
13
|
+
import { openVecStore } from "./vec-store.js";
|
|
14
14
|
|
|
15
15
|
export const countProjectChunks = (db, slug) => db.prepare("SELECT COUNT(*) AS n FROM chunks WHERE project_slug = ?").get(slug).n;
|
|
16
16
|
|
package/src/rag/onnx-fallback.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import fs from "node:fs/promises";
|
|
10
10
|
import { existsSync, rmSync } from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
|
-
import
|
|
12
|
+
import { openDatabase } from "karajan-core/db";
|
|
13
13
|
import { parseDocument } from "yaml";
|
|
14
14
|
import { getProjectConfigPath } from "../config/loader.js";
|
|
15
15
|
import { projectDbPath } from "./project-store.js";
|
|
@@ -31,7 +31,7 @@ export function resetEmptyStore(projectDir = process.cwd()) {
|
|
|
31
31
|
if (!existsSync(storePath)) return true;
|
|
32
32
|
// Read-only, schema-agnostic inspection: no DDL, no vec extension, no
|
|
33
33
|
// dimension assumptions — the store is only ever OPENED to be counted.
|
|
34
|
-
const db =
|
|
34
|
+
const db = openDatabase(storePath, { readOnly: true });
|
|
35
35
|
try {
|
|
36
36
|
const hasChunksTable = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='chunks'").get();
|
|
37
37
|
if (hasChunksTable && db.prepare("SELECT COUNT(*) AS n FROM chunks").get().n > 0) return false;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// HU-2757: a lease names the pid that holds a resource and when it took it. Nobody releases it:
|
|
2
|
+
// a dead pid, or an age of MAX_RUN_MS, means free. A mkdir mutex makes the decision atomic.
|
|
3
|
+
import { mkdirSync, readFileSync, renameSync, rmdirSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { getKarajanHome } from "../utils/paths.js";
|
|
6
|
+
|
|
7
|
+
// A lease that does not parse, or a lock, older than this was left by a crashed command: taken over.
|
|
8
|
+
export const GRACE_MS = 30 * 1000;
|
|
9
|
+
// An indexer is not trusted to run longer than this: past it, its pid may belong to someone else.
|
|
10
|
+
export const MAX_RUN_MS = 60 * 60 * 1000;
|
|
11
|
+
|
|
12
|
+
export const indexLeasePath = () => join(getKarajanHome(), "locks", "rag-index.json");
|
|
13
|
+
|
|
14
|
+
// EPERM (a pid of another user) counts as dead: a single-user machine has no such indexer.
|
|
15
|
+
const alive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } };
|
|
16
|
+
|
|
17
|
+
export const unlessGone = (fn) => {
|
|
18
|
+
try { return fn(); }
|
|
19
|
+
catch (err) {
|
|
20
|
+
if (err.code === "ENOENT") { return null; }
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** What the lease says: `{ pid, at, running, ... }`, or null when none was ever written. */
|
|
26
|
+
export function readLease(file, { now = Date.now() } = {}) {
|
|
27
|
+
// Read first, never check-then-read: another command may set the file aside in between.
|
|
28
|
+
const raw = unlessGone(() => readFileSync(file, "utf8"));
|
|
29
|
+
if (raw === null) return null;
|
|
30
|
+
try {
|
|
31
|
+
const note = JSON.parse(raw);
|
|
32
|
+
return { ...note, running: alive(note.pid) && now - note.at < MAX_RUN_MS };
|
|
33
|
+
} catch {
|
|
34
|
+
// Another command is writing it this very moment: running for a moment; past the grace, a crash.
|
|
35
|
+
const stat = unlessGone(() => statSync(file));
|
|
36
|
+
return stat === null ? null : { pid: null, at: 0, running: now - stat.mtimeMs < GRACE_MS };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The lease lands whole: written apart, then renamed. */
|
|
41
|
+
export function writeLease(file, record) {
|
|
42
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
43
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
44
|
+
writeFileSync(tmp, JSON.stringify(record), "utf8");
|
|
45
|
+
renameSync(tmp, file);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const takeLock = (lock, now) => {
|
|
49
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
50
|
+
try { mkdirSync(lock); return true; }
|
|
51
|
+
catch (err) {
|
|
52
|
+
if (err.code !== "EEXIST") throw err;
|
|
53
|
+
const held = unlessGone(() => statSync(lock));
|
|
54
|
+
if (held && now - held.mtimeMs < GRACE_MS) return false;
|
|
55
|
+
// The holder crashed. The lock is set aside under a unique name first: only the one caller
|
|
56
|
+
// whose rename succeeds removes it, so nobody removes a lock another caller has just taken.
|
|
57
|
+
const aside = `${lock}.${process.pid}.${now}.stale`;
|
|
58
|
+
if (unlessGone(() => renameSync(lock, aside)) !== null) rmdirSync(aside);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Runs `fn` under the mutex `${file}.lock`, or returns `{ busy: true }` when another command holds it. */
|
|
65
|
+
export function withMutex(file, now, fn) {
|
|
66
|
+
const lock = `${file}.lock`;
|
|
67
|
+
mkdirSync(dirname(lock), { recursive: true });
|
|
68
|
+
if (!takeLock(lock, now)) return { busy: true };
|
|
69
|
+
try { return fn(); } finally { unlessGone(() => rmdirSync(lock)); }
|
|
70
|
+
}
|
package/src/rag/project-store.js
CHANGED
|
@@ -17,7 +17,6 @@ import { execFileSync } from "node:child_process";
|
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
|
|
19
19
|
import { openVecStore } from "./vec-store.js";
|
|
20
|
-
import { explainNativeLoadError } from "../checks/native-build.js";
|
|
21
20
|
import { getKarajanHome } from "../utils/paths.js";
|
|
22
21
|
|
|
23
22
|
function projectRoot(dir) {
|
|
@@ -40,20 +39,11 @@ export function libraryDbPath() {
|
|
|
40
39
|
return process.env.KJ_LIBRARY_DB || path.join(getKarajanHome(), "library.db");
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
// KJC-BUG-0426: an unbuilt better-sqlite3 addon says how to build it, not "bindings file".
|
|
44
|
-
// Every opening of a RAG store goes through here.
|
|
45
|
-
export function openVecStoreExplained(opts) {
|
|
46
|
-
try {
|
|
47
|
-
return openVecStore(opts);
|
|
48
|
-
} catch (err) {
|
|
49
|
-
throw explainNativeLoadError(err);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
42
|
|
|
53
43
|
export function openLibraryStore({ dim = 768 } = {}) {
|
|
54
|
-
return
|
|
44
|
+
return openVecStore({ dim, path: libraryDbPath() });
|
|
55
45
|
}
|
|
56
46
|
|
|
57
47
|
export function openProjectStore({ projectDir = process.cwd(), dim = 768 } = {}) {
|
|
58
|
-
return
|
|
48
|
+
return openVecStore({ dim, path: projectDbPath(projectDir) });
|
|
59
49
|
}
|
package/src/review/card-first.js
CHANGED
|
@@ -41,7 +41,8 @@ async function findLiveCardInBranch(projectDir, branch) {
|
|
|
41
41
|
const plan = await loadPlan(projectDir, meta.planId);
|
|
42
42
|
for (const h of plan.hus || []) {
|
|
43
43
|
// KJC-BUG-0353: an HU is also named by the id of its replica in an external tracker.
|
|
44
|
-
|
|
44
|
+
// KJC-BUG-0443: a shared-board HU carries "repo#n · ref"; each part names it.
|
|
45
|
+
for (const ref of [h.short_id, h.id, ...String(h.external_ref ?? "").split(" · ")]) {
|
|
45
46
|
if (ref && refInBranch(ref, needle)) {
|
|
46
47
|
if (LIVE_STATUSES.has(h.status)) return { ref, live: true, status: h.status };
|
|
47
48
|
closedHit = { ref, live: false, status: h.status };
|
|
@@ -52,7 +53,12 @@ async function findLiveCardInBranch(projectDir, branch) {
|
|
|
52
53
|
return closedHit;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
// A failed check under the effective policy: blocks, or passes with a warning.
|
|
57
|
+
const failedCheck = (effective, reason, extra = {}) => (effective === "block"
|
|
58
|
+
? { ok: false, mode: "block", reason, ...extra }
|
|
59
|
+
: { ok: true, mode: "warn", reason, ...extra });
|
|
60
|
+
|
|
61
|
+
function exemption(config, branch) {
|
|
56
62
|
const exempt = config.method_gates?.card_first_exempt_branches || DEFAULT_EXEMPT_PREFIXES;
|
|
57
63
|
if (exempt.some((p) => branch.startsWith(p))) {
|
|
58
64
|
return { ok: true, mode: "exempt", reason: `branch prefix exempt (${branch.split("/")[0]}/…)` };
|
|
@@ -62,59 +68,64 @@ export async function checkCardFirst({ config = {}, projectDir = process.cwd(),
|
|
|
62
68
|
if ([config.base_branch || "main", "main", "master", "HEAD"].includes(branch)) {
|
|
63
69
|
return { ok: true, mode: "exempt", reason: "base branch — the branch-first gate owns this case" };
|
|
64
70
|
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
65
73
|
|
|
66
|
-
|
|
67
|
-
const
|
|
74
|
+
async function huBoardCheck({ projectDir, branch, policy }) {
|
|
75
|
+
const hit = await findLiveCardInBranch(projectDir, branch);
|
|
76
|
+
if (hit?.live) return { ok: true, mode: "pass", ref: hit.ref };
|
|
77
|
+
const reason = hit
|
|
78
|
+
? `branch references ${hit.ref} but its card is "${hit.status}" — card-first needs a live card (reopen it or create one: kj hu add)`
|
|
79
|
+
: `no live card referenced in branch "${branch}" — create it first (kj hu add "…" --id <REF>) and name the branch after it`;
|
|
80
|
+
return failedCheck(policy === "auto" ? "block" : policy, reason);
|
|
81
|
+
}
|
|
68
82
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return effective
|
|
77
|
-
? { ok: false, mode: "block", reason }
|
|
78
|
-
: { ok: true, mode: "warn", reason };
|
|
83
|
+
// A card-shaped reference in the branch, verified against the tracker when an adapter is declared.
|
|
84
|
+
async function trackerRefCheck({ config, projectDir, branch, deps, boardName, effective }) {
|
|
85
|
+
const ref = branch.match(CARD_REF_RE)[0];
|
|
86
|
+
const { verifyFn = verifyCardAlive } = deps;
|
|
87
|
+
const v = await verifyFn({ ref, verifyCmd: config.board?.verify_cmd, projectDir });
|
|
88
|
+
if (v.verified === false) {
|
|
89
|
+
const reason = `branch references ${ref} but ${boardName} reports it "${v.status}" — card-first needs a live card in the tracker (create/reopen it there)`;
|
|
90
|
+
return failedCheck(effective, reason, { level: v.level });
|
|
79
91
|
}
|
|
92
|
+
// KJC-TSK-0878 (issue #1371): "no he podido verificar" y "he verificado y
|
|
93
|
+
// esta mal" no son lo mismo, y ambas pueden merecer una parada. El proyecto
|
|
94
|
+
// decide si la primera para, y el mensaje dice cual de las dos es: un gate
|
|
95
|
+
// que aprueba lo que no ha comprobado da una confianza que no ha ganado.
|
|
96
|
+
// Sin adaptador declarado no hay nada que bloquear: la opcion no castiga a
|
|
97
|
+
// quien aun no tiene verify_cmd.
|
|
98
|
+
if (v.verified === null && config.board?.unverifiable === "block" && config.board?.verify_cmd) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false, mode: "block", level: v.level,
|
|
101
|
+
reason: `branch references ${ref} but the card could not be verified against ${boardName} — ${v.note} (board.unverifiable: block refuses to approve a check kj did not make)`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const huNote = `no live HU on the HU Board links ${ref}: kj hu add "…" --ref ${ref}`;
|
|
105
|
+
return { ok: true, mode: "pass", ref, level: v.level, note: v.note ? `${v.note}; ${huNote}` : huNote };
|
|
106
|
+
}
|
|
80
107
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
108
|
+
// planning-game / external: presence check, upgraded to LIVENESS against
|
|
109
|
+
// the real tracker when board.verify_cmd declares an adapter
|
|
110
|
+
// (KJC-TSK-0732, issue #1371). Unverifiable degrades honestly — the
|
|
111
|
+
// result names its level so nobody mistakes branch-ref for tracker.
|
|
112
|
+
async function externalBoardCheck({ config, projectDir, branch, deps, backend, policy }) {
|
|
85
113
|
const boardName = config.board?.name || backend;
|
|
86
114
|
const effective = policy === "auto" ? "warn" : policy;
|
|
87
115
|
// KJC-BUG-0353: the HU Board is always kj's board (KJC-BUG-0305) — a live HU
|
|
88
116
|
// the branch names, or whose replica it names, is checked here, not guessed.
|
|
89
117
|
const huHit = await findLiveCardInBranch(projectDir, branch);
|
|
90
118
|
if (huHit?.live) return { ok: true, mode: "pass", ref: huHit.ref, level: "hu-board" };
|
|
91
|
-
if (CARD_REF_RE.test(branch)) {
|
|
92
|
-
const ref = branch.match(CARD_REF_RE)[0];
|
|
93
|
-
const { verifyFn = verifyCardAlive } = deps;
|
|
94
|
-
const v = await verifyFn({ ref, verifyCmd: config.board?.verify_cmd, projectDir });
|
|
95
|
-
if (v.verified === false) {
|
|
96
|
-
const reason = `branch references ${ref} but ${boardName} reports it "${v.status}" — card-first needs a live card in the tracker (create/reopen it there)`;
|
|
97
|
-
return effective === "block"
|
|
98
|
-
? { ok: false, mode: "block", reason, level: v.level }
|
|
99
|
-
: { ok: true, mode: "warn", reason, level: v.level };
|
|
100
|
-
}
|
|
101
|
-
// KJC-TSK-0878 (issue #1371): "no he podido verificar" y "he verificado y
|
|
102
|
-
// esta mal" no son lo mismo, y ambas pueden merecer una parada. El proyecto
|
|
103
|
-
// decide si la primera para, y el mensaje dice cual de las dos es: un gate
|
|
104
|
-
// que aprueba lo que no ha comprobado da una confianza que no ha ganado.
|
|
105
|
-
// Sin adaptador declarado no hay nada que bloquear: la opcion no castiga a
|
|
106
|
-
// quien aun no tiene verify_cmd.
|
|
107
|
-
if (v.verified === null && config.board?.unverifiable === "block" && config.board?.verify_cmd) {
|
|
108
|
-
return {
|
|
109
|
-
ok: false, mode: "block", level: v.level,
|
|
110
|
-
reason: `branch references ${ref} but the card could not be verified against ${boardName} — ${v.note} (board.unverifiable: block refuses to approve a check kj did not make)`,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
const huNote = `no live HU on the HU Board links ${ref}: kj hu add "…" --ref ${ref}`;
|
|
114
|
-
return { ok: true, mode: "pass", ref, level: v.level, note: v.note ? `${v.note}; ${huNote}` : huNote };
|
|
115
|
-
}
|
|
119
|
+
if (CARD_REF_RE.test(branch)) return trackerRefCheck({ config, projectDir, branch, deps, boardName, effective });
|
|
116
120
|
const reason = `no card reference in branch "${branch}" — card-first on ${boardName}: create the card there and name the branch after it (e.g. feat/ABC-123-summary)`;
|
|
117
|
-
return effective
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
return failedCheck(effective, reason);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function checkCardFirst({ config = {}, projectDir = process.cwd(), branch, deps = {} }) {
|
|
125
|
+
const exempt = exemption(config, branch);
|
|
126
|
+
if (exempt) return exempt;
|
|
127
|
+
const backend = config.state_backend || "hu-board";
|
|
128
|
+
const policy = config.method_gates?.card_first || "auto";
|
|
129
|
+
if (backend === "hu-board") return huBoardCheck({ projectDir, branch, policy });
|
|
130
|
+
return externalBoardCheck({ config, projectDir, branch, deps, backend, policy });
|
|
120
131
|
}
|
|
@@ -47,10 +47,11 @@ function verdictOf(sides, findings, coverage) {
|
|
|
47
47
|
if (blockers.length) return { status: "rejected", approved: false, reason: `confirmed blocker(s): ${blockers.map(where).join(", ")}`, summary };
|
|
48
48
|
const rejecting = sides.filter((s) => !s.verdict?.approved).map((s) => s.reviewer);
|
|
49
49
|
if (rejecting.length === 0 && !coverage.truncated) return { status: "approved", approved: true, summary };
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return { status: "partial", approved: false, reason, summary };
|
|
50
|
+
const dispute = rejecting.length ? `${rejecting.join(" and ")} rejected, but no blocking finding was confirmed by the other family` : null;
|
|
51
|
+
// KJC-BUG-0453 (#2921): "partial" only when files were left out (they are in coverage.unreviewed);
|
|
52
|
+
// everything reviewed and the reviewers disagreeing is "disputed".
|
|
53
|
+
if (coverage.truncated) return { status: "partial", approved: false, reason: ["not every file was reviewed", dispute].filter(Boolean).join("; "), summary };
|
|
54
|
+
return { status: "disputed", approved: false, reason: dispute, summary };
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
/** The two sides (reviewer, model, verdict, coverage, findings, outcomes, verification) as one result. */
|
|
@@ -46,6 +46,14 @@ export const CONTRACT_BLOCK = [
|
|
|
46
46
|
"!.karajan/security-level.json",
|
|
47
47
|
// KJC-TSK-1163 (ADR 0025): which HU Board the repo uses; the team shares it.
|
|
48
48
|
"!.karajan/board.yml",
|
|
49
|
+
// ADR 0032 (KJC-TSK-1231): the team's own state, as the board: the rules the roles
|
|
50
|
+
// apply and atril's config with its lockfile. Shared plans travel in .karajan-shared/.
|
|
51
|
+
"!.karajan/coder-rules.md",
|
|
52
|
+
"!.karajan/review-rules.md",
|
|
53
|
+
// KJC-TSK-1234: the project config, once the pre-commit refuses it with a secret in it.
|
|
54
|
+
"!.karajan/kj.config.yml",
|
|
55
|
+
"!.karajan/atril.json",
|
|
56
|
+
"!.karajan/atril.lock.json",
|
|
49
57
|
];
|
|
50
58
|
|
|
51
59
|
/**
|
|
@@ -117,6 +125,8 @@ export async function excludeLocalArtifacts(projectDir, run = runCommand) {
|
|
|
117
125
|
}
|
|
118
126
|
}
|
|
119
127
|
|
|
128
|
+
const SHADOWING = new Set(["review-rules.md", "coder-rules.md"]);
|
|
129
|
+
|
|
120
130
|
export async function ensureGateTrackable(projectDir) {
|
|
121
131
|
const file = path.join(projectDir, ".gitignore");
|
|
122
132
|
let text;
|
|
@@ -126,14 +136,22 @@ export async function ensureGateTrackable(projectDir) {
|
|
|
126
136
|
// a partial state (e.g. a hand-written marker re-include without the hook
|
|
127
137
|
// ones) must still be completed, not silently accepted.
|
|
128
138
|
const reIncludes = CONTRACT_BLOCK.filter((l) => l.startsWith("!"));
|
|
129
|
-
if (reIncludes.every((l) => text.includes(l))) return { changed: false };
|
|
130
|
-
|
|
131
139
|
const isRootExclude = (l) => {
|
|
132
140
|
const t = l.trim();
|
|
133
141
|
return t === ".karajan/" || t === ".karajan" || t === ".karajan/*";
|
|
134
142
|
};
|
|
135
143
|
const lines = text.split("\n");
|
|
136
144
|
const idx = lines.findIndex(isRootExclude);
|
|
145
|
+
// ADR 0032: a bare rules line an older kj wrote after its re-include (or with none) undoes
|
|
146
|
+
// it, since the last match wins: it goes. One after the root exclude but before its own
|
|
147
|
+
// re-include was already overridden, so it is kept and moved ahead of the block, where it
|
|
148
|
+
// stays overridden. Lines before the root exclude are never touched.
|
|
149
|
+
const placed = (l, i) => {
|
|
150
|
+
if (idx === -1 || i <= idx || !SHADOWING.has(l.trim())) return "keep";
|
|
151
|
+
const reInclude = lines.findLastIndex((x) => x.trim() === `!.karajan/${l.trim()}`);
|
|
152
|
+
return reInclude === -1 || i > reInclude ? "drop" : "move";
|
|
153
|
+
};
|
|
154
|
+
if (reIncludes.every((l) => text.includes(l)) && lines.every((l, i) => placed(l, i) === "keep")) return { changed: false };
|
|
137
155
|
if (idx === -1) return { changed: false };
|
|
138
156
|
|
|
139
157
|
// Drop EVERY .karajan root exclude (a later duplicate would override the
|
|
@@ -142,9 +160,10 @@ export async function ensureGateTrackable(projectDir) {
|
|
|
142
160
|
// position of the first exclude. Exactly one coherent block, idempotent.
|
|
143
161
|
const partials = new Set(CONTRACT_BLOCK);
|
|
144
162
|
const marker = "@@kj-contract-block@@";
|
|
163
|
+
const moved = lines.filter((l, i) => placed(l, i) === "move");
|
|
145
164
|
const cleaned = lines.map((l, i) => (i === idx ? marker : l))
|
|
146
|
-
.filter((l) => l === marker || (!isRootExclude(l) && !partials.has(l.trim())));
|
|
147
|
-
const result = cleaned.flatMap((l) => (l === marker ? CONTRACT_BLOCK : [l]));
|
|
165
|
+
.filter((l, i) => l === marker || (!isRootExclude(l) && !partials.has(l.trim()) && placed(l, i) === "keep"));
|
|
166
|
+
const result = cleaned.flatMap((l) => (l === marker ? [...moved, ...CONTRACT_BLOCK] : [l]));
|
|
148
167
|
await fs.writeFile(file, result.join("\n"));
|
|
149
168
|
return { changed: true };
|
|
150
169
|
}
|