karajan-code 4.70.4 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 -143
- package/src/cli/register-plan.js +9 -100
- package/src/cli/register-roles-skills.js +1 -1
- 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 -150
- package/src/commands/go.js +71 -53
- package/src/commands/harden.js +90 -81
- 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 +121 -55
- package/src/commands/review-gate.js +421 -341
- package/src/commands/skills.js +4 -1
- 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 +107 -26
- package/src/config/defaults.js +1 -0
- package/src/config/loader.js +34 -17
- package/src/config/role-resolver.js +1 -0
- package/src/config/schema.js +1 -0
- package/src/config/secrets.js +36 -0
- package/src/config.js +1 -0
- package/src/environment/adr.js +16 -2
- package/src/git/karajan-label.js +20 -8
- package/src/guards/main-guard.js +54 -0
- package/src/harden/hook-templates.js +16 -1
- package/src/harden/sentinel/posttooluse.mjs +18 -6
- package/src/harden/sentinel/pretooluse-sentinel.mjs +64 -36
- package/src/harden/sentinel/sentinel-lib.mjs +129 -26
- package/src/harden/sentinel/sentinel-shell.mjs +5 -16
- package/src/harden/sentinel/sessionstart.mjs +3 -2
- package/src/harden/sentinel/stop.mjs +5 -4
- 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 +83 -13
- 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 +1 -35
- package/src/mcp/handlers/management-handlers.js +2 -42
- package/src/mcp/handlers/rag-handler.js +9 -1
- package/src/mcp/server-handlers.js +16 -22
- package/src/mcp/tools.js +1 -196
- 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 +71 -44
- package/src/rag/bootstrap-index.js +46 -54
- package/src/rag/indexer.js +9 -4
- package/src/rag/migrate.js +1 -1
- package/src/rag/onnx-fallback.js +2 -2
- package/src/rag/pid-lease.js +181 -0
- package/src/rag/project-store.js +2 -12
- package/src/review/card-first.js +57 -46
- package/src/review/gate-gitignore.js +23 -4
- package/src/review/gate-hook.js +12 -4
- package/src/review/one-shot-review.js +45 -35
- package/src/review/publish-pr.js +14 -2
- package/src/review/repo-docs.js +73 -0
- package/src/review/repo-stack.js +59 -0
- package/src/review/reviewer-fallback.js +19 -4
- 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 +1 -24
- package/src/roles/audit-role.js +106 -125
- package/src/skills/atril-cli.js +19 -1
- package/src/skills/atril-skills.js +5 -4
- package/src/skills/skills-search.js +72 -0
- package/src/skills/translate.js +62 -0
- 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/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
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// KJC-TSK-0541: the project endpoints of /api, composed by routes/api.js.
|
|
2
|
+
import { Router } from 'express';
|
|
3
|
+
import fsp from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
getProjects,
|
|
8
|
+
deleteProject,
|
|
9
|
+
getKjHome,
|
|
10
|
+
getHuBoardPlansDirs,
|
|
11
|
+
listPlanIdsForProject,
|
|
12
|
+
setProjectIsTest,
|
|
13
|
+
getProjectCost,
|
|
14
|
+
addTombstone,
|
|
15
|
+
getDb,
|
|
16
|
+
} from '../../db.js';
|
|
17
|
+
import { markPlanReady, renameProject } from '../../plan-mutations.js';
|
|
18
|
+
import { runPreflight } from '../../preflight.js';
|
|
19
|
+
import { projectPlans } from '../../plans-service.js';
|
|
20
|
+
|
|
21
|
+
const router = Router();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* GET /api/projects - List all projects with story counts.
|
|
25
|
+
*/
|
|
26
|
+
router.get('/projects', (_req, res) => {
|
|
27
|
+
try {
|
|
28
|
+
const projects = getProjects();
|
|
29
|
+
res.json(projects);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
res.status(500).json({ error: err.message });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* GET /api/projects/:id - Project detail.
|
|
37
|
+
*/
|
|
38
|
+
router.get('/projects/:id', (req, res) => {
|
|
39
|
+
try {
|
|
40
|
+
const projects = getProjects();
|
|
41
|
+
const project = projects.find((p) => p.id === req.params.id);
|
|
42
|
+
if (!project) return res.status(404).json({ error: 'Project not found' });
|
|
43
|
+
res.json(project);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
res.status(500).json({ error: err.message });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* PUT /api/projects/:id/name — PR-G: rename a project.
|
|
51
|
+
*
|
|
52
|
+
* Updates plan.name on every plan JSON belonging to the project AND
|
|
53
|
+
* the projects.name column directly so the UI reflects the new name
|
|
54
|
+
* on the next render. Body: { name: "Linux Assistant Orchestrator" }.
|
|
55
|
+
*/
|
|
56
|
+
router.put('/projects/:id/name', (req, res) => {
|
|
57
|
+
try {
|
|
58
|
+
const result = renameProject({ projectId: req.params.id, name: req.body?.name });
|
|
59
|
+
if (!result.ok) {
|
|
60
|
+
const code = /no encontrado/i.test(result.error) ? 404 : 400;
|
|
61
|
+
return res.status(code).json({ error: result.error });
|
|
62
|
+
}
|
|
63
|
+
res.json(result);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
res.status(500).json({ error: err.message });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* PATCH /api/projects/:id/is-test — KJC-TSK-0371 (board polish #3).
|
|
71
|
+
*
|
|
72
|
+
* Sets/clears the per-project ephemeral-cleanup override. The
|
|
73
|
+
* heuristic (id under tmp_/test_/demo_/kj-test- AND last_activity
|
|
74
|
+
* older than 24h → auto-delete on next board start) is the default.
|
|
75
|
+
* `is_test` lets the user override per-project:
|
|
76
|
+
* - body { is_test: 1 } → always treat as ephemeral
|
|
77
|
+
* - body { is_test: 0 } → NEVER auto-clean (keep this one)
|
|
78
|
+
* - body { is_test: null } → revert to the default heuristic
|
|
79
|
+
*
|
|
80
|
+
* Sending anything else is a 400 — explicit ternary, no truthy/
|
|
81
|
+
* falsy coercion, because that exact distinction is what the
|
|
82
|
+
* cleaner relies on.
|
|
83
|
+
*/
|
|
84
|
+
router.patch('/projects/:id/is-test', (req, res) => {
|
|
85
|
+
const raw = req.body?.is_test;
|
|
86
|
+
// Accept the three legal sentinel values explicitly — undefined
|
|
87
|
+
// (key missing) is rejected because this endpoint only exists to
|
|
88
|
+
// set or clear, not to "leave as-is".
|
|
89
|
+
const allowed = raw === 0 || raw === 1 || raw === null;
|
|
90
|
+
if (!allowed) {
|
|
91
|
+
return res.status(400).json({ error: 'is_test must be 0, 1, or null' });
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const ok = setProjectIsTest(req.params.id, raw);
|
|
95
|
+
if (!ok) return res.status(404).json({ error: 'project not found' });
|
|
96
|
+
res.json({ ok: true, project_id: req.params.id, is_test: raw });
|
|
97
|
+
} catch (err) {
|
|
98
|
+
res.status(500).json({ error: err.message });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* GET /api/projects/:id/preflight — Project readiness checks.
|
|
104
|
+
*
|
|
105
|
+
* Returns the same shape regardless of the project's state. Each
|
|
106
|
+
* check carries a status (ok|warn|fail|info), a plain-Spanish
|
|
107
|
+
* detail and (when relevant) a `consequence` describing what the
|
|
108
|
+
* user loses if it stays unfixed. Powers both:
|
|
109
|
+
* - the panel at the top of the board (always visible)
|
|
110
|
+
* - the safety modal that appears before ▶ Run plan / ▶ Run HU
|
|
111
|
+
* when there are blockers or warnings
|
|
112
|
+
*
|
|
113
|
+
* The route resolves projectDir by reading the first plan it can
|
|
114
|
+
* find for this project (plans carry projectDir since v2.7.4). If
|
|
115
|
+
* the project has no plans on disk we still return the env-only
|
|
116
|
+
* checks (Node, Karajan, agents, config) so the user gets feedback
|
|
117
|
+
* even before they generate their first plan.
|
|
118
|
+
*/
|
|
119
|
+
router.get('/projects/:id/preflight', async (req, res) => {
|
|
120
|
+
try {
|
|
121
|
+
const projectId = req.params.id;
|
|
122
|
+
let projectDir = null;
|
|
123
|
+
// KJC-BUG-0059 (v2.19.3): scan plans in BOTH the canonical
|
|
124
|
+
// ~/.karajan/plans/ AND the legacy ~/.kj/plans/ — the legacy
|
|
125
|
+
// fallback covers users who started the board before running any
|
|
126
|
+
// `kj` command on a fresh install (so the auto-migrator from
|
|
127
|
+
// v2.19.0 has not fired yet). Pre-fix, this endpoint hard-coded
|
|
128
|
+
// ~/.kj/plans/ as the default, missing every plan post-2.19.0
|
|
129
|
+
// → board card showed "directorio del proyecto: no detectado"
|
|
130
|
+
// even though the plan on disk had a valid projectDir.
|
|
131
|
+
try {
|
|
132
|
+
for await (const plan of projectPlans(projectId)) {
|
|
133
|
+
if (plan?.projectDir) { projectDir = plan.projectDir; break; }
|
|
134
|
+
}
|
|
135
|
+
} catch { /* projectDir stays null → preflight will report it */ }
|
|
136
|
+
const result = await runPreflight({ projectId, projectDir });
|
|
137
|
+
res.json(result);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
res.status(500).json({ error: err.message });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* GET /api/projects/:id/plans-outcome — Plan-level rollups (PR3).
|
|
145
|
+
*
|
|
146
|
+
* Reads every plan JSON for the project and returns the lightweight
|
|
147
|
+
* shape the board needs to render the "Plan finalizado · 8 done · 2
|
|
148
|
+
* failed · 15 min" banner. Plans that haven't finished yet have a
|
|
149
|
+
* null outcome — the front handles that by hiding the banner.
|
|
150
|
+
*/
|
|
151
|
+
router.get('/projects/:id/plans-outcome', async (req, res) => {
|
|
152
|
+
try {
|
|
153
|
+
const projectId = req.params.id;
|
|
154
|
+
// KJC-BUG-0059 (v2.19.3): scan canonical + legacy roots so users
|
|
155
|
+
// who have not yet run the auto-migrator still see their plans.
|
|
156
|
+
const out = [];
|
|
157
|
+
for await (const plan of projectPlans(projectId)) {
|
|
158
|
+
if (plan?.version !== 2) continue;
|
|
159
|
+
out.push({
|
|
160
|
+
planId: plan.planId,
|
|
161
|
+
name: plan.name || (plan.task ? plan.task.slice(0, 80) : plan.planId),
|
|
162
|
+
status: plan.status || 'draft',
|
|
163
|
+
updatedAt: plan.updatedAt || null,
|
|
164
|
+
outcome: plan.outcome || null,
|
|
165
|
+
huCount: Array.isArray(plan.hus) ? plan.hus.length : 0,
|
|
166
|
+
// KJC-TSK-1197: a plan mirroring the shared board says so, with its repo.
|
|
167
|
+
readonly: plan.readonly || null,
|
|
168
|
+
repo: plan.readonly ? plan.repo || null : null,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
res.json({ projectId, plans: out });
|
|
172
|
+
} catch (err) {
|
|
173
|
+
res.status(500).json({ error: err.message });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* GET /api/projects/:id/cost - Aggregated $ cost for a project (KJC-TSK-0516).
|
|
179
|
+
* Sums stories.cost_usd, groups by plan_id, returns 404 if project missing.
|
|
180
|
+
*/
|
|
181
|
+
router.get('/projects/:id/cost', (req, res) => {
|
|
182
|
+
try {
|
|
183
|
+
const cost = getProjectCost(req.params.id);
|
|
184
|
+
if (!cost) return res.status(404).json({ error: 'Project not found' });
|
|
185
|
+
res.json(cost);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
res.status(500).json({ error: err.message });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* DELETE /api/projects/:id - Cascade delete project + its stories + sessions.
|
|
193
|
+
* Also removes its session and plan directories from disk so the next sync
|
|
194
|
+
* does not re-import them.
|
|
195
|
+
*/
|
|
196
|
+
router.delete('/projects/:id', async (req, res) => {
|
|
197
|
+
try {
|
|
198
|
+
const id = req.params.id;
|
|
199
|
+
// Gather filesystem paths to remove BEFORE deleting DB rows so we
|
|
200
|
+
// can stop the next sync from resurrecting anything.
|
|
201
|
+
const db = getDb();
|
|
202
|
+
const storyIds = db.prepare('SELECT id FROM stories WHERE project_id = ?').all(id).map((r) => r.id);
|
|
203
|
+
const sessionIds = db.prepare('SELECT id FROM sessions WHERE project_id = ?').all(id).map((r) => r.id);
|
|
204
|
+
// KJC-BUG-0055: honour KJ_PLANS_DIR (the env override the sync /
|
|
205
|
+
// cleanup-zombies layers already respect). Previously hardcoded to
|
|
206
|
+
// ~/.kj/plans relative to KJ_HOME's parent, which broke when the
|
|
207
|
+
// two homes were configured separately and left orphan plan dirs
|
|
208
|
+
// on disk after a 🗑️ delete.
|
|
209
|
+
// KJC-BUG-0059 (v2.19.3): when removing a deleted project's
|
|
210
|
+
// residual plan dirs, sweep BOTH the canonical and legacy roots
|
|
211
|
+
// so the cleanup is consistent for users mid-migration.
|
|
212
|
+
const fsPaths = [
|
|
213
|
+
...sessionIds.map((sid) => path.join(getKjHome(), 'sessions', sid)),
|
|
214
|
+
...getHuBoardPlansDirs().map((root) => path.join(root, id)),
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
const existed = deleteProject(id);
|
|
218
|
+
if (!existed) return res.status(404).json({ error: 'Project not found' });
|
|
219
|
+
|
|
220
|
+
// Tombstone the project AND its children so chokidar / fullScan
|
|
221
|
+
// can't bring them back via syncSessionFile / syncPlanFile.
|
|
222
|
+
addTombstone('project', id, { source: 'api', fsPaths });
|
|
223
|
+
for (const sid of storyIds) addTombstone('story', sid, { source: 'api' });
|
|
224
|
+
for (const sid of sessionIds) addTombstone('session', sid, { source: 'api' });
|
|
225
|
+
|
|
226
|
+
// Best-effort fs cleanup; failure does NOT undo the tombstones.
|
|
227
|
+
let pathsRemoved = 0;
|
|
228
|
+
for (const p of fsPaths) {
|
|
229
|
+
try { await fsp.rm(p, { recursive: true, force: true }); pathsRemoved++; } catch { /* */ }
|
|
230
|
+
}
|
|
231
|
+
res.json({ deleted: true, pathsRemoved, tombstoned: 1 + storyIds.length + sessionIds.length });
|
|
232
|
+
} catch (err) {
|
|
233
|
+
res.status(500).json({ error: err.message });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* POST /api/projects/:id/ready - Sugar: mark every plan of a project ready
|
|
239
|
+
* in one call. Lets the board expose a single "Mark all certified" button
|
|
240
|
+
* per project view without the UI having to enumerate plan ids first.
|
|
241
|
+
*/
|
|
242
|
+
router.post('/projects/:id/ready', (req, res) => {
|
|
243
|
+
try {
|
|
244
|
+
const planIds = listPlanIdsForProject(req.params.id);
|
|
245
|
+
if (planIds.length === 0) {
|
|
246
|
+
return res.status(404).json({ error: 'No plan-backed stories for this project' });
|
|
247
|
+
}
|
|
248
|
+
const results = [];
|
|
249
|
+
for (const planId of planIds) {
|
|
250
|
+
const r = markPlanReady({ planId, projectId: req.params.id });
|
|
251
|
+
results.push({ planId, ...r });
|
|
252
|
+
}
|
|
253
|
+
const totalCertified = results
|
|
254
|
+
.filter((r) => r.ok)
|
|
255
|
+
.reduce((acc, r) => acc + r.count, 0);
|
|
256
|
+
res.json({
|
|
257
|
+
ready: true,
|
|
258
|
+
projectId: req.params.id,
|
|
259
|
+
plans: results,
|
|
260
|
+
totalCertified,
|
|
261
|
+
});
|
|
262
|
+
} catch (err) {
|
|
263
|
+
res.status(500).json({ error: err.message });
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
export default router;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// KJC-TSK-0541: the prompt endpoints of /api (questions the runner leaves for the board),
|
|
2
|
+
// composed by routes/api.js.
|
|
3
|
+
import { Router } from 'express';
|
|
4
|
+
import fsp from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { getKjHome, addTombstone, isTombstoned as isTombstonedFn } from '../../db.js';
|
|
8
|
+
import { publish as publishEvent } from '../../event-bus.js';
|
|
9
|
+
import { pathExists } from '../../plans-service.js';
|
|
10
|
+
|
|
11
|
+
const router = Router();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* DELETE /api/prompts/:promptId - Discard a pending prompt the runner left
|
|
15
|
+
* behind (KJC-TSK-0380). Removes the RPC files from disk, tombstones the
|
|
16
|
+
* prompt id so a stale chokidar event can't recreate it, and emits a
|
|
17
|
+
* `prompt:resolved` event so all open boards close the modal.
|
|
18
|
+
*/
|
|
19
|
+
router.delete('/prompts/:promptId', async (req, res) => {
|
|
20
|
+
try {
|
|
21
|
+
const promptId = req.params.promptId;
|
|
22
|
+
const dir = promptsDir();
|
|
23
|
+
const main = path.join(dir, `${promptId}.json`);
|
|
24
|
+
const answer = path.join(dir, `${promptId}.answer.json`);
|
|
25
|
+
let removed = 0;
|
|
26
|
+
for (const p of [main, answer]) {
|
|
27
|
+
try { await fsp.unlink(p); removed++; } catch { /* missing is fine */ }
|
|
28
|
+
}
|
|
29
|
+
addTombstone('prompt', promptId, { source: 'api', fsPaths: [main, answer] });
|
|
30
|
+
// Re-use the same event the runner emits when answered, so any open
|
|
31
|
+
// modal in any connected board client closes itself.
|
|
32
|
+
publishEvent({ type: 'prompt-resolved', promptId });
|
|
33
|
+
res.json({ deleted: true, filesRemoved: removed });
|
|
34
|
+
} catch (err) {
|
|
35
|
+
res.status(500).json({ error: err.message });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the prompts directory the runner writes RPC packets to.
|
|
41
|
+
* Same convention as src/utils/board-prompt-bridge.js on the runner.
|
|
42
|
+
*/
|
|
43
|
+
function promptsDir() {
|
|
44
|
+
return path.join(getKjHome(), 'prompts');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* GET /api/prompts - List currently pending prompts. The runner is
|
|
49
|
+
* blocked on each one waiting for an answer; the board reads this on
|
|
50
|
+
* boot to surface modals for prompts that were already pending when
|
|
51
|
+
* the page loaded (the SSE event fires only on new arrivals).
|
|
52
|
+
*/
|
|
53
|
+
// KJC-BUG-0038: prompts whose runner crashed leave the .json behind in
|
|
54
|
+
// ~/.kj/prompts/. Without a TTL, every board reload re-shows the zombie
|
|
55
|
+
// modal "Karajan needs an answer" with no runner behind it — the user
|
|
56
|
+
// can answer but nothing happens. 30 minutes covers the longest legitimate
|
|
57
|
+
// Solomon escalation; anything older is a crashed runner.
|
|
58
|
+
const PROMPT_ZOMBIE_TTL_MS = 30 * 60 * 1000;
|
|
59
|
+
|
|
60
|
+
router.get('/prompts', async (_req, res) => {
|
|
61
|
+
try {
|
|
62
|
+
const dir = promptsDir();
|
|
63
|
+
if (!(await pathExists(dir))) return res.json([]);
|
|
64
|
+
const entries = await fsp.readdir(dir);
|
|
65
|
+
const result = [];
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
for (const name of entries) {
|
|
68
|
+
if (!name.endsWith('.json') || name.endsWith('.answer.json')) continue;
|
|
69
|
+
const promptId = name.replace(/\.json$/, '');
|
|
70
|
+
const mainPath = path.join(dir, name);
|
|
71
|
+
const answerPath = path.join(dir, `${promptId}.answer.json`);
|
|
72
|
+
// KJC-TSK-0380 — skip tombstoned prompts and clean their files.
|
|
73
|
+
if (isTombstonedFn('prompt', promptId)) {
|
|
74
|
+
try { await fsp.unlink(mainPath); } catch { /* */ }
|
|
75
|
+
try { await fsp.unlink(answerPath); } catch { /* */ }
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
let parsed;
|
|
79
|
+
try {
|
|
80
|
+
parsed = JSON.parse(await fsp.readFile(mainPath, 'utf-8'));
|
|
81
|
+
} catch { continue; /* malformed — skip */ }
|
|
82
|
+
// KJC-BUG-0038: zombie detection. Prefer parsed.createdAt; fall back
|
|
83
|
+
// to mtime if the runner skipped the timestamp (older formats).
|
|
84
|
+
const createdMs = Date.parse(parsed?.createdAt || '');
|
|
85
|
+
const ageRef = Number.isFinite(createdMs) ? createdMs : await safeMtime(mainPath);
|
|
86
|
+
if (ageRef && (now - ageRef) > PROMPT_ZOMBIE_TTL_MS) {
|
|
87
|
+
try { await fsp.unlink(mainPath); } catch { /* */ }
|
|
88
|
+
try { await fsp.unlink(answerPath); } catch { /* */ }
|
|
89
|
+
addTombstone('prompt', promptId, { source: 'zombie-ttl', fsPaths: [mainPath, answerPath] });
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
result.push(parsed);
|
|
93
|
+
}
|
|
94
|
+
result.sort((a, b) => (a.createdAt || '').localeCompare(b.createdAt || ''));
|
|
95
|
+
res.json(result);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
res.status(500).json({ error: err.message });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
async function safeMtime(p) {
|
|
102
|
+
try { return (await fsp.stat(p)).mtimeMs; } catch { return 0; }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* POST /api/prompts/:promptId/answer - Body { answer }. Writes a
|
|
107
|
+
* sibling .answer.json the runner is polling for. The runner reads
|
|
108
|
+
* it, deletes both files, and resolves askQuestion. An answer of
|
|
109
|
+
* "stop" tells the runner to abort the session.
|
|
110
|
+
*/
|
|
111
|
+
router.post('/prompts/:promptId/answer', async (req, res) => {
|
|
112
|
+
try {
|
|
113
|
+
const { answer } = req.body || {};
|
|
114
|
+
if (typeof answer !== 'string') {
|
|
115
|
+
return res.status(400).json({ error: 'Body must contain an "answer" string' });
|
|
116
|
+
}
|
|
117
|
+
const dir = promptsDir();
|
|
118
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
119
|
+
const promptPath = path.join(dir, `${req.params.promptId}.json`);
|
|
120
|
+
if (!(await pathExists(promptPath))) {
|
|
121
|
+
return res.status(404).json({ error: `Prompt not found: ${req.params.promptId}` });
|
|
122
|
+
}
|
|
123
|
+
const answerPath = path.join(dir, `${req.params.promptId}.answer.json`);
|
|
124
|
+
await fsp.writeFile(
|
|
125
|
+
answerPath,
|
|
126
|
+
JSON.stringify({ answer, answeredAt: new Date().toISOString() }, null, 2),
|
|
127
|
+
'utf-8'
|
|
128
|
+
);
|
|
129
|
+
res.json({ ok: true });
|
|
130
|
+
} catch (err) {
|
|
131
|
+
res.status(500).json({ error: err.message });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
export default router;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// KJC-TSK-0541: the session endpoints of /api, composed by routes/api.js.
|
|
2
|
+
import { Router } from 'express';
|
|
3
|
+
import fsp from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
getProjects,
|
|
8
|
+
getSessionsByProject,
|
|
9
|
+
getSessionDetail,
|
|
10
|
+
deleteSession,
|
|
11
|
+
getKjHome,
|
|
12
|
+
addTombstone,
|
|
13
|
+
} from '../../db.js';
|
|
14
|
+
|
|
15
|
+
const router = Router();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* GET /api/projects/:id/sessions - Sessions for a project.
|
|
19
|
+
*/
|
|
20
|
+
router.get('/projects/:id/sessions', (req, res) => {
|
|
21
|
+
try {
|
|
22
|
+
const sessions = getSessionsByProject(req.params.id);
|
|
23
|
+
res.json(sessions);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
res.status(500).json({ error: err.message });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* GET /api/sessions/:id - Session detail with stages, commits, duration.
|
|
31
|
+
*/
|
|
32
|
+
router.get('/sessions/:id', (req, res) => {
|
|
33
|
+
try {
|
|
34
|
+
const session = getSessionDetail(req.params.id);
|
|
35
|
+
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
36
|
+
// Parse JSON fields for the response
|
|
37
|
+
const parsed = { ...session };
|
|
38
|
+
for (const field of ['checkpoints', 'llm_calls', 'config_snapshot', 'budget', 'commits', 'stages_completed']) {
|
|
39
|
+
if (parsed[field] && typeof parsed[field] === 'string') {
|
|
40
|
+
try { parsed[field] = JSON.parse(parsed[field]); } catch { /* keep as string */ }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
res.json(parsed);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
res.status(500).json({ error: err.message });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* GET /api/sessions - All sessions across all projects.
|
|
51
|
+
*/
|
|
52
|
+
router.get('/sessions', (_req, res) => {
|
|
53
|
+
try {
|
|
54
|
+
const allProjects = getProjects();
|
|
55
|
+
const sessions = [];
|
|
56
|
+
for (const p of allProjects) {
|
|
57
|
+
sessions.push(...getSessionsByProject(p.id));
|
|
58
|
+
}
|
|
59
|
+
// Also get default project sessions
|
|
60
|
+
sessions.push(...getSessionsByProject('default'));
|
|
61
|
+
// Deduplicate
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
const unique = sessions.filter((s) => {
|
|
64
|
+
if (seen.has(s.id)) return false;
|
|
65
|
+
seen.add(s.id);
|
|
66
|
+
return true;
|
|
67
|
+
});
|
|
68
|
+
unique.sort((a, b) => (b.created_at || '').localeCompare(a.created_at || ''));
|
|
69
|
+
res.json(unique);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
res.status(500).json({ error: err.message });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* DELETE /api/sessions/:id - Delete a single session.
|
|
77
|
+
*
|
|
78
|
+
* Tombstones + removes `~/.karajan/sessions/<id>/` (KJC-TSK-0380).
|
|
79
|
+
*/
|
|
80
|
+
router.delete('/sessions/:id', async (req, res) => {
|
|
81
|
+
try {
|
|
82
|
+
const id = req.params.id;
|
|
83
|
+
const dir = path.join(getKjHome(), 'sessions', id);
|
|
84
|
+
const ok = deleteSession(id);
|
|
85
|
+
if (!ok) return res.status(404).json({ error: 'Session not found' });
|
|
86
|
+
addTombstone('session', id, { source: 'api', fsPaths: [dir] });
|
|
87
|
+
let dirRemoved = false;
|
|
88
|
+
try { await fsp.rm(dir, { recursive: true, force: true }); dirRemoved = true; } catch { /* */ }
|
|
89
|
+
res.json({ deleted: true, dirRemoved });
|
|
90
|
+
} catch (err) {
|
|
91
|
+
res.status(500).json({ error: err.message });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export default router;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// KJC-TSK-0541: the story (HU) endpoints of /api, composed by routes/api.js.
|
|
2
|
+
import { Router } from 'express';
|
|
3
|
+
|
|
4
|
+
import { getStoriesByProject, getStoryDetail, getStoryRow } from '../../db.js';
|
|
5
|
+
import { setHuStatus, setHuFields } from '../../plan-mutations.js';
|
|
6
|
+
|
|
7
|
+
const router = Router();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* GET /api/projects/:id/stories - Stories for a project.
|
|
11
|
+
*/
|
|
12
|
+
router.get('/projects/:id/stories', (req, res) => {
|
|
13
|
+
try {
|
|
14
|
+
const stories = getStoriesByProject(req.params.id);
|
|
15
|
+
res.json(stories);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
res.status(500).json({ error: err.message });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* GET /api/stories/:id - Story detail with quality scores and context requests.
|
|
23
|
+
*/
|
|
24
|
+
router.get('/stories/:id', (req, res) => {
|
|
25
|
+
try {
|
|
26
|
+
const story = getStoryDetail(req.params.id);
|
|
27
|
+
if (!story) return res.status(404).json({ error: 'Story not found' });
|
|
28
|
+
res.json(story);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
res.status(500).json({ error: err.message });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* DELETE /api/stories/:id - Refused: stories are never deleted (KJC-TSK-0671).
|
|
36
|
+
*/
|
|
37
|
+
router.delete('/stories/:id', (req, res) => {
|
|
38
|
+
// KJC-TSK-0671 (absolute product rule): stories are NEVER deleted — the
|
|
39
|
+
// delete-and-recreate "fix" agents improvise loses history and produced
|
|
40
|
+
// the field saga this closes. Discard goes through the lifecycle instead.
|
|
41
|
+
res.status(405).json({
|
|
42
|
+
error: 'Stories are never deleted. Discard one with: kj hu move <id> skipped '
|
|
43
|
+
+ '(archived, history kept). Dashboard warnings are fixed with the command '
|
|
44
|
+
+ 'they name — see kj brief board.',
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* PATCH /api/stories/:id - Edit a single HU.
|
|
50
|
+
*
|
|
51
|
+
* Accepts two shapes in the same call (both optional, either or both
|
|
52
|
+
* can be present):
|
|
53
|
+
*
|
|
54
|
+
* { status: "pending" | "certified" | "done" | "needs_context" }
|
|
55
|
+
* → changes the lifecycle state (original PATCH semantics).
|
|
56
|
+
*
|
|
57
|
+
* { title?, scope?, task_type?, acceptance_criteria?,
|
|
58
|
+
* acceptance_tests?, blocked_by? }
|
|
59
|
+
* → edits the user-facing fields in place. Whitelist mirrors
|
|
60
|
+
* plan-hu-ops::updateHu so CLI and board stay aligned.
|
|
61
|
+
*
|
|
62
|
+
* When both are present we apply status last — the status change
|
|
63
|
+
* transitions the plan lifecycle and can rewrite plan.status, whereas
|
|
64
|
+
* field edits are local to the HU and should never fight the status
|
|
65
|
+
* auto-promotion.
|
|
66
|
+
*
|
|
67
|
+
* The row id on the board is `${projectId}::${huId}` — split here to
|
|
68
|
+
* feed the plan-mutation helpers. The source-of-truth plan JSON is
|
|
69
|
+
* rewritten before we ack, then re-synced into SQLite, so a reload
|
|
70
|
+
* renders the committed state.
|
|
71
|
+
*/
|
|
72
|
+
// KJC-TSK-0394 PR6 (AC 6): el PATCH endpoint sólo acepta el modelo
|
|
73
|
+
// canónico {pending, running, done}. Status legacy (`certified`,
|
|
74
|
+
// `coding`, `reviewing`, `failed`, `blocked`, `needs_context`) siguen
|
|
75
|
+
// vivos en plans existentes y los traduce la UI con canonicalStatus(),
|
|
76
|
+
// pero NO pueden ser seteados por escrituras nuevas — devolvemos 400
|
|
77
|
+
// con `suggestion` apuntando al mapping correcto. `running` queda fuera
|
|
78
|
+
// del dropdown del modal (lifecycle del orquestador), pero el endpoint
|
|
79
|
+
// lo acepta por si un programa externo lo necesita.
|
|
80
|
+
const ALLOWED_STORY_STATUSES = new Set(['pending', 'running', 'done']);
|
|
81
|
+
// Mapping canónico para la sugerencia del 400 cuando el caller manda
|
|
82
|
+
// un valor legacy. Mantenemos la regla viva en src/plan/plan-schema.js
|
|
83
|
+
// (mapLegacyStatusToResult) — esta tabla es sólo el subset accionable
|
|
84
|
+
// desde el endpoint (status + result).
|
|
85
|
+
const LEGACY_STATUS_SUGGESTION = {
|
|
86
|
+
certified: { status: 'done', result: 'pass' },
|
|
87
|
+
failed: { status: 'pending', result: 'fail' },
|
|
88
|
+
coding: { status: 'running', result: null },
|
|
89
|
+
reviewing: { status: 'running', result: null },
|
|
90
|
+
blocked: { status: 'pending', result: null },
|
|
91
|
+
needs_context: { status: 'pending', result: null },
|
|
92
|
+
};
|
|
93
|
+
// KJC-TSK-0406: coder_model y reviewer_model son editables desde el
|
|
94
|
+
// modal del board. Independientes — el usuario puede subir el reviewer
|
|
95
|
+
// y bajar el coder sin afectar al resto del plan.
|
|
96
|
+
// KJC-PRP-0002 PR6: `assignee` is editable from the board modal when the
|
|
97
|
+
// project is team-shared. The whitelist itself is project-agnostic — the
|
|
98
|
+
// modal hides the input on non-shared projects, but accepting the field
|
|
99
|
+
// here doesn't hurt (sync just stores it).
|
|
100
|
+
const EDITABLE_HU_FIELDS = ['title', 'scope', 'task_type', 'acceptance_criteria', 'acceptance_tests', 'blocked_by', 'coder_model', 'reviewer_model', 'coder_provider', 'reviewer_provider', 'assignee'];
|
|
101
|
+
|
|
102
|
+
router.patch('/stories/:id', (req, res) => {
|
|
103
|
+
try {
|
|
104
|
+
const body = req.body || {};
|
|
105
|
+
const hasStatus = Object.prototype.hasOwnProperty.call(body, 'status');
|
|
106
|
+
const fieldPatch = {};
|
|
107
|
+
for (const k of EDITABLE_HU_FIELDS) {
|
|
108
|
+
if (Object.prototype.hasOwnProperty.call(body, k)) fieldPatch[k] = body[k];
|
|
109
|
+
}
|
|
110
|
+
const hasFieldEdit = Object.keys(fieldPatch).length > 0;
|
|
111
|
+
|
|
112
|
+
if (!hasStatus && !hasFieldEdit) {
|
|
113
|
+
return res.status(400).json({
|
|
114
|
+
error: `Body must include status or one of: ${EDITABLE_HU_FIELDS.join(', ')}`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (hasStatus && !ALLOWED_STORY_STATUSES.has(body.status)) {
|
|
118
|
+
// KJC-TSK-0394 PR6 (AC 6): si el caller manda un status legacy,
|
|
119
|
+
// devolvemos un 400 con `suggestion` apuntando al mapping canónico.
|
|
120
|
+
// Una herramienta externa puede leer el body, aplicar el mapeo y
|
|
121
|
+
// re-intentar — sin tener que descubrir la tabla por ensayo y error.
|
|
122
|
+
const suggestion = LEGACY_STATUS_SUGGESTION[body.status] || null;
|
|
123
|
+
return res.status(400).json({
|
|
124
|
+
error: `status must be one of: ${[...ALLOWED_STORY_STATUSES].join(', ')}`,
|
|
125
|
+
suggestion,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const row = getStoryRow(req.params.id);
|
|
130
|
+
if (!row) return res.status(404).json({ error: 'Story not found' });
|
|
131
|
+
if (!row.plan_id) {
|
|
132
|
+
return res.status(409).json({
|
|
133
|
+
error: 'Story is not backed by a plan file (legacy row). Re-run `kj plan` to re-import it.',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const huId = req.params.id.includes('::')
|
|
137
|
+
? req.params.id.split('::').slice(1).join('::')
|
|
138
|
+
: req.params.id;
|
|
139
|
+
|
|
140
|
+
let updatedHu;
|
|
141
|
+
if (hasFieldEdit) {
|
|
142
|
+
const fieldResult = setHuFields({
|
|
143
|
+
planId: row.plan_id,
|
|
144
|
+
huId,
|
|
145
|
+
patch: fieldPatch,
|
|
146
|
+
projectId: row.project_id,
|
|
147
|
+
});
|
|
148
|
+
if (!fieldResult.ok) {
|
|
149
|
+
// KJC-TSK-0401: validación de blocked_by devuelve 400 con cycle?
|
|
150
|
+
// info para que el board destaque la cadena en la UI. Resto de
|
|
151
|
+
// fallos siguen devolviendo 404 (plan / hu no encontrados).
|
|
152
|
+
if (fieldResult.code === 'INVALID_BLOCKED_BY') {
|
|
153
|
+
return res.status(400).json({ error: fieldResult.error, cycle: fieldResult.cycle });
|
|
154
|
+
}
|
|
155
|
+
return res.status(404).json({ error: fieldResult.error });
|
|
156
|
+
}
|
|
157
|
+
updatedHu = fieldResult.hu;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let statusResult;
|
|
161
|
+
if (hasStatus) {
|
|
162
|
+
statusResult = setHuStatus({
|
|
163
|
+
planId: row.plan_id,
|
|
164
|
+
huId,
|
|
165
|
+
status: body.status,
|
|
166
|
+
projectId: row.project_id,
|
|
167
|
+
});
|
|
168
|
+
if (!statusResult.ok) return res.status(404).json({ error: statusResult.error });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
res.json({
|
|
172
|
+
updated: true,
|
|
173
|
+
id: req.params.id,
|
|
174
|
+
status: statusResult ? statusResult.status : undefined,
|
|
175
|
+
planStatus: statusResult ? statusResult.planStatus : undefined,
|
|
176
|
+
hu: updatedHu,
|
|
177
|
+
});
|
|
178
|
+
} catch (err) {
|
|
179
|
+
res.status(500).json({ error: err.message });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
export default router;
|