@karajan-family/code 4.21.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/LICENSE +661 -0
- package/README.md +98 -0
- package/bin/karajan-mcp.js +2 -0
- package/bin/kj-rag-mcp.js +75 -0
- package/bin/kj-tail +361 -0
- package/bin/kj.js +2 -0
- package/docs/README.es.md +80 -0
- package/docs/karajan-code-logo-small.png +0 -0
- package/library/logical-clocks-lamport.md +40 -0
- package/package.json +140 -0
- package/packages/ai-trash/README.md +37 -0
- package/packages/ai-trash/bin/kj-trash.js +10 -0
- package/packages/ai-trash/package.json +43 -0
- package/packages/ai-trash/src/cli.js +200 -0
- package/packages/ai-trash/src/destructive-parser.js +74 -0
- package/packages/ai-trash/src/git-snapshot.js +128 -0
- package/packages/ai-trash/src/hook.js +137 -0
- package/packages/ai-trash/src/index.js +14 -0
- package/packages/ai-trash/src/install.js +73 -0
- package/packages/ai-trash/src/logger.js +60 -0
- package/packages/ai-trash/src/manifest.js +91 -0
- package/packages/ai-trash/src/permissions.js +26 -0
- package/packages/ai-trash/src/snapshot.js +66 -0
- package/packages/ai-trash/src/ulid.js +66 -0
- package/packages/hu-board/README.md +69 -0
- package/packages/hu-board/package.json +28 -0
- package/packages/hu-board/public/app.js +259 -0
- package/packages/hu-board/public/index.html +77 -0
- package/packages/hu-board/public/pipeline.css +132 -0
- package/packages/hu-board/public/pipeline.html +48 -0
- package/packages/hu-board/public/pipeline.js +200 -0
- package/packages/hu-board/public/rag.html +21 -0
- package/packages/hu-board/public/rag.js +39 -0
- package/packages/hu-board/public/styles.css +973 -0
- package/packages/hu-board/public/utils/api.js +139 -0
- package/packages/hu-board/public/utils/board-view.js +473 -0
- package/packages/hu-board/public/utils/command-launcher.js +213 -0
- package/packages/hu-board/public/utils/config-editor.js +181 -0
- package/packages/hu-board/public/utils/dashboard-view.js +103 -0
- package/packages/hu-board/public/utils/formatters.js +300 -0
- package/packages/hu-board/public/utils/graph-view.js +216 -0
- package/packages/hu-board/public/utils/hu-actions.js +224 -0
- package/packages/hu-board/public/utils/init-listeners.js +180 -0
- package/packages/hu-board/public/utils/log-panel.js +222 -0
- package/packages/hu-board/public/utils/modals.js +309 -0
- package/packages/hu-board/public/utils/plan-rollup-view.js +53 -0
- package/packages/hu-board/public/utils/preflight-view.js +89 -0
- package/packages/hu-board/public/utils/project-actions.js +143 -0
- package/packages/hu-board/public/utils/project-picker-view.js +139 -0
- package/packages/hu-board/public/utils/run-launcher.js +169 -0
- package/packages/hu-board/public/utils/server-push.js +252 -0
- package/packages/hu-board/public/utils/sessions-view.js +199 -0
- package/packages/hu-board/public/utils/story-detail-view.js +324 -0
- package/packages/hu-board/public/utils/story-edit-form.js +271 -0
- package/packages/hu-board/public/wiki.html +32 -0
- package/packages/hu-board/public/wiki.js +53 -0
- package/packages/hu-board/src/auth.js +92 -0
- package/packages/hu-board/src/boot-info.js +16 -0
- package/packages/hu-board/src/cleanup-zombies.js +145 -0
- package/packages/hu-board/src/command-runner.js +133 -0
- package/packages/hu-board/src/config-yaml.js +531 -0
- package/packages/hu-board/src/db.js +963 -0
- package/packages/hu-board/src/ephemeral-cleaner.js +196 -0
- package/packages/hu-board/src/event-bus.js +58 -0
- package/packages/hu-board/src/format.js +207 -0
- package/packages/hu-board/src/hu-zombie-reaper.js +103 -0
- package/packages/hu-board/src/journal-parser.js +180 -0
- package/packages/hu-board/src/plan-mutations.js +424 -0
- package/packages/hu-board/src/preflight.js +286 -0
- package/packages/hu-board/src/routes/api.js +1374 -0
- package/packages/hu-board/src/routes/pipeline.js +45 -0
- package/packages/hu-board/src/routes/rag.js +42 -0
- package/packages/hu-board/src/routes/wiki.js +43 -0
- package/packages/hu-board/src/run-tracker.js +121 -0
- package/packages/hu-board/src/server.js +438 -0
- package/packages/hu-board/src/sync.js +732 -0
- package/packages/hu-board/src/token-store.js +56 -0
- package/packages/hu-board/src/zombie-reaper.js +206 -0
- package/scripts/ai-attribution-guard.yml +84 -0
- package/scripts/audit-test-diet.mjs +151 -0
- package/scripts/build-sea.mjs +150 -0
- package/scripts/diet-baseline.mjs +170 -0
- package/scripts/dual-publish.mjs +67 -0
- package/scripts/esbuild-sea.config.mjs +273 -0
- package/scripts/git-hooks/post-merge +18 -0
- package/scripts/install-binary.ps1 +174 -0
- package/scripts/install-binary.sh +159 -0
- package/scripts/install-guard-all-repos.sh +85 -0
- package/scripts/install-kj.sh +193 -0
- package/scripts/install.js +901 -0
- package/scripts/install.sh +7 -0
- package/scripts/postinstall.js +117 -0
- package/scripts/protect-all-repos.sh +88 -0
- package/scripts/quickstart-gate.mjs +97 -0
- package/scripts/regen-arch-stats.sh +71 -0
- package/scripts/setup-multi-instance.sh +150 -0
- package/scripts/verify-pack.mjs +278 -0
- package/src/activity-log.js +59 -0
- package/src/agents/agy-agent.js +62 -0
- package/src/agents/aider-agent.js +78 -0
- package/src/agents/availability.js +52 -0
- package/src/agents/base-agent.js +134 -0
- package/src/agents/claude-agent.js +413 -0
- package/src/agents/codex-agent.js +112 -0
- package/src/agents/copilot-agent.js +65 -0
- package/src/agents/gemini-agent.js +85 -0
- package/src/agents/host-agent.js +38 -0
- package/src/agents/index.js +59 -0
- package/src/agents/kimi-agent.js +45 -0
- package/src/agents/model-registry.js +137 -0
- package/src/agents/opencode-agent.js +72 -0
- package/src/agents/qwen-agent.js +20 -0
- package/src/agents/resolve-bin.js +73 -0
- package/src/audit/agent-readiness.js +259 -0
- package/src/audit/ai-slop-findings.js +102 -0
- package/src/audit/audit-fallback.js +66 -0
- package/src/audit/audit-history-display.js +61 -0
- package/src/audit/audit-history.js +102 -0
- package/src/audit/basal-cost.js +281 -0
- package/src/audit/circular-deps.js +144 -0
- package/src/audit/dead-exports.js +186 -0
- package/src/audit/deterministic-summary.js +327 -0
- package/src/audit/harness-scorecard.js +100 -0
- package/src/audit/harness-section.js +81 -0
- package/src/audit/infra-findings.js +100 -0
- package/src/audit/injection-findings.js +114 -0
- package/src/audit/issue-filter.js +133 -0
- package/src/audit/osv-findings.js +167 -0
- package/src/audit/plan-adherence.js +224 -0
- package/src/audit/semgrep-findings.js +129 -0
- package/src/audit/sonar-findings.js +93 -0
- package/src/audit/webperf-input.js +133 -0
- package/src/autonomy/arbiter-role.js +71 -0
- package/src/autonomy/arbiter.js +22 -0
- package/src/autonomy/decider.js +18 -0
- package/src/autonomy/decision-resolver.js +58 -0
- package/src/autonomy/outcome-report.js +55 -0
- package/src/autonomy/policy.js +41 -0
- package/src/autonomy/stage-ask.js +30 -0
- package/src/bootstrap.js +276 -0
- package/src/brain/agent-error-classifier.js +116 -0
- package/src/brain/decision-tracker.js +125 -0
- package/src/brain/decisor.js +243 -0
- package/src/brain/solomon-consult.js +171 -0
- package/src/brain/standby-scheduler.js +8 -0
- package/src/brain/standby-store.js +12 -0
- package/src/brain/with-brain-recovery.js +235 -0
- package/src/budget/comparison.js +96 -0
- package/src/canvas/parse-md.js +238 -0
- package/src/canvas/schema.js +101 -0
- package/src/canvas/to-flat.js +85 -0
- package/src/canvas/validate.js +163 -0
- package/src/checks/ai-surface.js +79 -0
- package/src/checks/ai-trash.js +34 -0
- package/src/checks/binaries.js +207 -0
- package/src/checks/ci.js +105 -0
- package/src/checks/config-files.js +217 -0
- package/src/checks/dir-setup.js +103 -0
- package/src/checks/guarantee-level.js +38 -0
- package/src/checks/hardware.js +150 -0
- package/src/checks/harness-scorecard.js +41 -0
- package/src/checks/mcp-health.js +169 -0
- package/src/checks/method.js +73 -0
- package/src/checks/native-build.js +67 -0
- package/src/checks/node.js +70 -0
- package/src/checks/ollama.js +41 -0
- package/src/checks/orphans.js +77 -0
- package/src/checks/ports.js +188 -0
- package/src/checks/project-checks.js +245 -0
- package/src/checks/qmd.js +41 -0
- package/src/checks/rag-hooks.js +54 -0
- package/src/checks/release-check.js +102 -0
- package/src/checks/rtk.js +36 -0
- package/src/checks/runner.js +326 -0
- package/src/checks/skills.js +53 -0
- package/src/checks/sonar.js +67 -0
- package/src/checks/squeezr.js +36 -0
- package/src/checks/system.js +53 -0
- package/src/checks/tokens.js +172 -0
- package/src/checks/types.js +103 -0
- package/src/ci/dispatch.js +99 -0
- package/src/ci/index.js +3 -0
- package/src/ci/pr-diff.js +26 -0
- package/src/ci/repo.js +45 -0
- package/src/cli/_shared.js +30 -0
- package/src/cli/advanced-commands.js +61 -0
- package/src/cli/register-meta.js +772 -0
- package/src/cli/register-pipeline.js +335 -0
- package/src/cli/register-plan.js +224 -0
- package/src/cli/register-roles-skills.js +54 -0
- package/src/cli/register-sonar.js +28 -0
- package/src/cli/register-standby.js +26 -0
- package/src/cli.js +155 -0
- package/src/commands/advanced.js +31 -0
- package/src/commands/agent-run.js +63 -0
- package/src/commands/agents.js +105 -0
- package/src/commands/architect.js +123 -0
- package/src/commands/audit.js +430 -0
- package/src/commands/autorun.js +49 -0
- package/src/commands/board.js +458 -0
- package/src/commands/check.js +40 -0
- package/src/commands/clean.js +145 -0
- package/src/commands/code.js +45 -0
- package/src/commands/config.js +74 -0
- package/src/commands/discover.js +105 -0
- package/src/commands/doctor.js +251 -0
- package/src/commands/env.js +207 -0
- package/src/commands/harden.js +203 -0
- package/src/commands/hu.js +210 -0
- package/src/commands/identity.js +60 -0
- package/src/commands/init.js +950 -0
- package/src/commands/install-tools.js +432 -0
- package/src/commands/mutate.js +94 -0
- package/src/commands/ollama.js +44 -0
- package/src/commands/onboard.js +45 -0
- package/src/commands/plan/_shared.js +55 -0
- package/src/commands/plan/add-hu.js +23 -0
- package/src/commands/plan/delete.js +9 -0
- package/src/commands/plan/fix.js +99 -0
- package/src/commands/plan/generate.js +532 -0
- package/src/commands/plan/index.js +13 -0
- package/src/commands/plan/list.js +21 -0
- package/src/commands/plan/migrate-result.js +41 -0
- package/src/commands/plan/ready.js +46 -0
- package/src/commands/plan/remove-hu.js +17 -0
- package/src/commands/plan/share.js +85 -0
- package/src/commands/plan/show.js +81 -0
- package/src/commands/plan/unshare.js +41 -0
- package/src/commands/plan/validate.js +20 -0
- package/src/commands/plan.js +17 -0
- package/src/commands/policy.js +164 -0
- package/src/commands/privacy.js +45 -0
- package/src/commands/qmd.js +86 -0
- package/src/commands/rag-mcp.js +18 -0
- package/src/commands/rag.js +173 -0
- package/src/commands/report-issue.js +77 -0
- package/src/commands/report.js +469 -0
- package/src/commands/researcher.js +54 -0
- package/src/commands/resume.js +68 -0
- package/src/commands/review-gate.js +355 -0
- package/src/commands/review.js +89 -0
- package/src/commands/roles.js +116 -0
- package/src/commands/run.js +185 -0
- package/src/commands/scan.js +18 -0
- package/src/commands/skills.js +86 -0
- package/src/commands/sonar.js +53 -0
- package/src/commands/standby.js +117 -0
- package/src/commands/start.js +121 -0
- package/src/commands/status.js +36 -0
- package/src/commands/sync.js +103 -0
- package/src/commands/telemetry.js +70 -0
- package/src/commands/tournament.js +85 -0
- package/src/commands/triage.js +78 -0
- package/src/commands/undo.js +62 -0
- package/src/commands/watch.js +46 -0
- package/src/commands/webperf.js +162 -0
- package/src/commands/worktree.js +76 -0
- package/src/config/defaults.js +270 -0
- package/src/config/loader.js +204 -0
- package/src/config/overrides.js +220 -0
- package/src/config/role-resolver.js +171 -0
- package/src/config/schema.js +299 -0
- package/src/config/test-harness.js +78 -0
- package/src/config.js +34 -0
- package/src/domains/domain-loader.js +245 -0
- package/src/domains/domain-registry.js +140 -0
- package/src/domains/domain-synthesizer.js +129 -0
- package/src/environment/adr.js +45 -0
- package/src/environment/board-access.js +97 -0
- package/src/environment/board-select.js +59 -0
- package/src/environment/briefs.js +129 -0
- package/src/environment/issue-report.js +72 -0
- package/src/environment/mcp-wiring.js +44 -0
- package/src/environment/playbook.js +127 -0
- package/src/git/automation.js +334 -0
- package/src/git/hu-automation.js +171 -0
- package/src/git/hu-snapshot.js +10 -0
- package/src/golden/asserter.js +92 -0
- package/src/golden/loader.js +52 -0
- package/src/golden/runner.js +92 -0
- package/src/golden/schema.js +41 -0
- package/src/guards/intent-guard.js +161 -0
- package/src/guards/output-guard.js +149 -0
- package/src/guards/perf-guard.js +126 -0
- package/src/guards/policy-guard.js +90 -0
- package/src/guards/policy-resolver.js +85 -0
- package/src/guards/secret-redactor.js +71 -0
- package/src/harden/advisory.js +203 -0
- package/src/harden/alternatives.js +50 -0
- package/src/harden/check.js +81 -0
- package/src/harden/config-engine.js +128 -0
- package/src/harden/config-templates.js +122 -0
- package/src/harden/guidelines-engine.js +48 -0
- package/src/harden/guidelines-templates.js +36 -0
- package/src/harden/harden-engine.js +80 -0
- package/src/harden/harness-hooks.js +107 -0
- package/src/harden/hook-commands.js +25 -0
- package/src/harden/hook-templates.js +183 -0
- package/src/harden/interactive.js +76 -0
- package/src/harden/sentinel-hooks.js +721 -0
- package/src/harden/stack-roots.js +105 -0
- package/src/harden/test-on-push.js +38 -0
- package/src/harden/workflow-engine.js +116 -0
- package/src/harden/workflow-templates.js +285 -0
- package/src/hu/acceptance-runner.js +138 -0
- package/src/hu/auto-generator.js +358 -0
- package/src/hu/graph.js +71 -0
- package/src/hu/lazy-planner.js +109 -0
- package/src/hu/model-router.js +108 -0
- package/src/hu/parallel-executor.js +94 -0
- package/src/hu/split-validator.js +143 -0
- package/src/hu/splitting-detector.js +158 -0
- package/src/hu/splitting-generator.js +175 -0
- package/src/hu/store.js +392 -0
- package/src/hu/worktree-bootstrap.js +59 -0
- package/src/identity/bootstrap.js +48 -0
- package/src/identity/compare.js +38 -0
- package/src/identity/detect.js +51 -0
- package/src/identity/store.js +66 -0
- package/src/infrastructure/command-runner.js +44 -0
- package/src/infrastructure/environment.js +38 -0
- package/src/infrastructure/file-system-service.js +63 -0
- package/src/infrastructure/mocks.js +182 -0
- package/src/lang/chunk-go.js +94 -0
- package/src/lang/chunk-java.js +90 -0
- package/src/lang/chunk-python.js +88 -0
- package/src/lang/chunk-rust.js +102 -0
- package/src/lang/go.js +14 -0
- package/src/lang/java.js +16 -0
- package/src/lang/js.js +8 -0
- package/src/lang/python.js +14 -0
- package/src/lang/registry.js +79 -0
- package/src/lang/rust.js +14 -0
- package/src/lang/tree-sitter-loader.js +45 -0
- package/src/mcp/direct-role-runner.js +112 -0
- package/src/mcp/error-classifiers.js +66 -0
- package/src/mcp/handlers/direct-handlers.js +486 -0
- package/src/mcp/handlers/hu-handlers.js +93 -0
- package/src/mcp/handlers/management-handlers.js +265 -0
- package/src/mcp/handlers/rag-handler.js +52 -0
- package/src/mcp/handlers/run-handler.js +313 -0
- package/src/mcp/orphan-guard.js +131 -0
- package/src/mcp/preflight.js +47 -0
- package/src/mcp/progress.js +149 -0
- package/src/mcp/response-compressor.js +81 -0
- package/src/mcp/run-kj.js +135 -0
- package/src/mcp/server-handlers.js +79 -0
- package/src/mcp/server.js +66 -0
- package/src/mcp/shared-helpers.js +297 -0
- package/src/mcp/sovereignty-guard.js +112 -0
- package/src/mcp/suggest-handler.js +92 -0
- package/src/mcp/tool-arg-normalizers.js +16 -0
- package/src/mcp/tools.js +432 -0
- package/src/mutate/diff-scope.js +105 -0
- package/src/mutate/reviewer-signal.js +76 -0
- package/src/mutate/runner.js +72 -0
- package/src/mutate/tool-registry.js +90 -0
- package/src/onboarder/cache.js +24 -0
- package/src/onboarder/collectors/framework-multilang.js +64 -0
- package/src/onboarder/collectors/index.js +136 -0
- package/src/orchestrator/agent-fallback.js +81 -0
- package/src/orchestrator/brain-coordinator.js +186 -0
- package/src/orchestrator/ci-integration.js +139 -0
- package/src/orchestrator/config-init.js +510 -0
- package/src/orchestrator/direct-actions.js +217 -0
- package/src/orchestrator/drivers/error-recovery.js +287 -0
- package/src/orchestrator/drivers/init-context.js +227 -0
- package/src/orchestrator/drivers/iteration-loop.js +380 -0
- package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +69 -0
- package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
- package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +114 -0
- package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
- package/src/orchestrator/drivers/post-loop.js +383 -0
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +136 -0
- package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
- package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +391 -0
- package/src/orchestrator/drivers/pre-loop.js +507 -0
- package/src/orchestrator/drivers/run-hu-batch.js +427 -0
- package/src/orchestrator/feedback-enrichment.js +140 -0
- package/src/orchestrator/feedback-queue.js +151 -0
- package/src/orchestrator/flow-control.js +170 -0
- package/src/orchestrator/flow-runner.js +364 -0
- package/src/orchestrator/fs-leak-detector.js +210 -0
- package/src/orchestrator/hu-scheduler.js +114 -0
- package/src/orchestrator/hu-sub-pipeline.js +616 -0
- package/src/orchestrator/integrations.js +81 -0
- package/src/orchestrator/iteration-gate.js +73 -0
- package/src/orchestrator/iteration-stages.js +9 -0
- package/src/orchestrator/parallel-limiter.js +74 -0
- package/src/orchestrator/pipeline-context.js +72 -0
- package/src/orchestrator/post-loop-stages.js +468 -0
- package/src/orchestrator/pre-loop-stages.js +11 -0
- package/src/orchestrator/preflight-checks.js +462 -0
- package/src/orchestrator/repair/repair-detector.js +117 -0
- package/src/orchestrator/repair/repair-runner.js +142 -0
- package/src/orchestrator/reviewer-fallback.js +45 -0
- package/src/orchestrator/role-output-compressor.js +189 -0
- package/src/orchestrator/session-journal.js +308 -0
- package/src/orchestrator/solomon-escalation.js +305 -0
- package/src/orchestrator/solomon-rules.js +212 -0
- package/src/orchestrator/stages/architect-stage.js +172 -0
- package/src/orchestrator/stages/coder-stage.js +480 -0
- package/src/orchestrator/stages/domain-curator-stage.js +82 -0
- package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
- package/src/orchestrator/stages/hu-reviewer-stage.js +394 -0
- package/src/orchestrator/stages/perf-stage.js +140 -0
- package/src/orchestrator/stages/planner-stage.js +85 -0
- package/src/orchestrator/stages/rag-context-stage.js +77 -0
- package/src/orchestrator/stages/rag-preload-decisor.js +58 -0
- package/src/orchestrator/stages/research-stage.js +69 -0
- package/src/orchestrator/stages/reviewer-stage.js +422 -0
- package/src/orchestrator/stages/sonar-stage.js +421 -0
- package/src/orchestrator/stages/stage-classes.js +84 -0
- package/src/orchestrator/stages/stage-executor.js +155 -0
- package/src/orchestrator/stages/tdd-discipline-stage.js +56 -0
- package/src/orchestrator/stages/tool-judge-stage.js +53 -0
- package/src/orchestrator/stages/triage-stage.js +172 -0
- package/src/orchestrator/standby.js +71 -0
- package/src/orchestrator/verification-gate.js +183 -0
- package/src/orchestrator.js +22 -0
- package/src/plan/adr-generator.js +170 -0
- package/src/plan/adr-loader.js +60 -0
- package/src/plan/plan-executor.js +100 -0
- package/src/plan/plan-fixer.js +192 -0
- package/src/plan/plan-hu-ops.js +16 -0
- package/src/plan/plan-id.js +4 -0
- package/src/plan/plan-migrate-result.js +24 -0
- package/src/plan/plan-reviewer.js +236 -0
- package/src/plan/plan-schema.js +334 -0
- package/src/plan/plan-store.js +260 -0
- package/src/plan/plan-structural-pass.js +122 -0
- package/src/plan/plan-validation.js +4 -0
- package/src/plan/preflight-hu.js +105 -0
- package/src/plan/spec-detector.js +61 -0
- package/src/plan/tests-synthesizer.js +131 -0
- package/src/planning-game/adapter.js +108 -0
- package/src/planning-game/architect-adrs.js +45 -0
- package/src/planning-game/client.js +108 -0
- package/src/planning-game/decomposition.js +87 -0
- package/src/planning-game/pipeline-adapter.js +251 -0
- package/src/plugins/loader.js +67 -0
- package/src/policy/add.js +111 -0
- package/src/policy/decisions.js +42 -0
- package/src/policy/engine.js +82 -0
- package/src/policy/exceptions.js +60 -0
- package/src/privacy/onboarding.js +35 -0
- package/src/privacy/scan.js +120 -0
- package/src/prompts/arbiter.js +38 -0
- package/src/prompts/architect.js +103 -0
- package/src/prompts/audit.js +474 -0
- package/src/prompts/coder.js +287 -0
- package/src/prompts/diff-clip.js +27 -0
- package/src/prompts/discover.js +222 -0
- package/src/prompts/hu-reviewer.js +219 -0
- package/src/prompts/planner.js +312 -0
- package/src/prompts/prompt-layout.js +80 -0
- package/src/prompts/prompt-resolver.js +120 -0
- package/src/prompts/reviewer.js +75 -0
- package/src/prompts/rtk-snippet.js +25 -0
- package/src/prompts/spec-reviewer.js +66 -0
- package/src/prompts/split-signal.js +82 -0
- package/src/prompts/start-decision.js +52 -0
- package/src/prompts/tool-correctness-judge.js +31 -0
- package/src/prompts/triage.js +57 -0
- package/src/rag/auto-update.js +72 -0
- package/src/rag/chunker-ast.js +56 -0
- package/src/rag/chunker.js +132 -0
- package/src/rag/embedder.js +2 -0
- package/src/rag/embedders/_cloud-base.js +2 -0
- package/src/rag/embedders/cohere.js +2 -0
- package/src/rag/embedders/factory.js +2 -0
- package/src/rag/embedders/mistral.js +2 -0
- package/src/rag/embedders/onnx.js +2 -0
- package/src/rag/embedders/openai.js +2 -0
- package/src/rag/embedders/voyage.js +2 -0
- package/src/rag/eval.js +61 -0
- package/src/rag/governed-embedder.js +39 -0
- package/src/rag/indexer.js +199 -0
- package/src/rag/library.js +62 -0
- package/src/rag/ollama-capability.js +59 -0
- package/src/rag/ollama-manager.js +98 -0
- package/src/rag/onnx-fallback.js +69 -0
- package/src/rag/rerank.js +4 -0
- package/src/rag/retriever.js +2 -0
- package/src/rag/vec-store.js +17 -0
- package/src/rag/watcher.js +62 -0
- package/src/rag/where-parser.js +4 -0
- package/src/repeat-detector.js +88 -0
- package/src/review/card-first.js +99 -0
- package/src/review/card-verify.js +83 -0
- package/src/review/diff-generator.js +112 -0
- package/src/review/gate-gitignore.js +77 -0
- package/src/review/git-stash-helper.js +104 -0
- package/src/review/mutation-pregate.js +48 -0
- package/src/review/one-shot-review.js +122 -0
- package/src/review/parser.js +80 -0
- package/src/review/policy-gate.js +26 -0
- package/src/review/profiles.js +66 -0
- package/src/review/reviewer-fallback.js +125 -0
- package/src/review/schema.js +31 -0
- package/src/review/scope-filter.js +152 -0
- package/src/review/snapshot-diff.js +122 -0
- package/src/review/solomon-arbitration.js +113 -0
- package/src/review/sonar-pregate.js +63 -0
- package/src/review/tdd-discipline.js +90 -0
- package/src/review/tdd-policy.js +69 -0
- package/src/review/tests-with-code.js +33 -0
- package/src/review/verdict-store.js +87 -0
- package/src/review/workspace.js +21 -0
- package/src/roles/agent-role.js +194 -0
- package/src/roles/architect-role.js +77 -0
- package/src/roles/audit-role.js +258 -0
- package/src/roles/base-role.js +165 -0
- package/src/roles/coder-role.js +107 -0
- package/src/roles/commiter-role.js +110 -0
- package/src/roles/discover-role.js +81 -0
- package/src/roles/domain-curator-role.js +141 -0
- package/src/roles/hu-reviewer-role.js +64 -0
- package/src/roles/impeccable-role.js +85 -0
- package/src/roles/index.js +18 -0
- package/src/roles/karajan-brain-role.js +134 -0
- package/src/roles/onboarder-role.js +61 -0
- package/src/roles/perf-role.js +122 -0
- package/src/roles/planner-role.js +151 -0
- package/src/roles/refactorer-role.js +30 -0
- package/src/roles/repairer-role.js +181 -0
- package/src/roles/researcher-role.js +70 -0
- package/src/roles/reviewer-role.js +104 -0
- package/src/roles/security-role.js +73 -0
- package/src/roles/solomon-role.js +265 -0
- package/src/roles/sonar-role.js +70 -0
- package/src/roles/spec-reviewer-role.js +106 -0
- package/src/roles/tester-role.js +235 -0
- package/src/roles/tool-correctness-judge-role.js +78 -0
- package/src/roles/triage-role.js +99 -0
- package/src/session/journal/decisions-writer.js +200 -0
- package/src/session/journal/iteration-logger.js +194 -0
- package/src/session/journal/summary-writer.js +320 -0
- package/src/session/journal/tree-writer.js +164 -0
- package/src/session/journal/write-all.js +89 -0
- package/src/session/mutators.js +339 -0
- package/src/session/runtime-overrides.js +59 -0
- package/src/session/store.js +106 -0
- package/src/session-cleanup.js +116 -0
- package/src/skills/addyosmani-catalog.js +315 -0
- package/src/skills/addyosmani-role-map.js +139 -0
- package/src/skills/openskills-client.js +157 -0
- package/src/skills/semantic-detector.js +167 -0
- package/src/skills/skill-detector.js +385 -0
- package/src/skills/skill-loader.js +219 -0
- package/src/skills/skills-cache.js +128 -0
- package/src/sonar/api.js +128 -0
- package/src/sonar/cloud-scanner.js +78 -0
- package/src/sonar/config-resolver.js +88 -0
- package/src/sonar/credentials.js +60 -0
- package/src/sonar/discovery.js +124 -0
- package/src/sonar/enforcer.js +19 -0
- package/src/sonar/issue-filter.js +12 -0
- package/src/sonar/manager.js +181 -0
- package/src/sonar/project-key.js +120 -0
- package/src/sonar/scanner.js +307 -0
- package/src/sonar/token-bootstrap.js +205 -0
- package/src/sonar/token-recovery.js +67 -0
- package/src/spec-review/refine-loop.js +58 -0
- package/src/spec-review/run-spec-review.js +119 -0
- package/src/start/assessment.js +74 -0
- package/src/start/maturity.js +73 -0
- package/src/start/start-decider-role.js +79 -0
- package/src/start/sweep.js +157 -0
- package/src/sync/apply-canvas-patch.js +55 -0
- package/src/sync/detect-drift.js +113 -0
- package/src/tournament/crown.js +88 -0
- package/src/tournament/judge.js +150 -0
- package/src/tournament/run.js +142 -0
- package/src/tournament/scoreboard.js +124 -0
- package/src/tracker-bootstrap.js +44 -0
- package/src/types/agent.js +55 -0
- package/src/types/config.js +114 -0
- package/src/types/hu.js +47 -0
- package/src/types/index.js +38 -0
- package/src/types/policy.js +43 -0
- package/src/types/session.js +75 -0
- package/src/types/solomon.js +87 -0
- package/src/types/stage.js +68 -0
- package/src/utils/agent-detect.js +93 -0
- package/src/utils/ai-trash.js +48 -0
- package/src/utils/async-lock.js +31 -0
- package/src/utils/atomic-write.js +6 -0
- package/src/utils/banner.js +66 -0
- package/src/utils/binary-sources.js +70 -0
- package/src/utils/board-prompt-bridge.js +108 -0
- package/src/utils/budget.js +229 -0
- package/src/utils/cli-ask-question.js +93 -0
- package/src/utils/cli-progress.js +220 -0
- package/src/utils/cli-run-log.js +83 -0
- package/src/utils/cwd-confirm.js +39 -0
- package/src/utils/derive-project-name-from-cwd.js +119 -0
- package/src/utils/display/event-handlers.js +403 -0
- package/src/utils/display/formatters.js +132 -0
- package/src/utils/display/header.js +47 -0
- package/src/utils/display/resume-hint.js +41 -0
- package/src/utils/display/session-summary.js +118 -0
- package/src/utils/display/solomon.js +42 -0
- package/src/utils/display/spec-findings.js +44 -0
- package/src/utils/doc-links.js +29 -0
- package/src/utils/docker-install.js +34 -0
- package/src/utils/escape-regexp.js +15 -0
- package/src/utils/events.js +35 -0
- package/src/utils/fs.js +19 -0
- package/src/utils/garbage-collector.js +481 -0
- package/src/utils/git.js +279 -0
- package/src/utils/home-migration.js +87 -0
- package/src/utils/injection-guard.js +186 -0
- package/src/utils/install-hints.js +180 -0
- package/src/utils/json-extract.js +64 -0
- package/src/utils/locale.js +59 -0
- package/src/utils/logger.js +96 -0
- package/src/utils/managed-markers.js +93 -0
- package/src/utils/manifest-sniff.js +82 -0
- package/src/utils/messages.js +61 -0
- package/src/utils/model-selector.js +107 -0
- package/src/utils/os-detect.js +79 -0
- package/src/utils/paths.js +13 -0
- package/src/utils/pending-user-action.js +95 -0
- package/src/utils/port-check.js +6 -0
- package/src/utils/port-occupant.js +96 -0
- package/src/utils/pricing.js +29 -0
- package/src/utils/process.js +4 -0
- package/src/utils/project-detect.js +160 -0
- package/src/utils/prompt-project-name.js +35 -0
- package/src/utils/provider-env.js +116 -0
- package/src/utils/qmd-collection.js +72 -0
- package/src/utils/qmd-detect.js +18 -0
- package/src/utils/qmd-install.js +37 -0
- package/src/utils/quota-simulator.js +56 -0
- package/src/utils/rate-limit-detector.js +181 -0
- package/src/utils/repo-cleaner.js +79 -0
- package/src/utils/retry.js +88 -0
- package/src/utils/role-assigner.js +137 -0
- package/src/utils/role-env.js +58 -0
- package/src/utils/rtk-detect.js +18 -0
- package/src/utils/rtk-install.js +38 -0
- package/src/utils/rtk-wrapper.js +87 -0
- package/src/utils/run-context.js +75 -0
- package/src/utils/run-log.js +217 -0
- package/src/utils/run-registry.js +9 -0
- package/src/utils/shared-paths.js +12 -0
- package/src/utils/squeezr-detect.js +21 -0
- package/src/utils/squeezr-install.js +37 -0
- package/src/utils/stack-detect.js +135 -0
- package/src/utils/stall-detector.js +159 -0
- package/src/utils/status-dashboard.js +254 -0
- package/src/utils/task-file.js +109 -0
- package/src/utils/telemetry.js +124 -0
- package/src/utils/templates-root.js +61 -0
- package/src/utils/tool-call-extractor.js +56 -0
- package/src/utils/tool-governor.js +116 -0
- package/src/utils/tool-installer.js +93 -0
- package/src/utils/update-check.js +280 -0
- package/src/utils/vector-store-orphans.js +89 -0
- package/src/utils/welcome.js +47 -0
- package/src/utils/with-timeout.js +42 -0
- package/src/utils/wizard.js +47 -0
- package/src/webperf/cwv-gate.js +94 -0
- package/src/webperf/devtools-detect.js +112 -0
- package/src/webperf/scanner.js +209 -0
- package/src/webperf/webperf-role-integration.js +66 -0
- package/templates/coder-rules.md +45 -0
- package/templates/docker-compose.sonar.yml +60 -0
- package/templates/domains/example-dental.md +57 -0
- package/templates/kj.config.yml +128 -0
- package/templates/review-rules.md +17 -0
- package/templates/roles/architect/anthropic.md +74 -0
- package/templates/roles/architect/default.md +72 -0
- package/templates/roles/architect/google.md +61 -0
- package/templates/roles/architect/openai.md +60 -0
- package/templates/roles/architect/opencode.md +59 -0
- package/templates/roles/architect.md +76 -0
- package/templates/roles/audit.md +68 -0
- package/templates/roles/coder/anthropic.md +81 -0
- package/templates/roles/coder/default.md +78 -0
- package/templates/roles/coder/google.md +87 -0
- package/templates/roles/coder/openai.md +68 -0
- package/templates/roles/coder/opencode.md +58 -0
- package/templates/roles/coder.md +152 -0
- package/templates/roles/commiter.md +44 -0
- package/templates/roles/discover.md +167 -0
- package/templates/roles/domain-curator.md +52 -0
- package/templates/roles/hu-reviewer.md +236 -0
- package/templates/roles/impeccable-design.md +87 -0
- package/templates/roles/impeccable.md +125 -0
- package/templates/roles/karajan-brain.md +95 -0
- package/templates/roles/onboarder.md +45 -0
- package/templates/roles/planner/anthropic.md +59 -0
- package/templates/roles/planner/default.md +57 -0
- package/templates/roles/planner/google.md +75 -0
- package/templates/roles/planner/openai.md +55 -0
- package/templates/roles/planner/opencode.md +62 -0
- package/templates/roles/planner.md +61 -0
- package/templates/roles/refactorer.md +48 -0
- package/templates/roles/repairer.md +103 -0
- package/templates/roles/researcher.md +41 -0
- package/templates/roles/reviewer/anthropic.md +61 -0
- package/templates/roles/reviewer/default.md +62 -0
- package/templates/roles/reviewer/google.md +63 -0
- package/templates/roles/reviewer/openai.md +57 -0
- package/templates/roles/reviewer/opencode.md +42 -0
- package/templates/roles/reviewer-paranoid.md +38 -0
- package/templates/roles/reviewer-relaxed.md +34 -0
- package/templates/roles/reviewer-strict.md +37 -0
- package/templates/roles/reviewer.md +62 -0
- package/templates/roles/security.md +54 -0
- package/templates/roles/solomon/anthropic.md +71 -0
- package/templates/roles/solomon/default.md +83 -0
- package/templates/roles/solomon/google.md +61 -0
- package/templates/roles/solomon/openai.md +53 -0
- package/templates/roles/solomon/opencode.md +55 -0
- package/templates/roles/solomon.md +83 -0
- package/templates/roles/sonar.md +49 -0
- package/templates/roles/spec-reviewer.md +47 -0
- package/templates/roles/tester.md +50 -0
- package/templates/roles/triage.md +69 -0
- package/templates/skills/kj-architect.md +45 -0
- package/templates/skills/kj-audit.md +63 -0
- package/templates/skills/kj-board.md +16 -0
- package/templates/skills/kj-code.md +51 -0
- package/templates/skills/kj-csv-transform.md +40 -0
- package/templates/skills/kj-data-report.md +44 -0
- package/templates/skills/kj-discover.md +24 -0
- package/templates/skills/kj-rag-query.md +50 -0
- package/templates/skills/kj-review.md +47 -0
- package/templates/skills/kj-run.md +73 -0
- package/templates/skills/kj-security.md +49 -0
- package/templates/skills/kj-sonar.md +41 -0
- package/templates/skills/kj-sql-analysis.md +40 -0
- package/templates/skills/kj-test.md +40 -0
- package/templates/workflows/automerge.yml +30 -0
- package/templates/workflows/houston-override.yml +46 -0
- package/templates/workflows/kj-ci-gateway.yml +58 -0
- package/vendor/tree-sitter-grammars/README.md +21 -0
- package/vendor/tree-sitter-grammars/go.wasm +0 -0
- package/vendor/tree-sitter-grammars/java.wasm +0 -0
- package/vendor/tree-sitter-grammars/python.wasm +0 -0
- package/vendor/tree-sitter-grammars/rust.wasm +0 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan persistence — save, load, list, update, delete plans.
|
|
3
|
+
* Plans are stored at ~/.kj/plans/<projectSlug>/<planId>.json
|
|
4
|
+
* Supports v1 (text-only) and v2 (plans + HUs) schemas.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { writeJsonAtomic } from "../utils/atomic-write.js";
|
|
10
|
+
import { resolveHome } from "../utils/paths.js";
|
|
11
|
+
import { getSharedPlansDir } from "../utils/shared-paths.js";
|
|
12
|
+
import { generatePlanId, normaliseAlias } from "./plan-id.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A plan JSON file failed to parse — typically an interrupted write
|
|
16
|
+
* leaves it truncated. Previously every read site swallowed this with
|
|
17
|
+
* a silent catch and the plan vanished from `kj plan list` and
|
|
18
|
+
* `kj plan load` as if it had never existed.
|
|
19
|
+
*
|
|
20
|
+
* Now: warn loudly so the user knows their plan is corrupt, and rename
|
|
21
|
+
* the file to `<name>.corrupt-<ts>` so it stops blocking subsequent
|
|
22
|
+
* reads of the same alias / planId (without destroying evidence).
|
|
23
|
+
* Best-effort: if the rename itself fails (read-only FS, permissions),
|
|
24
|
+
* we log and move on — never escalate to a throw, that would block the
|
|
25
|
+
* caller from reading the rest of the directory.
|
|
26
|
+
*/
|
|
27
|
+
async function handleCorruptPlanFile(filePath, error) {
|
|
28
|
+
const corruptPath = `${filePath}.corrupt-${Date.now()}`;
|
|
29
|
+
// eslint-disable-next-line no-console
|
|
30
|
+
console.warn(`[plan-store] Corrupt plan JSON at ${filePath}: ${error.message}`);
|
|
31
|
+
try {
|
|
32
|
+
await fs.rename(filePath, corruptPath);
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.warn(`[plan-store] Renamed to ${path.basename(corruptPath)} so it stops blocking subsequent reads.`);
|
|
35
|
+
} catch (renameErr) {
|
|
36
|
+
// eslint-disable-next-line no-console
|
|
37
|
+
console.warn(`[plan-store] Could not rename ${filePath}: ${renameErr.message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
import { isPlanV2, migratePlanV1toV2 } from "./plan-schema.js";
|
|
41
|
+
|
|
42
|
+
// Delegates to the unified resolver in src/utils/paths.js. PR 3 of
|
|
43
|
+
// KJC-PCS-0047 flipped the default from `.kj` to `.karajan` — plans
|
|
44
|
+
// now live under `~/.karajan/plans/`. The migrator (src/utils/
|
|
45
|
+
// home-migration.js) auto-moves legacy `~/.kj/plans/` content on the
|
|
46
|
+
// next `kj` invocation, so existing users are not stranded.
|
|
47
|
+
export function getKjHome() {
|
|
48
|
+
return resolveHome({ defaultSegment: ".karajan" });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Deterministic slug for a project path — stable across plan-store writes,
|
|
53
|
+
* HU-board syncs and board URL routing. Keep in lockstep with
|
|
54
|
+
* `packages/hu-board/src/sync.js::deriveProjectIdFromDir`.
|
|
55
|
+
* @param {string} projectDir - absolute project path
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export function projectSlug(projectDir) {
|
|
59
|
+
return projectDir
|
|
60
|
+
.replace(/^\//, "")
|
|
61
|
+
.replace(/[/\\]/g, "_")
|
|
62
|
+
.replaceAll(/[^a-zA-Z0-9_.-]/g, "")
|
|
63
|
+
.slice(0, 120);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function plansDir(projectDir) {
|
|
67
|
+
return path.join(getKjHome(), "plans", projectSlug(projectDir));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Save a plan to disk. Accepts both v1 and v2 schemas.
|
|
72
|
+
* @param {string} projectDir
|
|
73
|
+
* @param {object} planResult - v1 { task, plan, raw } or v2 { planId, version, hus, ... }
|
|
74
|
+
* @returns {Promise<string>} planId
|
|
75
|
+
*/
|
|
76
|
+
export async function savePlan(projectDir, planResult) {
|
|
77
|
+
const dir = plansDir(projectDir);
|
|
78
|
+
await fs.mkdir(dir, { recursive: true });
|
|
79
|
+
|
|
80
|
+
// v2 plans already have planId and full schema
|
|
81
|
+
if (isPlanV2(planResult)) {
|
|
82
|
+
const filePath = path.join(dir, `${planResult.planId}.json`);
|
|
83
|
+
planResult.updatedAt = new Date().toISOString();
|
|
84
|
+
// TSK-0341 / garbage-collector: stamp the absolute projectDir on every
|
|
85
|
+
// plan so the GC can check `fs.exists(projectDir)` reliably. Reverse-
|
|
86
|
+
// engineering it from the slug is lossy (underscores in the original
|
|
87
|
+
// name collide with underscores introduced by slash-substitution).
|
|
88
|
+
if (!planResult.projectDir && projectDir) planResult.projectDir = projectDir;
|
|
89
|
+
// Humanización IDs: si el plan no tiene alias todavía, derívalo de
|
|
90
|
+
// plan.name (lo que el planner pidió al usuario). Resuelve
|
|
91
|
+
// colisiones con sufijo -2, -3… leyendo el dir antes.
|
|
92
|
+
if (!planResult.alias && planResult.name) {
|
|
93
|
+
planResult.alias = await resolveUniqueAlias(dir, planResult.planId, normaliseAlias(planResult.name));
|
|
94
|
+
}
|
|
95
|
+
await writeJsonAtomic(filePath, planResult);
|
|
96
|
+
return planResult.planId;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// v1: generate planId and wrap
|
|
100
|
+
const planId = planResult.planId || generatePlanId();
|
|
101
|
+
const record = {
|
|
102
|
+
planId,
|
|
103
|
+
task: planResult.task || null,
|
|
104
|
+
researchContext: planResult.researchContext || null,
|
|
105
|
+
architectContext: planResult.architectContext || null,
|
|
106
|
+
plan: planResult.plan || null,
|
|
107
|
+
raw: planResult.raw || null,
|
|
108
|
+
createdAt: new Date().toISOString()
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const filePath = path.join(dir, `${planId}.json`);
|
|
112
|
+
await writeJsonAtomic(filePath, record);
|
|
113
|
+
return planId;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resuelve un alias único en `dir`. Si `base` ya está en uso por otro
|
|
118
|
+
* plan, intenta `base-2`, `base-3`… hasta encontrar libre.
|
|
119
|
+
* @param {string} dir
|
|
120
|
+
* @param {string} ownPlanId - planId del plan que se está guardando (su propio archivo se ignora en el lookup).
|
|
121
|
+
* @param {string|null} base
|
|
122
|
+
* @returns {Promise<string|null>}
|
|
123
|
+
*/
|
|
124
|
+
async function resolveUniqueAlias(dir, ownPlanId, base) {
|
|
125
|
+
if (!base) return null;
|
|
126
|
+
let files;
|
|
127
|
+
try { files = await fs.readdir(dir); } catch { return base; }
|
|
128
|
+
const taken = new Set();
|
|
129
|
+
for (const f of files) {
|
|
130
|
+
if (!f.endsWith(".json")) continue;
|
|
131
|
+
const filePath = path.join(dir, f);
|
|
132
|
+
try {
|
|
133
|
+
const record = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
134
|
+
if (record.planId === ownPlanId) continue;
|
|
135
|
+
if (record.alias) taken.add(record.alias);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (err.code !== "ENOENT") await handleCorruptPlanFile(filePath, err);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!taken.has(base)) return base;
|
|
141
|
+
for (let i = 2; i < 1000; i += 1) {
|
|
142
|
+
const candidate = `${base}-${i}`;
|
|
143
|
+
if (!taken.has(candidate)) return candidate;
|
|
144
|
+
}
|
|
145
|
+
return `${base}-${ownPlanId.slice(-4)}`; // fallback improbable
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Load a plan by ID or alias. Auto-migrates v1 to v2 on read (lazy).
|
|
150
|
+
* Acepta tanto el planId canónico (plan-XXX-XX) como el alias humano
|
|
151
|
+
* (greta-app). Primero busca por filename exacto (planId.json), luego
|
|
152
|
+
* escanea el dir y matchea por alias.
|
|
153
|
+
*
|
|
154
|
+
* @returns {Promise<object|null>} v2 plan o null
|
|
155
|
+
*/
|
|
156
|
+
export async function loadPlan(projectDir, ref) {
|
|
157
|
+
if (!ref) return null;
|
|
158
|
+
// Local plans take precedence — if a teammate's shared copy and your
|
|
159
|
+
// working copy share a planId, your local edits win until you re-share.
|
|
160
|
+
const local = await loadPlanFromDir(plansDir(projectDir), ref);
|
|
161
|
+
if (local) return local;
|
|
162
|
+
// Fallback: shared plans committed by a teammate. KJC-PRP-0002 PR1.
|
|
163
|
+
return loadPlanFromDir(getSharedPlansDir(projectDir), ref);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function loadPlanFromDir(dir, ref) {
|
|
167
|
+
// 1. Match exacto por planId.json (camino caliente).
|
|
168
|
+
const direct = path.join(dir, `${ref}.json`);
|
|
169
|
+
try {
|
|
170
|
+
const data = await fs.readFile(direct, "utf8");
|
|
171
|
+
const plan = JSON.parse(data);
|
|
172
|
+
if (!isPlanV2(plan)) return migratePlanV1toV2(plan);
|
|
173
|
+
return plan;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
// ENOENT is expected (caller may have passed an alias, not a planId)
|
|
176
|
+
// → fall through to the alias scan. Anything else means the file is
|
|
177
|
+
// there but unreadable / corrupt; surface it instead of silently
|
|
178
|
+
// hiding the plan.
|
|
179
|
+
if (err.code !== "ENOENT") await handleCorruptPlanFile(direct, err);
|
|
180
|
+
}
|
|
181
|
+
// 2. Escaneo del dir por alias. Coste O(N) pero N suele ser <10.
|
|
182
|
+
let files;
|
|
183
|
+
try { files = await fs.readdir(dir); } catch { return null; }
|
|
184
|
+
for (const f of files) {
|
|
185
|
+
if (!f.endsWith(".json")) continue;
|
|
186
|
+
const filePath = path.join(dir, f);
|
|
187
|
+
try {
|
|
188
|
+
const record = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
189
|
+
if (record.alias === ref || record.planId === ref) {
|
|
190
|
+
if (!isPlanV2(record)) return migratePlanV1toV2(record);
|
|
191
|
+
return record;
|
|
192
|
+
}
|
|
193
|
+
} catch (err) {
|
|
194
|
+
if (err.code !== "ENOENT") await handleCorruptPlanFile(filePath, err);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Delete a plan from disk.
|
|
202
|
+
* @returns {Promise<boolean>}
|
|
203
|
+
*/
|
|
204
|
+
export async function deletePlan(projectDir, planId) {
|
|
205
|
+
const filePath = path.join(plansDir(projectDir), `${planId}.json`);
|
|
206
|
+
try {
|
|
207
|
+
await fs.unlink(filePath);
|
|
208
|
+
return true;
|
|
209
|
+
} catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* List all plans for a project, sorted newest first.
|
|
216
|
+
*/
|
|
217
|
+
export async function listPlans(projectDir) {
|
|
218
|
+
const dir = plansDir(projectDir);
|
|
219
|
+
let files;
|
|
220
|
+
try {
|
|
221
|
+
files = await fs.readdir(dir);
|
|
222
|
+
} catch {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const plans = [];
|
|
227
|
+
for (const file of files) {
|
|
228
|
+
if (!file.endsWith(".json")) continue;
|
|
229
|
+
if (file.includes(".corrupt-")) continue; // own renamed-aside files
|
|
230
|
+
const filePath = path.join(dir, file);
|
|
231
|
+
try {
|
|
232
|
+
const data = await fs.readFile(filePath, "utf8");
|
|
233
|
+
const record = JSON.parse(data);
|
|
234
|
+
plans.push({
|
|
235
|
+
planId: record.planId,
|
|
236
|
+
alias: record.alias || null,
|
|
237
|
+
task: record.task,
|
|
238
|
+
name: record.name || null,
|
|
239
|
+
status: record.status || "draft",
|
|
240
|
+
version: record.version || 1,
|
|
241
|
+
huCount: Array.isArray(record.hus) ? record.hus.length : 0,
|
|
242
|
+
createdAt: record.createdAt
|
|
243
|
+
});
|
|
244
|
+
} catch (err) {
|
|
245
|
+
if (err.code !== "ENOENT") await handleCorruptPlanFile(filePath, err);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
plans.sort((a, b) => (b.createdAt || "").localeCompare(a.createdAt || ""));
|
|
250
|
+
return plans;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get the most recent plan.
|
|
255
|
+
*/
|
|
256
|
+
export async function getLatestPlan(projectDir) {
|
|
257
|
+
const all = await listPlans(projectDir);
|
|
258
|
+
if (all.length === 0) return null;
|
|
259
|
+
return loadPlan(projectDir, all[0].planId);
|
|
260
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// KJC-TSK-0399: deterministic post-review pass — cycles, orphan refs,
|
|
2
|
+
// missing short_id. No LLM. Fix shape: { kind, huId?, ...details };
|
|
3
|
+
// stored in plan.review.structural_fixes.
|
|
4
|
+
|
|
5
|
+
export function runStructuralPass(plan) {
|
|
6
|
+
const fixes = [];
|
|
7
|
+
if (!plan || !Array.isArray(plan.hus) || plan.hus.length === 0) {
|
|
8
|
+
return { fixes, modified: false };
|
|
9
|
+
}
|
|
10
|
+
fixes.push(...removeOrphanRefs(plan));
|
|
11
|
+
fixes.push(...breakCycles(plan));
|
|
12
|
+
fixes.push(...assignMissingShortIds(plan));
|
|
13
|
+
const modified = fixes.length > 0;
|
|
14
|
+
if (modified) plan.updatedAt = new Date().toISOString();
|
|
15
|
+
return { fixes, modified };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function removeOrphanRefs(plan) {
|
|
19
|
+
const fixes = [];
|
|
20
|
+
const known = new Set(plan.hus.map((h) => h.id));
|
|
21
|
+
for (const hu of plan.hus) {
|
|
22
|
+
const removed = [];
|
|
23
|
+
if (Array.isArray(hu.blocked_by) && hu.blocked_by.length > 0) {
|
|
24
|
+
hu.blocked_by = hu.blocked_by.filter((d) => {
|
|
25
|
+
if (known.has(d)) return true;
|
|
26
|
+
removed.push(d); return false;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (removed.length > 0) fixes.push({ kind: "orphan_ref_removed", huId: hu.id, removed });
|
|
30
|
+
}
|
|
31
|
+
return fixes;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function breakCycles(plan) {
|
|
35
|
+
const fixes = [];
|
|
36
|
+
let cycles = findCycles(plan.hus);
|
|
37
|
+
let budget = plan.hus.length;
|
|
38
|
+
while (cycles.length > 0 && budget-- > 0) {
|
|
39
|
+
const cycle = cycles[0];
|
|
40
|
+
const dropped = breakCycle(plan, cycle);
|
|
41
|
+
if (!dropped) break;
|
|
42
|
+
fixes.push({ kind: "cycle_break", cycle: [...cycle], droppedEdge: dropped });
|
|
43
|
+
cycles = findCycles(plan.hus);
|
|
44
|
+
}
|
|
45
|
+
return fixes;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function findCycles(hus) {
|
|
49
|
+
const graph = new Map();
|
|
50
|
+
for (const hu of hus) graph.set(hu.id, Array.isArray(hu.blocked_by) ? hu.blocked_by : []);
|
|
51
|
+
const cycles = [];
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
const onStack = new Set();
|
|
54
|
+
const stack = [];
|
|
55
|
+
function dfs(node) {
|
|
56
|
+
onStack.add(node); stack.push(node); seen.add(node);
|
|
57
|
+
for (const dep of graph.get(node) || []) {
|
|
58
|
+
if (!graph.has(dep)) continue;
|
|
59
|
+
if (onStack.has(dep)) {
|
|
60
|
+
const idx = stack.indexOf(dep);
|
|
61
|
+
if (idx >= 0) {
|
|
62
|
+
const cycle = stack.slice(idx);
|
|
63
|
+
if (!cycles.some((c) => sameCycle(c, cycle))) cycles.push(cycle);
|
|
64
|
+
}
|
|
65
|
+
} else if (!seen.has(dep)) dfs(dep);
|
|
66
|
+
}
|
|
67
|
+
stack.pop(); onStack.delete(node);
|
|
68
|
+
}
|
|
69
|
+
for (const id of graph.keys()) if (!seen.has(id)) dfs(id);
|
|
70
|
+
return cycles;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function sameCycle(a, b) {
|
|
74
|
+
if (a.length !== b.length) return false;
|
|
75
|
+
return [...a].sort().join("|") === [...b].sort().join("|");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Heurística: dropea arista cuyo origen tiene menos incoming. Tiebreak edge.to.
|
|
79
|
+
export function breakCycle(plan, cycle) {
|
|
80
|
+
const incoming = new Map();
|
|
81
|
+
for (const hu of plan.hus) {
|
|
82
|
+
for (const dep of hu.blocked_by || []) incoming.set(dep, (incoming.get(dep) || 0) + 1);
|
|
83
|
+
}
|
|
84
|
+
const edges = [];
|
|
85
|
+
for (let i = 0; i < cycle.length; i++) {
|
|
86
|
+
const from = cycle[i];
|
|
87
|
+
const to = cycle[(i + 1) % cycle.length];
|
|
88
|
+
const hu = plan.hus.find((h) => h.id === from);
|
|
89
|
+
if (hu && Array.isArray(hu.blocked_by) && hu.blocked_by.includes(to)) edges.push({ from, to });
|
|
90
|
+
}
|
|
91
|
+
if (edges.length === 0) return null;
|
|
92
|
+
edges.sort((a, b) => {
|
|
93
|
+
const d = (incoming.get(a.from) || 0) - (incoming.get(b.from) || 0);
|
|
94
|
+
return d !== 0 ? d : a.to.localeCompare(b.to);
|
|
95
|
+
});
|
|
96
|
+
const drop = edges[0];
|
|
97
|
+
const hu = plan.hus.find((h) => h.id === drop.from);
|
|
98
|
+
hu.blocked_by = hu.blocked_by.filter((d) => d !== drop.to);
|
|
99
|
+
return drop;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function assignMissingShortIds(plan) {
|
|
103
|
+
const fixes = [];
|
|
104
|
+
const used = new Set();
|
|
105
|
+
let maxAuto = 0;
|
|
106
|
+
for (const hu of plan.hus) {
|
|
107
|
+
if (!hu.short_id) continue;
|
|
108
|
+
used.add(hu.short_id);
|
|
109
|
+
const m = String(hu.short_id).match(/^AUTOFIX-(\d+)$/);
|
|
110
|
+
if (m) maxAuto = Math.max(maxAuto, Number(m[1]));
|
|
111
|
+
}
|
|
112
|
+
for (const hu of plan.hus) {
|
|
113
|
+
if (hu.short_id) continue;
|
|
114
|
+
maxAuto += 1;
|
|
115
|
+
const candidate = `AUTOFIX-${String(maxAuto).padStart(3, "0")}`;
|
|
116
|
+
if (used.has(candidate)) continue;
|
|
117
|
+
hu.short_id = candidate;
|
|
118
|
+
used.add(candidate);
|
|
119
|
+
fixes.push({ kind: "short_id_assigned", huId: hu.id, assigned: candidate });
|
|
120
|
+
}
|
|
121
|
+
return fixes;
|
|
122
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-0397: auto-inject a [PREFLIGHT-000] HU at the top of every
|
|
3
|
+
* plan generated by `kj plan generate` so the sub-pipeline gates on a
|
|
4
|
+
* clean environment before burning tokens on functional HUs.
|
|
5
|
+
*
|
|
6
|
+
* Design points:
|
|
7
|
+
* - Pure shell tests — no Karajan-specific runtime, just commands
|
|
8
|
+
* that exit 0 when the environment is ready.
|
|
9
|
+
* - Stack-aware: Node, Python and Firebase signals come straight
|
|
10
|
+
* from detectProjectStack() + on-disk file probes. Adding a new
|
|
11
|
+
* stack is one entry in `composePreflightTests`.
|
|
12
|
+
* - Idempotent: re-running over a plan that already has a HU with
|
|
13
|
+
* id `PREFLIGHT-000` or a title containing 'preflight' / 'verificar
|
|
14
|
+
* entorno' returns the plan unchanged. Required because the same
|
|
15
|
+
* planner output may flow through structural-pass + plan-fixer
|
|
16
|
+
* before reaching savePlan.
|
|
17
|
+
* - All other HUs' `blocked_by` get the PREFLIGHT-000 id appended
|
|
18
|
+
* (unless they already have it, again idempotent). Their own
|
|
19
|
+
* dependencies are preserved.
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
|
|
24
|
+
import { detectProjectStack } from "../utils/stack-detect.js";
|
|
25
|
+
|
|
26
|
+
const PREFLIGHT_ID = "PREFLIGHT-000";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Compose stack-aware shell acceptance tests. Always includes a
|
|
30
|
+
* `git status --porcelain` clean-tree check; everything else is
|
|
31
|
+
* additive based on what the project actually uses.
|
|
32
|
+
*/
|
|
33
|
+
export function composePreflightTests(stack, projectDir = ".") {
|
|
34
|
+
const tests = [
|
|
35
|
+
{ type: "shell", content: "git status --porcelain | (! grep -q .)" },
|
|
36
|
+
];
|
|
37
|
+
const lang = stack?.language;
|
|
38
|
+
if (lang === "javascript" || lang === "typescript") {
|
|
39
|
+
tests.push({ type: "shell", content: "node --version | grep -qE '^v(2[0-9]|[3-9][0-9])'" });
|
|
40
|
+
if (existsSync(join(projectDir, "package.json"))) {
|
|
41
|
+
tests.push({ type: "shell", content: "npm install --no-audit --no-fund" });
|
|
42
|
+
tests.push({ type: "shell", content: "test -z \"$(npm run | grep -E '^ test$')\" || npm test" });
|
|
43
|
+
tests.push({ type: "shell", content: "test -z \"$(npm run | grep -E '^ lint$')\" || npm run lint" });
|
|
44
|
+
}
|
|
45
|
+
} else if (lang === "python") {
|
|
46
|
+
tests.push({ type: "shell", content: "python --version | grep -qE 'Python 3\\.(1[0-9]|[2-9][0-9])'" });
|
|
47
|
+
if (existsSync(join(projectDir, "requirements.txt"))) {
|
|
48
|
+
tests.push({ type: "shell", content: "pip install -r requirements.txt" });
|
|
49
|
+
} else if (existsSync(join(projectDir, "pyproject.toml"))) {
|
|
50
|
+
tests.push({ type: "shell", content: "test -f poetry.lock && poetry install || pip install -e ." });
|
|
51
|
+
}
|
|
52
|
+
tests.push({ type: "shell", content: "command -v pytest && pytest --collect-only || true" });
|
|
53
|
+
}
|
|
54
|
+
if (existsSync(join(projectDir, "firebase.json"))) {
|
|
55
|
+
tests.push({ type: "shell", content: "firebase projects:list" });
|
|
56
|
+
}
|
|
57
|
+
if (existsSync(join(projectDir, ".gcloudignore"))) {
|
|
58
|
+
tests.push({ type: "shell", content: "gcloud auth list --filter=status:ACTIVE --format='value(account)' | (! test -z \"$(cat)\")" });
|
|
59
|
+
}
|
|
60
|
+
return tests;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Return true when the plan already has a preflight-style HU (by id or
|
|
65
|
+
* title). Pattern-matches conservatively to avoid duplicate injection.
|
|
66
|
+
*/
|
|
67
|
+
export function hasPreflightHu(plan) {
|
|
68
|
+
if (!plan?.hus?.length) return false;
|
|
69
|
+
return plan.hus.some((hu) => {
|
|
70
|
+
if (hu.id === PREFLIGHT_ID) return true;
|
|
71
|
+
const title = (hu.title || hu.description || "").toLowerCase();
|
|
72
|
+
return title.includes("preflight-000") || title.includes("verificar entorno") || title.includes("preflight check");
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Prepend a [PREFLIGHT-000] HU to the plan and append its id to the
|
|
78
|
+
* `blocked_by` of every other HU. Returns the mutated plan reference
|
|
79
|
+
* for convenience; mutation is in place so callers don't need to
|
|
80
|
+
* reassign. No-op when the plan already has a preflight HU.
|
|
81
|
+
*/
|
|
82
|
+
export async function prependPreflightHu(plan, projectDir) {
|
|
83
|
+
if (!plan?.hus) return plan;
|
|
84
|
+
if (hasPreflightHu(plan)) return plan;
|
|
85
|
+
const stack = await detectProjectStack(projectDir);
|
|
86
|
+
const preflight = {
|
|
87
|
+
id: PREFLIGHT_ID,
|
|
88
|
+
title: "[PREFLIGHT-000] Verificar entorno de desarrollo listo",
|
|
89
|
+
description: "[PREFLIGHT-000] Block functional HUs until the project's environment is reproducible: deps installed, build/test/lint passing, git tree clean, cloud auth ready when applicable.",
|
|
90
|
+
task_type: "infra",
|
|
91
|
+
// KJC-BUG-0108: without an explicit status, `kj plan ready` rejects the
|
|
92
|
+
// whole plan with "invalid status undefined".
|
|
93
|
+
status: "pending",
|
|
94
|
+
blocked_by: [],
|
|
95
|
+
reuse: [],
|
|
96
|
+
acceptance_tests: composePreflightTests(stack, projectDir),
|
|
97
|
+
spec_section: null,
|
|
98
|
+
};
|
|
99
|
+
for (const hu of plan.hus) {
|
|
100
|
+
if (!Array.isArray(hu.blocked_by)) hu.blocked_by = [];
|
|
101
|
+
if (!hu.blocked_by.includes(PREFLIGHT_ID)) hu.blocked_by.push(PREFLIGHT_ID);
|
|
102
|
+
}
|
|
103
|
+
plan.hus.unshift(preflight);
|
|
104
|
+
return plan;
|
|
105
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect numbered/sectioned headings in a task description so the
|
|
3
|
+
* planner prompt can demand `spec_section` per HU instead of leaving
|
|
4
|
+
* it as a soft "preferred" hint that LLMs ignore.
|
|
5
|
+
*
|
|
6
|
+
* Recognises three common shapes (kept intentionally narrow — false
|
|
7
|
+
* positives turn the planner prompt into noise):
|
|
8
|
+
*
|
|
9
|
+
* ## 1. Overview → section "1", title "Overview"
|
|
10
|
+
* ### 2.1 add → section "2.1", title "add"
|
|
11
|
+
* ## §5 Initial Scope → section "5", title "Initial Scope"
|
|
12
|
+
*
|
|
13
|
+
* The matcher is anchored to markdown heading lines (`^#+`) so prose
|
|
14
|
+
* mentions of a number ("see version 4.2") never trigger detection.
|
|
15
|
+
*
|
|
16
|
+
* Returns `{ hasSpec, sections, sectionList }`:
|
|
17
|
+
* - `hasSpec` boolean — true when at least one heading matched.
|
|
18
|
+
* Prompt builders use this to switch language from
|
|
19
|
+
* "preferred" to "MUST".
|
|
20
|
+
* - `sections` string[] — bare section refs, dedup, in document
|
|
21
|
+
* order. e.g. ["1", "2", "2.1", "2.2", "3"].
|
|
22
|
+
* - `sectionList` Array<{section, title}> — full list with titles
|
|
23
|
+
* so the prompt can echo the checklist back at the
|
|
24
|
+
* LLM. Keeps order, dedup by section ref.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} text
|
|
27
|
+
* @returns {{ hasSpec: boolean, sections: string[], sectionList: Array<{section: string, title: string}> }}
|
|
28
|
+
*/
|
|
29
|
+
export function detectSpecSections(text) {
|
|
30
|
+
const empty = { hasSpec: false, sections: [], sectionList: [] };
|
|
31
|
+
if (!text || typeof text !== "string") return empty;
|
|
32
|
+
|
|
33
|
+
// Anchored to heading lines. Two regex shapes — numbered sections
|
|
34
|
+
// and §-prefixed sections — are both common in the wild.
|
|
35
|
+
//
|
|
36
|
+
// ^#{1,6}\s* ← markdown heading marker (any depth)
|
|
37
|
+
// §?\s* ← optional "§"
|
|
38
|
+
// (\d+(?:\.\d+){0,4}) ← section number, up to 5 levels deep
|
|
39
|
+
// [.)]?\s+ ← optional trailing "." or ")", then space
|
|
40
|
+
// (.+?)\s*$ ← title (lazy, trim trailing whitespace)
|
|
41
|
+
const headingRe = /^#{1,6}\s*§?\s*(\d+(?:\.\d+){0,4})[.)]?\s+(.+?)\s*$/;
|
|
42
|
+
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
const sectionList = [];
|
|
45
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
46
|
+
const line = rawLine.trimEnd();
|
|
47
|
+
const m = headingRe.exec(line);
|
|
48
|
+
if (!m) continue;
|
|
49
|
+
const section = m[1];
|
|
50
|
+
if (seen.has(section)) continue;
|
|
51
|
+
seen.add(section);
|
|
52
|
+
sectionList.push({ section, title: m[2].trim() });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (sectionList.length === 0) return empty;
|
|
56
|
+
return {
|
|
57
|
+
hasSpec: true,
|
|
58
|
+
sections: sectionList.map((s) => s.section),
|
|
59
|
+
sectionList,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests-first second pass for `kj plan` (v2.7.5).
|
|
3
|
+
*
|
|
4
|
+
* The first planner call asks for steps + acceptance_tests in one
|
|
5
|
+
* shot. Modern LLMs (Claude, Codex) reliably produce the steps but
|
|
6
|
+
* frequently skip the acceptance_tests sub-instruction when the
|
|
7
|
+
* surrounding spec is dense — a known weakness of long single-shot
|
|
8
|
+
* prompts. Pre-v2.7.5 we papered over this with a generic `npx
|
|
9
|
+
* vitest run` placeholder that wasn't actually a per-HU contract.
|
|
10
|
+
*
|
|
11
|
+
* This module runs a focused SECOND pass that asks the LLM ONLY to
|
|
12
|
+
* fill in tests for the HUs that came back without them. Because the
|
|
13
|
+
* prompt is shorter and has a single deliverable, the hit rate is
|
|
14
|
+
* effectively 100% on the HUs that slipped through the first time.
|
|
15
|
+
*
|
|
16
|
+
* Returns a map `{ huId → acceptance_tests[] }` so the caller can
|
|
17
|
+
* patch the plan in place. The patch always normalises through
|
|
18
|
+
* `plan-schema::normaliseAcceptanceTests`, so any malformed entries
|
|
19
|
+
* the LLM produces are silently dropped (better than corrupting the
|
|
20
|
+
* plan with junk).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { extractFirstJson } from "../utils/json-extract.js";
|
|
24
|
+
import { normaliseAcceptanceTests } from "./plan-schema.js";
|
|
25
|
+
import { withBrainRecovery } from "../brain/with-brain-recovery.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build the focused-pass prompt asking ONLY for tests for the given
|
|
29
|
+
* HUs. Exported for unit-testing the prompt shape without an LLM.
|
|
30
|
+
*
|
|
31
|
+
* @param {object} args
|
|
32
|
+
* @param {string} args.task
|
|
33
|
+
* @param {Array<{ id: string, title?: string, scope?: string }>} args.husNeedingTests
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
export function buildSynthesizerPrompt({ task, husNeedingTests, stack = null, testFramework = null }) {
|
|
37
|
+
const lines = [
|
|
38
|
+
"You previously generated an implementation plan for the task below, but some HUs were left without acceptance_tests.",
|
|
39
|
+
"Your ONLY job in this call is to fill that gap.",
|
|
40
|
+
"",
|
|
41
|
+
"Output MUST be a JSON object with this exact shape:",
|
|
42
|
+
"",
|
|
43
|
+
"```json",
|
|
44
|
+
"{",
|
|
45
|
+
' "tests": {',
|
|
46
|
+
' "<huId>": [',
|
|
47
|
+
' { "type": "gherkin", "content": "Given <ctx>\\nWhen <action>\\nThen <outcome>" },',
|
|
48
|
+
' { "type": "shell", "content": "<bash command that must exit 0>" }',
|
|
49
|
+
" ]",
|
|
50
|
+
" }",
|
|
51
|
+
"}",
|
|
52
|
+
"```",
|
|
53
|
+
"",
|
|
54
|
+
"Rules:",
|
|
55
|
+
"- The keys of `tests` are the HU ids EXACTLY as listed below.",
|
|
56
|
+
"- 2-4 tests per HU. At least one happy-path and one edge case.",
|
|
57
|
+
"- gherkin = behaviour spec; shell = concrete bash command exiting 0 on pass.",
|
|
58
|
+
"- Tests must be specific to the HU. Do NOT use generic fallbacks like `npx vitest run`.",
|
|
59
|
+
"- Tests must be writable BEFORE the HU is implemented (they should fail until done).",
|
|
60
|
+
"- Return ONLY the JSON — no prose, no markdown fences around it.",
|
|
61
|
+
"",
|
|
62
|
+
];
|
|
63
|
+
// Stack-aware hint: tell the LLM what test framework / language to
|
|
64
|
+
// emit shell commands for. Without this it defaulted to vitest /
|
|
65
|
+
// npm even on Python / Go / Rust projects (sesgo-de-stack audit).
|
|
66
|
+
if (stack || testFramework) {
|
|
67
|
+
lines.push("## Project Stack — generate shell tests for THIS stack, not generic JS");
|
|
68
|
+
if (stack?.language) lines.push(`- Language: **${stack.language}**`);
|
|
69
|
+
if (stack?.frameworks?.length) lines.push(`- Frameworks: ${stack.frameworks.join(", ")}`);
|
|
70
|
+
if (testFramework?.hasTests && testFramework?.framework) {
|
|
71
|
+
lines.push(`- Test framework already in use: **${testFramework.framework}** — use it for the shell commands.`);
|
|
72
|
+
} else if (stack?.language && stack.language !== "javascript" && stack.language !== "typescript") {
|
|
73
|
+
lines.push(`- Use the canonical test runner for ${stack.language} (pytest / go test / cargo test / rspec). Do NOT emit vitest, jest, or npm commands.`);
|
|
74
|
+
}
|
|
75
|
+
lines.push("");
|
|
76
|
+
}
|
|
77
|
+
lines.push("## Task", task, "", "## HUs that need acceptance_tests", "");
|
|
78
|
+
for (const hu of husNeedingTests) {
|
|
79
|
+
lines.push(`### ${hu.id}: ${hu.title || ""}`);
|
|
80
|
+
lines.push(hu.scope ? `Scope: ${hu.scope}` : "(no scope)");
|
|
81
|
+
lines.push("");
|
|
82
|
+
}
|
|
83
|
+
return lines.join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Run the focused second pass. Pass an `agent` with a `runTask`
|
|
88
|
+
* method (Claude / Codex / Gemini wrappers all expose it).
|
|
89
|
+
*
|
|
90
|
+
* @param {object} args
|
|
91
|
+
* @param {object} args.agent
|
|
92
|
+
* @param {string} args.task
|
|
93
|
+
* @param {Array<{ id: string, title?: string, scope?: string }>} args.husNeedingTests
|
|
94
|
+
* @param {Function} [args.onOutput]
|
|
95
|
+
* @param {number} [args.silenceTimeoutMs]
|
|
96
|
+
* @param {number} [args.timeoutMs]
|
|
97
|
+
* @returns {Promise<{ ok: boolean, tests?: Record<string, object[]>, error?: string }>}
|
|
98
|
+
*/
|
|
99
|
+
export async function synthesizeMissingTests({ agent, task, husNeedingTests, onOutput, silenceTimeoutMs, timeoutMs, emitter, eventBase, logger }) {
|
|
100
|
+
if (!Array.isArray(husNeedingTests) || husNeedingTests.length === 0) {
|
|
101
|
+
return { ok: true, tests: {} };
|
|
102
|
+
}
|
|
103
|
+
const prompt = buildSynthesizerPrompt({ task, husNeedingTests });
|
|
104
|
+
const runArgs = { prompt, role: "planner" };
|
|
105
|
+
if (onOutput) runArgs.onOutput = onOutput;
|
|
106
|
+
if (silenceTimeoutMs) runArgs.silenceTimeoutMs = silenceTimeoutMs;
|
|
107
|
+
if (timeoutMs) runArgs.timeoutMs = timeoutMs;
|
|
108
|
+
|
|
109
|
+
// KJC-TSK-0413: Brain Recovery wraps el agent call.
|
|
110
|
+
const result = await withBrainRecovery({
|
|
111
|
+
agent, taskArgs: runArgs, role: "tests-synthesizer",
|
|
112
|
+
emitter, eventBase, logger,
|
|
113
|
+
});
|
|
114
|
+
if (!result.ok) {
|
|
115
|
+
return { ok: false, error: result.error || `synthesizer agent failed (${result.recovery?.class || "unknown"})`, recovery: result.recovery, action: result.action };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const parsed = extractFirstJson(result.output || "");
|
|
119
|
+
const rawTests = parsed?.tests;
|
|
120
|
+
if (!rawTests || typeof rawTests !== "object") {
|
|
121
|
+
return { ok: false, error: "synthesizer returned no `tests` object" };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Normalise per HU so junk entries don't slip into the plan.
|
|
125
|
+
const tests = {};
|
|
126
|
+
for (const [huId, list] of Object.entries(rawTests)) {
|
|
127
|
+
const cleaned = normaliseAcceptanceTests(list);
|
|
128
|
+
if (cleaned.length > 0) tests[huId] = cleaned;
|
|
129
|
+
}
|
|
130
|
+
return { ok: true, tests };
|
|
131
|
+
}
|