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
|
@@ -1,376 +1,31 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// Only fires on graceful exit; SIGKILL leaks are caught by the
|
|
30
|
-
// global-setup mtime purge at the start of the next run.
|
|
31
|
-
const parent = dirname(_vitestKjHome);
|
|
32
|
-
process.on('exit', () => {
|
|
33
|
-
try {
|
|
34
|
-
rmSync(parent, { recursive: true, force: true });
|
|
35
|
-
} catch {
|
|
36
|
-
// best-effort: never crash a test process on cleanup failure
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
return _vitestKjHome;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Returns the KJ home directory, respecting KJ_HOME env var.
|
|
44
|
-
* Under VITEST without KJ_HOME, returns a per-process tmp dir to
|
|
45
|
-
* prevent accidental writes into the developer's real `~/.karajan/`.
|
|
46
|
-
* @returns {string}
|
|
47
|
-
*/
|
|
48
|
-
export function getKjHome() {
|
|
49
|
-
// KJC-TSK-0420: same precedence chain as src/utils/paths.js::resolveHome():
|
|
50
|
-
// KARAJAN_HOME (canonical) > KJ_HOME (legacy) > VITEST tmp > ~/.karajan.
|
|
51
|
-
// Kept inline rather than cross-workspace imported so the hu-board package
|
|
52
|
-
// does not gain a new src/ dep; the precedence behaviour is the same.
|
|
53
|
-
if (process.env.KARAJAN_HOME) return process.env.KARAJAN_HOME;
|
|
54
|
-
if (process.env.KJ_HOME) return process.env.KJ_HOME;
|
|
55
|
-
if (process.env.VITEST) return vitestTmpKjHome();
|
|
56
|
-
return join(process.env.HOME || '/root', '.karajan');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Canonical path to the HU Board's run-logs directory
|
|
61
|
-
* (`<karajan-home>/hu-board-runs/`). Centralised here so the four
|
|
62
|
-
* board callers (plan-mutations, command-runner, api.js GET routes
|
|
63
|
-
* for plan + commandId logs) and the garbage-collector all agree on
|
|
64
|
-
* the same string — KJC-TSK-0421.
|
|
65
|
-
*
|
|
66
|
-
* @returns {string}
|
|
67
|
-
*/
|
|
68
|
-
export function getHuBoardRunsDir() {
|
|
69
|
-
return join(getKjHome(), 'hu-board-runs');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Canonical path to the plans directory the board scans
|
|
74
|
-
* (`<karajan-home>/plans/`). Respects `KJ_PLANS_DIR` as an absolute
|
|
75
|
-
* override, mirroring sync.js. KJC-BUG-0059 (v2.19.3): preflight
|
|
76
|
-
* endpoint used a hard-coded `~/.kj/plans/` legacy fallback that
|
|
77
|
-
* survived the v2.19.0 consolidation, causing the board to report
|
|
78
|
-
* "directorio del proyecto: no detectado" even when the plan on disk
|
|
79
|
-
* had a valid `projectDir`.
|
|
80
|
-
*
|
|
81
|
-
* @returns {string}
|
|
82
|
-
*/
|
|
83
|
-
export function getHuBoardPlansDir() {
|
|
84
|
-
return process.env.KJ_PLANS_DIR || join(getKjHome(), 'plans');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Legacy `~/.kj/plans/` directory — only returned when KJ_PLANS_DIR
|
|
89
|
-
* is unset (it would be the same path anyway when overridden).
|
|
90
|
-
* Callers iterate canonical + legacy so users who have not yet run
|
|
91
|
-
* the v2.19.0 auto-migrator still see their plans. KJC-BUG-0059.
|
|
92
|
-
*
|
|
93
|
-
* @returns {string|null}
|
|
94
|
-
*/
|
|
95
|
-
export function getHuBoardLegacyPlansDir() {
|
|
96
|
-
if (process.env.KJ_PLANS_DIR) return null;
|
|
97
|
-
return join(process.env.HOME || '/root', '.kj', 'plans');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Ordered list of every plans root the board should scan: canonical
|
|
102
|
-
* first (`~/.karajan/plans/`), legacy second when applicable
|
|
103
|
-
* (`~/.kj/plans/`). Honours `KJ_PLANS_DIR` as a single override,
|
|
104
|
-
* matching `sync.js`'s fullScan / startWatcher precedence so the
|
|
105
|
-
* preflight, plans-outcome, delete-project and delete-plan endpoints
|
|
106
|
-
* see the same set of files the watcher sees. KJC-BUG-0059.
|
|
107
|
-
*
|
|
108
|
-
* @returns {string[]}
|
|
109
|
-
*/
|
|
110
|
-
export function getHuBoardPlansDirs() {
|
|
111
|
-
const dirs = [getHuBoardPlansDir()];
|
|
112
|
-
const legacy = getHuBoardLegacyPlansDir();
|
|
113
|
-
if (legacy) dirs.push(legacy);
|
|
114
|
-
return dirs;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Bump when the schema gains a column / index / table that older
|
|
118
|
-
// Karajan versions cannot understand. A DB written by a newer Karajan
|
|
119
|
-
// refuses to open here so it does not get silently downgraded.
|
|
120
|
-
const SCHEMA_VERSION = 1;
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Tries to open the SQLite database; if it is corrupt, moves it aside
|
|
124
|
-
* and opens a fresh one. `fullScan` afterwards rebuilds the cache from
|
|
125
|
-
* the JSON files on disk — the DB is a cache, never the source of
|
|
126
|
-
* truth. Without this the board crashed at startup with a raw
|
|
127
|
-
* `SqliteError: database disk image is malformed` whenever the WAL
|
|
128
|
-
* checkpoint died in the wrong place.
|
|
129
|
-
*
|
|
130
|
-
* @param {string} dbPath
|
|
131
|
-
* @returns {import('better-sqlite3').Database}
|
|
132
|
-
*/
|
|
133
|
-
function openDbResilient(dbPath) {
|
|
134
|
-
try {
|
|
135
|
-
const handle = new Database(dbPath);
|
|
136
|
-
// better-sqlite3 opens lazily — the first pragma is what actually
|
|
137
|
-
// touches the file and surfaces "file is not a database". Probe
|
|
138
|
-
// `schema_version` (a built-in SQLite pragma) to force it.
|
|
139
|
-
handle.pragma("schema_version", { simple: true });
|
|
140
|
-
return handle;
|
|
141
|
-
} catch (err) {
|
|
142
|
-
const code = err?.code || "";
|
|
143
|
-
const msg = err?.message || "";
|
|
144
|
-
if (code === "SQLITE_CORRUPT" || code === "SQLITE_NOTADB" || /malformed|not a database/i.test(msg)) {
|
|
145
|
-
const aside = `${dbPath}.corrupt-${Date.now()}`;
|
|
146
|
-
try { renameSync(dbPath, aside); } catch { /* may already be gone */ }
|
|
147
|
-
// eslint-disable-next-line no-console
|
|
148
|
-
console.warn(`[hu-board] DB at ${dbPath} was corrupt; moved aside to ${aside}. Rebuilding from disk via fullScan.`);
|
|
149
|
-
return new Database(dbPath);
|
|
150
|
-
}
|
|
151
|
-
throw err;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Initializes the SQLite database and creates tables if they don't exist.
|
|
157
|
-
* @returns {import('better-sqlite3').Database}
|
|
158
|
-
*/
|
|
159
|
-
export function initDb() {
|
|
160
|
-
const kjHome = getKjHome();
|
|
161
|
-
mkdirSync(kjHome, { recursive: true });
|
|
162
|
-
|
|
163
|
-
const dbPath = join(kjHome, 'hu-board.db');
|
|
164
|
-
db = openDbResilient(dbPath);
|
|
165
|
-
|
|
166
|
-
db.pragma('journal_mode = WAL');
|
|
167
|
-
db.pragma('foreign_keys = ON');
|
|
168
|
-
// Wait up to 5s for a competing writer (e.g. concurrent fullScan +
|
|
169
|
-
// mutation) before giving up. Without this any contention surfaced
|
|
170
|
-
// as SQLITE_BUSY and crashed the request mid-flight.
|
|
171
|
-
db.pragma('busy_timeout = 5000');
|
|
172
|
-
|
|
173
|
-
// Schema-version guard. If the DB was written by a newer Karajan with
|
|
174
|
-
// a different schema, refuse to open instead of silently mis-binding
|
|
175
|
-
// named parameters against a renamed column.
|
|
176
|
-
const onDisk = db.pragma('user_version', { simple: true });
|
|
177
|
-
if (onDisk > SCHEMA_VERSION) {
|
|
178
|
-
throw new Error(
|
|
179
|
-
`[hu-board] DB schema is from a newer Karajan (user_version=${onDisk}, this build expects ${SCHEMA_VERSION}). `
|
|
180
|
-
+ `Upgrade karajan-code or remove ${dbPath} (it will be rebuilt from disk).`
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
if (onDisk < SCHEMA_VERSION) {
|
|
184
|
-
db.pragma(`user_version = ${SCHEMA_VERSION}`);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
db.exec(`
|
|
188
|
-
CREATE TABLE IF NOT EXISTS projects (
|
|
189
|
-
id TEXT PRIMARY KEY,
|
|
190
|
-
name TEXT,
|
|
191
|
-
first_seen TEXT,
|
|
192
|
-
last_activity TEXT,
|
|
193
|
-
total_stories INTEGER DEFAULT 0
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
CREATE TABLE IF NOT EXISTS stories (
|
|
197
|
-
id TEXT PRIMARY KEY,
|
|
198
|
-
project_id TEXT,
|
|
199
|
-
session_id TEXT,
|
|
200
|
-
status TEXT DEFAULT 'pending',
|
|
201
|
-
title TEXT,
|
|
202
|
-
original_text TEXT,
|
|
203
|
-
certified_as TEXT,
|
|
204
|
-
certified_want TEXT,
|
|
205
|
-
certified_so_that TEXT,
|
|
206
|
-
quality_total INTEGER,
|
|
207
|
-
quality_d1 INTEGER,
|
|
208
|
-
quality_d2 INTEGER,
|
|
209
|
-
quality_d3 INTEGER,
|
|
210
|
-
quality_d4 INTEGER,
|
|
211
|
-
quality_d5 INTEGER,
|
|
212
|
-
quality_d6 INTEGER,
|
|
213
|
-
antipatterns TEXT,
|
|
214
|
-
ac_format TEXT,
|
|
215
|
-
acceptance_criteria TEXT,
|
|
216
|
-
created_at TEXT,
|
|
217
|
-
updated_at TEXT,
|
|
218
|
-
certified_at TEXT
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
222
|
-
id TEXT PRIMARY KEY,
|
|
223
|
-
project_id TEXT,
|
|
224
|
-
task TEXT,
|
|
225
|
-
status TEXT,
|
|
226
|
-
created_at TEXT,
|
|
227
|
-
updated_at TEXT,
|
|
228
|
-
iterations INTEGER DEFAULT 0,
|
|
229
|
-
duration_ms INTEGER,
|
|
230
|
-
approved INTEGER DEFAULT 0,
|
|
231
|
-
commits TEXT,
|
|
232
|
-
stages_completed TEXT,
|
|
233
|
-
checkpoints TEXT,
|
|
234
|
-
llm_calls TEXT,
|
|
235
|
-
config_snapshot TEXT,
|
|
236
|
-
budget TEXT
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
CREATE TABLE IF NOT EXISTS context_requests (
|
|
240
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
241
|
-
story_id TEXT,
|
|
242
|
-
fields_needed TEXT,
|
|
243
|
-
question TEXT,
|
|
244
|
-
answer TEXT,
|
|
245
|
-
requested_at TEXT,
|
|
246
|
-
answered_at TEXT
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
-- KJC-TSK-0380 — Tombstones: "deleted, do not resurrect".
|
|
250
|
-
-- The board's data is reconstructed from the filesystem on every
|
|
251
|
-
-- fullScan(), so a plain DELETE from the table is silently undone
|
|
252
|
-
-- the next time chokidar fires. The tombstone table holds the ids
|
|
253
|
-
-- the user explicitly killed; sync* functions check it before any
|
|
254
|
-
-- upsert and skip + clean fs paths.
|
|
255
|
-
CREATE TABLE IF NOT EXISTS tombstones (
|
|
256
|
-
resource_type TEXT NOT NULL,
|
|
257
|
-
resource_id TEXT NOT NULL,
|
|
258
|
-
deleted_at TEXT NOT NULL,
|
|
259
|
-
source TEXT NOT NULL,
|
|
260
|
-
fs_paths TEXT,
|
|
261
|
-
PRIMARY KEY (resource_type, resource_id)
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
CREATE INDEX IF NOT EXISTS idx_stories_project ON stories(project_id);
|
|
265
|
-
CREATE INDEX IF NOT EXISTS idx_stories_status ON stories(status);
|
|
266
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
|
|
267
|
-
CREATE INDEX IF NOT EXISTS idx_context_story ON context_requests(story_id);
|
|
268
|
-
CREATE INDEX IF NOT EXISTS idx_tombstones_deleted_at ON tombstones(deleted_at);
|
|
269
|
-
`);
|
|
270
|
-
|
|
271
|
-
// --- Migrations ---
|
|
272
|
-
// Each ALTER is idempotent on restart — we swallow the duplicate-column
|
|
273
|
-
// error so adding one over time doesn't force a board DB nuke.
|
|
274
|
-
//
|
|
275
|
-
// plan_id: lets the mutation endpoints locate the source-of-truth plan
|
|
276
|
-
// JSON at ~/.karajan/plans/<slug>/<planId>.json without a scan.
|
|
277
|
-
// ac_count: number of acceptance criteria, surfaced on the card.
|
|
278
|
-
// test_count: number of acceptance_tests, surfaced on the card.
|
|
279
|
-
// blocked_by: denormalised JSON array of HU ids this story waits on,
|
|
280
|
-
// so the card can render `⏳ waits for: lao-001` inline.
|
|
281
|
-
try { db.exec('ALTER TABLE stories ADD COLUMN plan_id TEXT'); } catch { /* already migrated */ }
|
|
282
|
-
try { db.exec('ALTER TABLE stories ADD COLUMN ac_count INTEGER'); } catch { /* already migrated */ }
|
|
283
|
-
try { db.exec('ALTER TABLE stories ADD COLUMN test_count INTEGER'); } catch { /* already migrated */ }
|
|
284
|
-
try { db.exec('ALTER TABLE stories ADD COLUMN blocked_by TEXT'); } catch { /* already migrated */ }
|
|
285
|
-
// acceptance_tests is the raw JSON of `hu.acceptance_tests` — the
|
|
286
|
-
// plan stores each test as either a plain string or an object with
|
|
287
|
-
// { name, description, given/when/then, ... }. The modal renders
|
|
288
|
-
// the list; the card just shows the count (test_count above).
|
|
289
|
-
try { db.exec('ALTER TABLE stories ADD COLUMN acceptance_tests TEXT'); } catch { /* already migrated */ }
|
|
290
|
-
// plan_order is the index of the HU in `plan.hus[]` as the planner
|
|
291
|
-
// emitted it. The planner's output is already topological (it calls
|
|
292
|
-
// addHu with `blocked_by: [prevId]`), so sorting the board by this
|
|
293
|
-
// column puts roots first and dependents last — matching what
|
|
294
|
-
// `kj run --plan` will actually execute.
|
|
295
|
-
try { db.exec('ALTER TABLE stories ADD COLUMN plan_order INTEGER'); } catch { /* already migrated */ }
|
|
296
|
-
// spec_section (v2.7.5 PR C): the SPEC.md heading this HU
|
|
297
|
-
// implements (e.g. "5.3" or "§5 Initial Scope"). Surfaces in the
|
|
298
|
-
// card + modal so the user can trace HU → spec coverage.
|
|
299
|
-
try { db.exec('ALTER TABLE stories ADD COLUMN spec_section TEXT'); } catch { /* already migrated */ }
|
|
300
|
-
// outcome (PR3): JSON blob with what actually happened during
|
|
301
|
-
// execution — iterations, duration, commits, branch, blockers,
|
|
302
|
-
// human summary. Stamped by hu-sub-pipeline at the end of each
|
|
303
|
-
// HU and ingested by syncPlanFile so the board can show the 📄
|
|
304
|
-
// indicator and detail modal without parsing the plan JSON
|
|
305
|
-
// again. Stored as TEXT (JSON.stringify'd) for simplicity —
|
|
306
|
-
// parsed on read by the UI.
|
|
307
|
-
try { db.exec('ALTER TABLE stories ADD COLUMN outcome TEXT'); } catch { /* already migrated */ }
|
|
308
|
-
// KJC-TSK-0394: result (pass | fail | partial | null) ortogonal al status.
|
|
309
|
-
// Diferente del campo `outcome` arriba (que es blob JSON con metadata del
|
|
310
|
-
// run). result es el enum de "último resultado" para mostrar como badge.
|
|
311
|
-
try { db.exec('ALTER TABLE stories ADD COLUMN result TEXT'); } catch { /* already migrated */ }
|
|
312
|
-
// KJC-TSK-0406: coder_model y reviewer_model independientes por HU.
|
|
313
|
-
// Asignados por el triage según complexity (KJC-TSK-0405) y override-able
|
|
314
|
-
// desde el modal del board. coder_provider y reviewer_provider permiten
|
|
315
|
-
// cross-provider review (claude↔codex) sin tocar el config global.
|
|
316
|
-
try { db.exec('ALTER TABLE stories ADD COLUMN coder_model TEXT'); } catch { /* already migrated */ }
|
|
317
|
-
try { db.exec('ALTER TABLE stories ADD COLUMN reviewer_model TEXT'); } catch { /* already migrated */ }
|
|
318
|
-
try { db.exec('ALTER TABLE stories ADD COLUMN coder_provider TEXT'); } catch { /* already migrated */ }
|
|
319
|
-
try { db.exec('ALTER TABLE stories ADD COLUMN reviewer_provider TEXT'); } catch { /* already migrated */ }
|
|
320
|
-
// KJC-PRP-0002 PR6: free-form handle of the human (or dev_XXX AI) who
|
|
321
|
-
// owns this HU on a team-shared board. NULL = unassigned. Only surfaced
|
|
322
|
-
// in the UI when the parent project is `is_shared = 1`, but stored
|
|
323
|
-
// unconditionally so solo devs who later promote a plan don't lose
|
|
324
|
-
// values they typed pre-share.
|
|
325
|
-
try { db.exec('ALTER TABLE stories ADD COLUMN assignee TEXT'); } catch { /* already migrated */ }
|
|
326
|
-
// KJC-TSK-0514 (Cost C): persisted USD cost of running this HU. NULL
|
|
327
|
-
// until the orchestrator (Cost D) calls setStoryCost on HU close. UI
|
|
328
|
-
// renders "—" while NULL. Stored as REAL so the SQL aggregations in
|
|
329
|
-
// Cost E (/api/projects/:id/cost) can SUM directly without parsing.
|
|
330
|
-
try { db.exec('ALTER TABLE stories ADD COLUMN cost_usd REAL'); } catch { /* already migrated */ }
|
|
331
|
-
// KJC-TSK-0525 (Φ0-G): persisted cached/input tokens per HU so the
|
|
332
|
-
// board can render the "🎯 N% cached" badge without re-parsing the
|
|
333
|
-
// session summary. NULL = unmeasured (older runs, cold runs without
|
|
334
|
-
// cache support). INTEGER because providers report token counts as
|
|
335
|
-
// ints. Ratio is computed at read time as cached_tokens / tokens_in.
|
|
336
|
-
try { db.exec('ALTER TABLE stories ADD COLUMN cached_tokens INTEGER'); } catch { /* already migrated */ }
|
|
337
|
-
try { db.exec('ALTER TABLE stories ADD COLUMN tokens_in INTEGER'); } catch { /* already migrated */ }
|
|
338
|
-
try { db.exec('CREATE INDEX IF NOT EXISTS idx_stories_plan ON stories(plan_id)'); } catch { /* ignore */ }
|
|
339
|
-
|
|
340
|
-
// is_test (KJC-TSK-0371 — board polish #3): per-project flag that
|
|
341
|
-
// controls the ephemeral-cleanup heuristic. NULL = follow heuristic
|
|
342
|
-
// (id under tmp_/test_/demo_/kj-test- → ephemeral after 24h);
|
|
343
|
-
// 1 = always treat as ephemeral; 0 = NEVER auto-clean (user override).
|
|
344
|
-
// The UI surfaces this as a toggle on each project card.
|
|
345
|
-
try { db.exec('ALTER TABLE projects ADD COLUMN is_test INTEGER'); } catch { /* already migrated */ }
|
|
346
|
-
|
|
347
|
-
// is_shared (KJC-PRP-0002 PR3): 1 when at least one plan of the project came
|
|
348
|
-
// from `<projectDir>/.karajan-shared/plans/`. Surfaced as a 🔗 badge on the
|
|
349
|
-
// board so the user can tell at a glance which projects are team-tracked.
|
|
350
|
-
try { db.exec('ALTER TABLE projects ADD COLUMN is_shared INTEGER'); } catch { /* already migrated */ }
|
|
351
|
-
|
|
352
|
-
return db;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Returns the current database instance.
|
|
357
|
-
* @returns {import('better-sqlite3').Database}
|
|
358
|
-
*/
|
|
359
|
-
export function getDb() {
|
|
360
|
-
if (!db) throw new Error('Database not initialized. Call initDb() first.');
|
|
361
|
-
return db;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Lazily initialise the DB if it isn't open yet (KJC-BUG-0090). The cost
|
|
366
|
-
* writers run from `kj run`/`kj autorun` paths that may never start the board
|
|
367
|
-
* (which is what normally calls initDb), so cost writes failed with
|
|
368
|
-
* "Database not initialized". This makes those writers self-sufficient.
|
|
369
|
-
*/
|
|
370
|
-
export function ensureDb() {
|
|
371
|
-
if (!db) initDb();
|
|
372
|
-
return db;
|
|
373
|
-
}
|
|
1
|
+
import { getDb } from './db-connection.js';
|
|
2
|
+
|
|
3
|
+
// KJC-TSK-0542: db.js is the facade every caller imports. The paths and the connection (with
|
|
4
|
+
// the schema) live in their own modules and are re-exported from here.
|
|
5
|
+
export {
|
|
6
|
+
getKjHome,
|
|
7
|
+
getHuBoardRunsDir,
|
|
8
|
+
getHuBoardPlansDir,
|
|
9
|
+
getHuBoardLegacyPlansDir,
|
|
10
|
+
getHuBoardPlansDirs,
|
|
11
|
+
} from './db-paths.js';
|
|
12
|
+
export { initDb, getDb, ensureDb, closeDb } from './db-connection.js';
|
|
13
|
+
export {
|
|
14
|
+
addTombstone,
|
|
15
|
+
isTombstoned,
|
|
16
|
+
getTombstone,
|
|
17
|
+
removeTombstone,
|
|
18
|
+
listTombstones,
|
|
19
|
+
} from './db-tombstones.js';
|
|
20
|
+
export {
|
|
21
|
+
getProjects,
|
|
22
|
+
getStoriesByProject,
|
|
23
|
+
getProjectCost,
|
|
24
|
+
getStoryDetail,
|
|
25
|
+
getSessionsByProject,
|
|
26
|
+
getSessionDetail,
|
|
27
|
+
getDashboardStats,
|
|
28
|
+
} from './db-queries.js';
|
|
374
29
|
|
|
375
30
|
/**
|
|
376
31
|
* Inserts or updates a project record.
|
|
@@ -655,191 +310,6 @@ export function upsertSession(session) {
|
|
|
655
310
|
});
|
|
656
311
|
}
|
|
657
312
|
|
|
658
|
-
/**
|
|
659
|
-
* Returns all projects with aggregated stats.
|
|
660
|
-
* @returns {Array<object>}
|
|
661
|
-
*/
|
|
662
|
-
export function getProjects() {
|
|
663
|
-
return getDb().prepare(`
|
|
664
|
-
SELECT p.*,
|
|
665
|
-
(SELECT COUNT(*) FROM stories s WHERE s.project_id = p.id) AS story_count,
|
|
666
|
-
(SELECT COUNT(*) FROM stories s WHERE s.project_id = p.id AND s.status = 'certified') AS certified_count,
|
|
667
|
-
(SELECT COUNT(*) FROM stories s WHERE s.project_id = p.id AND s.status = 'pending') AS pending_count,
|
|
668
|
-
(SELECT COUNT(*) FROM sessions ss WHERE ss.project_id = p.id) AS session_count
|
|
669
|
-
FROM projects p
|
|
670
|
-
ORDER BY p.last_activity DESC
|
|
671
|
-
`).all();
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
/**
|
|
675
|
-
* Returns stories for a specific project.
|
|
676
|
-
* @param {string} projectId
|
|
677
|
-
* @returns {Array<object>}
|
|
678
|
-
*/
|
|
679
|
-
/**
|
|
680
|
-
* Fetch all stories of a project ordered by plan_order ASC (roots first,
|
|
681
|
-
* dependents last — matches what `kj run --plan` will actually execute)
|
|
682
|
-
* and falling back to updated_at DESC for legacy rows that never had
|
|
683
|
-
* plan_order stamped.
|
|
684
|
-
*/
|
|
685
|
-
export function getStoriesByProject(projectId) {
|
|
686
|
-
return getDb().prepare(`
|
|
687
|
-
SELECT * FROM stories WHERE project_id = ?
|
|
688
|
-
ORDER BY plan_order IS NULL, plan_order ASC, updated_at DESC
|
|
689
|
-
`).all(projectId);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
/**
|
|
693
|
-
* Aggregate $ cost for a project (KJC-TSK-0516, Cost E).
|
|
694
|
-
* Sums stories.cost_usd, groups by plan_id ('unassigned' for NULL).
|
|
695
|
-
* Returns null if project does not exist; otherwise an object with
|
|
696
|
-
* totalUsd=0 and byPlan=[] when no stories carry cost_usd yet.
|
|
697
|
-
* @param {string} projectId
|
|
698
|
-
* @returns {{ totalUsd:number, byPlan:Array<{planId:string,totalUsd:number,huCount:number}>, unknownModelTokens:object, currency:string } | null}
|
|
699
|
-
*/
|
|
700
|
-
export function getProjectCost(projectId) {
|
|
701
|
-
const project = getDb()
|
|
702
|
-
.prepare('SELECT id FROM projects WHERE id = ?')
|
|
703
|
-
.get(projectId);
|
|
704
|
-
if (!project) return null;
|
|
705
|
-
|
|
706
|
-
// Φ0-G: cached_tokens / tokens_in are nullable independently of
|
|
707
|
-
// cost_usd, so we read all three columns but keep cost as the
|
|
708
|
-
// primary filter (a row with only cache telemetry but no $ should
|
|
709
|
-
// not appear in the per-plan totals — cost rules the byPlan list).
|
|
710
|
-
const rows = getDb()
|
|
711
|
-
.prepare(
|
|
712
|
-
`SELECT plan_id, cost_usd, cached_tokens, tokens_in FROM stories
|
|
713
|
-
WHERE project_id = ? AND cost_usd IS NOT NULL`
|
|
714
|
-
)
|
|
715
|
-
.all(projectId);
|
|
716
|
-
|
|
717
|
-
const round4 = (n) => Math.round(n * 10000) / 10000;
|
|
718
|
-
const byPlanMap = new Map();
|
|
719
|
-
let totalUsd = 0;
|
|
720
|
-
let totalCached = 0;
|
|
721
|
-
let totalTokensIn = 0;
|
|
722
|
-
for (const r of rows) {
|
|
723
|
-
const planId = r.plan_id || 'unassigned';
|
|
724
|
-
const cost = Number(r.cost_usd);
|
|
725
|
-
if (!Number.isFinite(cost)) continue;
|
|
726
|
-
totalUsd += cost;
|
|
727
|
-
const cached = Number(r.cached_tokens);
|
|
728
|
-
const tin = Number(r.tokens_in);
|
|
729
|
-
if (Number.isFinite(cached) && cached >= 0) totalCached += cached;
|
|
730
|
-
if (Number.isFinite(tin) && tin >= 0) totalTokensIn += tin;
|
|
731
|
-
const cur = byPlanMap.get(planId) || { planId, totalUsd: 0, huCount: 0, cachedTokens: 0, tokensIn: 0 };
|
|
732
|
-
cur.totalUsd += cost;
|
|
733
|
-
cur.huCount += 1;
|
|
734
|
-
if (Number.isFinite(cached) && cached >= 0) cur.cachedTokens += cached;
|
|
735
|
-
if (Number.isFinite(tin) && tin >= 0) cur.tokensIn += tin;
|
|
736
|
-
byPlanMap.set(planId, cur);
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
const ratio = (cached, tin) => tin > 0 ? Math.round((cached / tin) * 1000) / 10 : null;
|
|
740
|
-
const byPlan = Array.from(byPlanMap.values())
|
|
741
|
-
.map((p) => ({
|
|
742
|
-
planId: p.planId,
|
|
743
|
-
totalUsd: round4(p.totalUsd),
|
|
744
|
-
huCount: p.huCount,
|
|
745
|
-
cachedTokens: p.cachedTokens,
|
|
746
|
-
tokensIn: p.tokensIn,
|
|
747
|
-
cachedRatioPct: ratio(p.cachedTokens, p.tokensIn),
|
|
748
|
-
}))
|
|
749
|
-
.sort((a, b) => b.totalUsd - a.totalUsd);
|
|
750
|
-
|
|
751
|
-
return {
|
|
752
|
-
totalUsd: round4(totalUsd),
|
|
753
|
-
byPlan,
|
|
754
|
-
cachedTokens: totalCached,
|
|
755
|
-
tokensIn: totalTokensIn,
|
|
756
|
-
cachedRatioPct: ratio(totalCached, totalTokensIn),
|
|
757
|
-
unknownModelTokens: { tokensIn: 0, tokensOut: 0, models: [] },
|
|
758
|
-
currency: 'USD',
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
/**
|
|
763
|
-
* Returns full story detail including context requests.
|
|
764
|
-
* @param {string} storyId
|
|
765
|
-
* @returns {object | null}
|
|
766
|
-
*/
|
|
767
|
-
export function getStoryDetail(storyId) {
|
|
768
|
-
const story = getDb().prepare('SELECT * FROM stories WHERE id = ?').get(storyId);
|
|
769
|
-
if (!story) return null;
|
|
770
|
-
|
|
771
|
-
const contextRequests = getDb().prepare(
|
|
772
|
-
'SELECT * FROM context_requests WHERE story_id = ? ORDER BY requested_at DESC'
|
|
773
|
-
).all(storyId);
|
|
774
|
-
|
|
775
|
-
return { ...story, context_requests: contextRequests };
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
/**
|
|
779
|
-
* Returns sessions for a specific project.
|
|
780
|
-
*
|
|
781
|
-
* The `project_name` column is joined in so the UI can show a human
|
|
782
|
-
* label like `tmp_kj-test-4 · trivial(doc) · 08:35` instead of just
|
|
783
|
-
* the cryptic `s_2026-05-07T...` id.
|
|
784
|
-
*
|
|
785
|
-
* @param {string} projectId
|
|
786
|
-
* @returns {Array<object>}
|
|
787
|
-
*/
|
|
788
|
-
export function getSessionsByProject(projectId) {
|
|
789
|
-
return getDb().prepare(`
|
|
790
|
-
SELECT s.id, s.project_id, s.task, s.status, s.created_at, s.updated_at,
|
|
791
|
-
s.iterations, s.duration_ms, s.approved, s.stages_completed,
|
|
792
|
-
p.name AS project_name
|
|
793
|
-
FROM sessions s
|
|
794
|
-
LEFT JOIN projects p ON p.id = s.project_id
|
|
795
|
-
WHERE s.project_id = ?
|
|
796
|
-
ORDER BY s.created_at DESC
|
|
797
|
-
`).all(projectId);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
/**
|
|
801
|
-
* Returns full session detail (with the joined project_name).
|
|
802
|
-
* @param {string} sessionId
|
|
803
|
-
* @returns {object | null}
|
|
804
|
-
*/
|
|
805
|
-
export function getSessionDetail(sessionId) {
|
|
806
|
-
return getDb().prepare(`
|
|
807
|
-
SELECT s.*, p.name AS project_name
|
|
808
|
-
FROM sessions s
|
|
809
|
-
LEFT JOIN projects p ON p.id = s.project_id
|
|
810
|
-
WHERE s.id = ?
|
|
811
|
-
`).get(sessionId);
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
/**
|
|
815
|
-
* Returns global dashboard statistics.
|
|
816
|
-
* @returns {object}
|
|
817
|
-
*/
|
|
818
|
-
export function getDashboardStats() {
|
|
819
|
-
const db = getDb();
|
|
820
|
-
const totalStories = db.prepare('SELECT COUNT(*) AS count FROM stories').get().count;
|
|
821
|
-
const certifiedStories = db.prepare("SELECT COUNT(*) AS count FROM stories WHERE status = 'certified'").get().count;
|
|
822
|
-
const pendingStories = db.prepare("SELECT COUNT(*) AS count FROM stories WHERE status = 'pending'").get().count;
|
|
823
|
-
const doneStories = db.prepare("SELECT COUNT(*) AS count FROM stories WHERE status = 'done'").get().count;
|
|
824
|
-
const needsContextStories = db.prepare("SELECT COUNT(*) AS count FROM stories WHERE status = 'needs_context'").get().count;
|
|
825
|
-
const avgQuality = db.prepare('SELECT AVG(quality_total) AS avg FROM stories WHERE quality_total IS NOT NULL').get().avg;
|
|
826
|
-
const totalSessions = db.prepare('SELECT COUNT(*) AS count FROM sessions').get().count;
|
|
827
|
-
const approvedSessions = db.prepare('SELECT COUNT(*) AS count FROM sessions WHERE approved = 1').get().count;
|
|
828
|
-
const totalProjects = db.prepare('SELECT COUNT(*) AS count FROM projects').get().count;
|
|
829
|
-
|
|
830
|
-
return {
|
|
831
|
-
total_stories: totalStories,
|
|
832
|
-
certified_stories: certifiedStories,
|
|
833
|
-
pending_stories: pendingStories,
|
|
834
|
-
done_stories: doneStories,
|
|
835
|
-
needs_context_stories: needsContextStories,
|
|
836
|
-
avg_quality: avgQuality ? Math.round(avgQuality * 10) / 10 : null,
|
|
837
|
-
total_sessions: totalSessions,
|
|
838
|
-
approved_sessions: approvedSessions,
|
|
839
|
-
total_projects: totalProjects,
|
|
840
|
-
};
|
|
841
|
-
}
|
|
842
|
-
|
|
843
313
|
/**
|
|
844
314
|
* Inserts a context request for a story.
|
|
845
315
|
* @param {object} req
|
|
@@ -913,64 +383,3 @@ export function deleteSession(sessionId) {
|
|
|
913
383
|
const res = db.prepare('DELETE FROM sessions WHERE id = ?').run(sessionId);
|
|
914
384
|
return res.changes > 0;
|
|
915
385
|
}
|
|
916
|
-
|
|
917
|
-
// --- Tombstones (KJC-TSK-0380) ---------------------------------------
|
|
918
|
-
// "This (resource_type, resource_id) is dead — don't bring it back." The
|
|
919
|
-
// sync layer consults this table BEFORE every upsert and skips records
|
|
920
|
-
// whose id is buried. Restoration is explicit via removeTombstone().
|
|
921
|
-
|
|
922
|
-
export function addTombstone(resourceType, resourceId, opts = {}) {
|
|
923
|
-
getDb().prepare(`
|
|
924
|
-
INSERT OR REPLACE INTO tombstones (resource_type, resource_id, deleted_at, source, fs_paths)
|
|
925
|
-
VALUES (?, ?, ?, ?, ?)
|
|
926
|
-
`).run(
|
|
927
|
-
resourceType, resourceId,
|
|
928
|
-
new Date().toISOString(),
|
|
929
|
-
opts.source || 'api',
|
|
930
|
-
opts.fsPaths ? JSON.stringify(opts.fsPaths) : null,
|
|
931
|
-
);
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
export function isTombstoned(resourceType, resourceId) {
|
|
935
|
-
return !!getDb().prepare(
|
|
936
|
-
'SELECT 1 FROM tombstones WHERE resource_type = ? AND resource_id = ? LIMIT 1'
|
|
937
|
-
).get(resourceType, resourceId);
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
/**
|
|
941
|
-
* Read a single tombstone row (or null). Needed so callers can compare
|
|
942
|
-
* `deleted_at` against a resource's own timestamp before deciding whether
|
|
943
|
-
* to revive it (KJC-BUG-0055).
|
|
944
|
-
*/
|
|
945
|
-
export function getTombstone(resourceType, resourceId) {
|
|
946
|
-
const row = getDb().prepare(
|
|
947
|
-
'SELECT resource_type, resource_id, deleted_at, source, fs_paths FROM tombstones WHERE resource_type = ? AND resource_id = ?'
|
|
948
|
-
).get(resourceType, resourceId);
|
|
949
|
-
if (!row) return null;
|
|
950
|
-
return { ...row, fs_paths: row.fs_paths ? JSON.parse(row.fs_paths) : [] };
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
export function removeTombstone(resourceType, resourceId) {
|
|
954
|
-
return getDb().prepare(
|
|
955
|
-
'DELETE FROM tombstones WHERE resource_type = ? AND resource_id = ?'
|
|
956
|
-
).run(resourceType, resourceId).changes > 0;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
export function listTombstones() {
|
|
960
|
-
return getDb().prepare(
|
|
961
|
-
'SELECT resource_type, resource_id, deleted_at, source, fs_paths FROM tombstones ORDER BY deleted_at DESC'
|
|
962
|
-
).all().map((row) => ({
|
|
963
|
-
...row,
|
|
964
|
-
fs_paths: row.fs_paths ? JSON.parse(row.fs_paths) : [],
|
|
965
|
-
}));
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
/**
|
|
969
|
-
* Closes the database connection.
|
|
970
|
-
*/
|
|
971
|
-
export function closeDb() {
|
|
972
|
-
if (db) {
|
|
973
|
-
db.close();
|
|
974
|
-
db = null;
|
|
975
|
-
}
|
|
976
|
-
}
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*
|
|
28
28
|
* What "cleanup" means: DELETE the project row + cascade-delete its
|
|
29
29
|
* stories and sessions, ADD a project tombstone, and rm-rf the
|
|
30
|
-
* on-disk dirs (`
|
|
30
|
+
* on-disk dirs (`sessions/<sid>/`,
|
|
31
31
|
* `~/.kj/plans/<projectId>/`). Without the tombstone + fs cleanup,
|
|
32
32
|
* the very next chokidar `add` event or `fullScan` would re-import
|
|
33
33
|
* those files as a brand-new project (KJC-BUG-0055).
|
|
@@ -123,7 +123,7 @@ export function findEphemeralProjects(projects, opts = {}) {
|
|
|
123
123
|
* has no effect on the second pass.
|
|
124
124
|
*
|
|
125
125
|
* @param {object} deps
|
|
126
|
-
* @param {object} deps.db
|
|
126
|
+
* @param {object} deps.db SQLite handle (karajan-core/db adapter)
|
|
127
127
|
* @param {object} [deps.opts] threshold overrides
|
|
128
128
|
* @param {Function} [deps.now] () => number, for tests
|
|
129
129
|
* @returns {Array<{ id: string, reason: string, stories_deleted: number, sessions_deleted: number }>}
|
|
@@ -165,7 +165,6 @@ export function cleanupEphemeralProjects({
|
|
|
165
165
|
const sessionIds = selSessions.all(project.id).map((r) => r.id);
|
|
166
166
|
const fsPaths = home
|
|
167
167
|
? [
|
|
168
|
-
...storyIds.map((sid) => path.join(home, "hu-stories", sid)),
|
|
169
168
|
...sessionIds.map((sid) => path.join(home, "sessions", sid)),
|
|
170
169
|
path.join(plansRoot, project.id),
|
|
171
170
|
]
|