@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,721 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sentinel-hooks — SEN-A (KJC-TSK-0713, epic KJC-PCS-0071 Karajan Sentinel).
|
|
3
|
+
* v3 had authority without intelligence; v4 intelligence without authority.
|
|
4
|
+
* The Sentinel separates them: a deterministic PROGRAM (zero LLM) supervises
|
|
5
|
+
* the agent through the harness's synchronous hooks — PostToolUse records
|
|
6
|
+
* method facts per session, Stop BLOCKS ending the turn while violations are
|
|
7
|
+
* open. Claude Code only: it is the one harness with synchronous blocking
|
|
8
|
+
* hooks, which is why the guaranteed level requires Claude as host (ADR).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { CARD_REF_RE } from "../review/card-first.js";
|
|
15
|
+
import { mergeClaudeHooks, writeHarnessScript } from "./harness-hooks.js";
|
|
16
|
+
|
|
17
|
+
/** The harness lives at the PROJECT root — resolve it even from a subdir. */
|
|
18
|
+
export function resolveSentinelRoot(dir = process.cwd()) {
|
|
19
|
+
try {
|
|
20
|
+
return (
|
|
21
|
+
execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || dir
|
|
22
|
+
);
|
|
23
|
+
} catch {
|
|
24
|
+
return dir;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const LIB_BODY = `// kj sentinel shared lib (KJC-TSK-0714) — managed by \`kj harden\`.
|
|
29
|
+
// Single source for every sentinel script: state, branch, classification,
|
|
30
|
+
// violations, and escape recording.
|
|
31
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
32
|
+
import { execSync } from "node:child_process";
|
|
33
|
+
import { dirname, join } from "node:path";
|
|
34
|
+
import { fileURLToPath } from "node:url";
|
|
35
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
export const STATE = join(here, "sentinel-state.json");
|
|
37
|
+
export const ROOT = join(here, "..", "..");
|
|
38
|
+
// INF-A (KJC-TSK-0758): infra IS source — terraform/k8s/shell/Docker edits
|
|
39
|
+
// engage the method like app code; *_test.* files (terratest) are tests.
|
|
40
|
+
export const TESTS = /(^|\\/)(tests?|__tests__|spec)\\/|\\.(test|spec)\\.[a-z]+$|_test\\.[a-z]+$/;
|
|
41
|
+
export const CODE = /\\.(m?[jt]sx?|c[jt]s|py|go|rs|java|rb|php|cs|swift|kt|astro|svelte|vue|c|h|cc|cpp|hpp|tf|hcl|ya?ml|sh)$|(^|\\/)(Dockerfile|Makefile|Vagrantfile)$/;
|
|
42
|
+
export const CARD = new RegExp(${JSON.stringify(CARD_REF_RE.source)}, "i");
|
|
43
|
+
export const BASE_BRANCHES = new Set(["main", "master"]);
|
|
44
|
+
export const load = () => { try { return JSON.parse(readFileSync(STATE, "utf8")); } catch { return {}; } };
|
|
45
|
+
export const save = (state) => writeFileSync(STATE, JSON.stringify(state, null, 2));
|
|
46
|
+
export const session = (state, sid) => {
|
|
47
|
+
state.sessions ||= {};
|
|
48
|
+
return (state.sessions[sid] ||= { edited_sources: [], edited_tests: [], escapes: [], errors: [], blocks: 0 });
|
|
49
|
+
};
|
|
50
|
+
export const branchOf = () => {
|
|
51
|
+
try { return execSync("git rev-parse --abbrev-ref HEAD", { cwd: ROOT, stdio: ["ignore", "pipe", "ignore"] }).toString().trim(); }
|
|
52
|
+
catch { return null; }
|
|
53
|
+
};
|
|
54
|
+
const _git = (args, cwd) => {
|
|
55
|
+
try { return execSync("git " + args, { cwd, stdio: ["ignore", "pipe", "ignore"] }).toString().trim(); } catch { return ""; }
|
|
56
|
+
};
|
|
57
|
+
let _home;
|
|
58
|
+
// MONO-0 (ADR 0002): lane identity = worktree toplevel; repo identity =
|
|
59
|
+
// git-common-dir. Same repo but another toplevel than OUR root => foreign
|
|
60
|
+
// lane (returns its toplevel); anything else => null.
|
|
61
|
+
export const foreignLane = (targetDir) => {
|
|
62
|
+
const top = _git("rev-parse --show-toplevel", targetDir);
|
|
63
|
+
if (!top) return null;
|
|
64
|
+
_home ||= { top: _git("rev-parse --show-toplevel", ROOT), common: _git("rev-parse --path-format=absolute --git-common-dir", ROOT) };
|
|
65
|
+
if (!_home.top || top === _home.top) return null;
|
|
66
|
+
const common = _git("rev-parse --path-format=absolute --git-common-dir", targetDir);
|
|
67
|
+
return common && common === _home.common ? top : null;
|
|
68
|
+
};
|
|
69
|
+
// KJC-TSK-0765 board-sync: cards merged in this session but not yet moved in
|
|
70
|
+
// the tracker. The method does not advance until the board is true.
|
|
71
|
+
export const pendingMoves = (s) => (s && Array.isArray(s.pending_moves) ? s.pending_moves : []);
|
|
72
|
+
export const pendingText = (p) => (p.card ? p.card : "card sin identificar") + " (PR #" + p.pr + ") mergeada sin mover en el tracker — muevela a To Validate / Fixed con sus commits (update_card del PG o kj hu move)";
|
|
73
|
+
export const violations = (s, branch) => {
|
|
74
|
+
const v = pendingMoves(s).map(pendingText);
|
|
75
|
+
if (!s || !(s.edited_sources || []).length) return v;
|
|
76
|
+
if (BASE_BRANCHES.has(branch)) v.push("Fuentes editadas en la rama base '" + branch + "' — crea una rama: git checkout -b feat/<CARD-ID>-descripcion");
|
|
77
|
+
else if (branch && !CARD.test(branch)) v.push("La rama '" + branch + "' no referencia ninguna card — usa feat/<CARD-ID>-descripcion (y una card VIVA en el board)");
|
|
78
|
+
if (!(s.edited_tests || []).length) v.push("Fuentes editadas sin tocar un solo test (" + s.edited_sources.join(", ") + ") — escribe o actualiza el test que prueba el cambio");
|
|
79
|
+
return v;
|
|
80
|
+
};
|
|
81
|
+
export const recordEscape = (sid, escape, tool) => {
|
|
82
|
+
const state = load();
|
|
83
|
+
const s = session(state, sid);
|
|
84
|
+
s.at = Date.now();
|
|
85
|
+
if (!s.escapes.includes(escape)) s.escapes.push(escape);
|
|
86
|
+
(state.escape_events ||= []).push({ escape, tool, sid, ts: Date.now() });
|
|
87
|
+
save(state);
|
|
88
|
+
};
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
const POST_BODY = `#!/usr/bin/env node
|
|
92
|
+
// kj sentinel state writer (KJC-TSK-0713) — managed by \`kj harden\`.
|
|
93
|
+
// Records deterministic method facts per session; never blocks, never fails
|
|
94
|
+
// a tool call (PostToolUse, always exit 0).
|
|
95
|
+
import { relative } from "node:path";
|
|
96
|
+
import { spawnSync } from "node:child_process";
|
|
97
|
+
import { CODE, TESTS, ROOT, CARD, branchOf, load, save, session } from "./sentinel-lib.mjs";
|
|
98
|
+
const ESCAPES = ["KJ_ALLOW_WRITE", "KJ_ALLOW_REWRITE", "KJ_ALLOW_NO_CARD", "KJ_ALLOW_NO_TESTS", "KJ_ALLOW_PII", "KJ_ALLOW_POLICY", "KJ_ALLOW_IDENTITY", "KJ_ALLOW_BOARD"];
|
|
99
|
+
let raw = "";
|
|
100
|
+
process.stdin.on("data", (d) => { raw += d; });
|
|
101
|
+
process.stdin.on("end", () => {
|
|
102
|
+
try {
|
|
103
|
+
const { session_id: sid = "default", tool_name: tool, tool_input: input = {}, tool_response: response = {} } = JSON.parse(raw);
|
|
104
|
+
// KJC-TSK-0765 board-sync: a merge that gh CONFIRMS leaves its card pending
|
|
105
|
+
// in the tracker (the PreToolUse gate then refuses to advance until it is
|
|
106
|
+
// moved). Recorded here, after the fact, so a failed merge never blocks.
|
|
107
|
+
const text = typeof response === "string" ? response : JSON.stringify(response);
|
|
108
|
+
// Closing states that count as "registered in the tracker" (PG statuses and
|
|
109
|
+
// HU Board moves). The pending entry is cleared by the REAL tool call that
|
|
110
|
+
// moved the card — never by a promise in the agent's prose.
|
|
111
|
+
const CLOSING = ["to validate", "to-validate", "fixed", "verified", "closed", "done", "done&validated", "validated"];
|
|
112
|
+
const clearPending = (cardId) => {
|
|
113
|
+
const state = load();
|
|
114
|
+
const s = session(state, sid);
|
|
115
|
+
const before = (s.pending_moves || []).length;
|
|
116
|
+
s.pending_moves = (s.pending_moves || []).filter((p) => p.card !== null && p.card !== cardId);
|
|
117
|
+
if (s.pending_moves.length !== before) save(state);
|
|
118
|
+
};
|
|
119
|
+
if (/__update_card$/.test(String(tool)) && /^mcp__/.test(String(tool))) {
|
|
120
|
+
// The MCP response reaches the hook as {content:[{text:"<json>"}]}: once
|
|
121
|
+
// stringified, the inner quotes are ESCAPED — match both forms (found live:
|
|
122
|
+
// the first real update_card did not clear its pending entry).
|
|
123
|
+
const cid = /\\\\?"cardId\\\\?"\\s*:\\s*\\\\?"([A-Za-z0-9-]+)\\\\?"/.exec(text);
|
|
124
|
+
const st = /\\\\?"status\\\\?"\\s*:\\s*\\\\?"([^"\\\\]+)\\\\?"/.exec(text);
|
|
125
|
+
if (cid && st && CLOSING.includes(st[1].toLowerCase())) clearPending(cid[1].toUpperCase());
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
if (tool === "Bash") {
|
|
129
|
+
const cmdText = String(input.command || "");
|
|
130
|
+
// gh prints "merged pull request #N" only on a TTY: under a tool call the
|
|
131
|
+
// success line is ABSENT (found live on the first dogfood merge). The
|
|
132
|
+
// authoritative signal is the PR state itself: one gh pr view per merge.
|
|
133
|
+
const mergeCmd = /\\bgh\\s+pr\\s+merge(\\s+(\\d+))?/.exec(cmdText);
|
|
134
|
+
const said = /merged pull request #(\\d+)/i.exec(text);
|
|
135
|
+
let mergedPr = said ? Number(said[1]) : null;
|
|
136
|
+
if (mergedPr === null && mergeCmd) {
|
|
137
|
+
const args = ["pr", "view", ...(mergeCmd[2] ? [mergeCmd[2]] : []), "--json", "number,state"];
|
|
138
|
+
const r = spawnSync("gh", args, { cwd: ROOT, encoding: "utf8" });
|
|
139
|
+
try { const v = JSON.parse(r.status === 0 ? r.stdout : "null"); if (v && v.state === "MERGED") mergedPr = Number(v.number); } catch { /* unknown = not confirmed */ }
|
|
140
|
+
}
|
|
141
|
+
if (mergedPr !== null) {
|
|
142
|
+
const state = load();
|
|
143
|
+
const s = session(state, sid);
|
|
144
|
+
const ref = CARD.exec(branchOf() || "");
|
|
145
|
+
(s.pending_moves ||= []).push({ card: ref ? ref[0].toUpperCase() : null, pr: mergedPr, at: Date.now() });
|
|
146
|
+
save(state);
|
|
147
|
+
}
|
|
148
|
+
const moved = /kj\\s+hu\\s+move\\s+([A-Za-z0-9-]+)\\s+([a-z&-]+)/i.exec(String(input.command || ""));
|
|
149
|
+
if (moved && CLOSING.includes(moved[2].toLowerCase()) && !/error|fail/i.test(text)) clearPending(moved[1].toUpperCase());
|
|
150
|
+
process.exit(0);
|
|
151
|
+
}
|
|
152
|
+
const file = input.file_path || input.notebook_path;
|
|
153
|
+
if (!["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(tool) || !file) process.exit(0);
|
|
154
|
+
const state = load();
|
|
155
|
+
const s = session(state, sid);
|
|
156
|
+
s.at = Date.now();
|
|
157
|
+
const rel = relative(ROOT, file).replaceAll("\\\\", "/");
|
|
158
|
+
const bucket = TESTS.test(rel) ? s.edited_tests : CODE.test(rel) ? s.edited_sources : null;
|
|
159
|
+
if (bucket && !bucket.includes(rel)) bucket.push(rel);
|
|
160
|
+
for (const e of ESCAPES)
|
|
161
|
+
if (process.env[e] === "1" && !s.escapes.includes(e)) {
|
|
162
|
+
s.escapes.push(e);
|
|
163
|
+
(state.escape_events ||= []).push({ escape: e, tool, sid, ts: Date.now() });
|
|
164
|
+
}
|
|
165
|
+
const ids = Object.keys(state.sessions);
|
|
166
|
+
if (ids.length > 5) delete state.sessions[ids.sort((a, b) => (state.sessions[a].at || 0) - (state.sessions[b].at || 0))[0]];
|
|
167
|
+
save(state);
|
|
168
|
+
} catch { /* fail open — the sentinel never breaks a tool call */ }
|
|
169
|
+
process.exit(0);
|
|
170
|
+
});
|
|
171
|
+
`;
|
|
172
|
+
|
|
173
|
+
const STOP_BODY = `#!/usr/bin/env node
|
|
174
|
+
// kj sentinel stop gate (KJC-TSK-0713) — managed by \`kj harden\`. Exit 2
|
|
175
|
+
// BLOCKS ending the turn while method violations are open (stderr lists each
|
|
176
|
+
// one with its remediation). Fails OPEN on corrupt state, git errors, or
|
|
177
|
+
// after 3 unresolved blocks — a sentinel bug never bricks the session — and
|
|
178
|
+
// the fail-open is recorded in the state. \`--status\` prints, never blocks.
|
|
179
|
+
import { spawnSync } from "node:child_process";
|
|
180
|
+
import { load, save, branchOf, violations, ROOT } from "./sentinel-lib.mjs";
|
|
181
|
+
if (process.argv.includes("--status")) {
|
|
182
|
+
const st = load();
|
|
183
|
+
const branch = branchOf();
|
|
184
|
+
const sessions = Object.entries(st.sessions || {});
|
|
185
|
+
if (!sessions.length) console.log("sentinel: sin actividad registrada en esta sesion");
|
|
186
|
+
for (const [sid, s] of sessions) {
|
|
187
|
+
console.log("session " + sid + ": sources=[" + (s.edited_sources || []).join(", ") + "] tests=[" + (s.edited_tests || []).join(", ") + "] escapes=[" + (s.escapes || []).join(", ") + "] blocks=" + (s.blocks || 0) + ((s.errors || []).length ? " errors=" + s.errors.length : ""));
|
|
188
|
+
for (const x of violations(s, branch)) console.log(" ROJO: " + x);
|
|
189
|
+
}
|
|
190
|
+
process.exit(0);
|
|
191
|
+
}
|
|
192
|
+
let raw = "";
|
|
193
|
+
process.stdin.on("data", (d) => { raw += d; });
|
|
194
|
+
process.stdin.on("end", () => {
|
|
195
|
+
try {
|
|
196
|
+
if (process.env.KJ_SENTINEL_OFF === "1") process.exit(0);
|
|
197
|
+
const { session_id: sid = "default" } = JSON.parse(raw);
|
|
198
|
+
const state = load();
|
|
199
|
+
// Tamper check runs regardless of session state: shell indirection leaves
|
|
200
|
+
// no edit-tool record, but it cannot survive \`kj sentinel verify\`, whose
|
|
201
|
+
// root of trust is the INSTALLED kj package (outside this project tree) —
|
|
202
|
+
// nothing under .karajan can vouch for itself. No kj on PATH → fail open.
|
|
203
|
+
const ver = spawnSync("kj", ["sentinel", "verify", "--json"], { cwd: ROOT, encoding: "utf8" });
|
|
204
|
+
let tampered = [];
|
|
205
|
+
if (!ver.error && ver.status !== null && ver.status !== 0) {
|
|
206
|
+
try { tampered = JSON.parse(ver.stdout).mismatched || ["unknown"]; } catch { tampered = ["unknown"]; }
|
|
207
|
+
}
|
|
208
|
+
if (tampered.length) {
|
|
209
|
+
state.tamper_blocks = (state.tamper_blocks || 0) + 1;
|
|
210
|
+
save(state);
|
|
211
|
+
if (state.tamper_blocks > 3) {
|
|
212
|
+
console.error("kj sentinel: fail-open tras 3 bloqueos por manipulacion sin restaurar — corre kj harden y avisa a tu usuario");
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
console.error("kj sentinel: scripts del supervisor modificados fuera de kj harden (" + tampered.join(", ") + ") — restaura con kj harden antes de terminar; si lo cambiaste tu (humano), reinstalar lo deja en verde.");
|
|
216
|
+
process.exit(2);
|
|
217
|
+
}
|
|
218
|
+
state.tamper_blocks = 0;
|
|
219
|
+
const s = state.sessions?.[sid];
|
|
220
|
+
if (!s) process.exit(0);
|
|
221
|
+
const v = violations(s, branchOf());
|
|
222
|
+
if (!v.length) {
|
|
223
|
+
s.blocks = 0;
|
|
224
|
+
save(state);
|
|
225
|
+
if ((s.escapes || []).length)
|
|
226
|
+
console.log(JSON.stringify({ systemMessage: "kj sentinel: esta sesion uso " + s.escapes.length + " escape(s): " + s.escapes.join(", ") + " — decision registrada; detalle en kj sentinel status." }));
|
|
227
|
+
process.exit(0);
|
|
228
|
+
}
|
|
229
|
+
s.blocks = (s.blocks || 0) + 1;
|
|
230
|
+
if (s.blocks > 3) {
|
|
231
|
+
(s.errors ||= []).push("fail-open: 3 bloqueos consecutivos sin resolver — el sentinel se aparta para no colgar la sesion");
|
|
232
|
+
save(state);
|
|
233
|
+
console.error("kj sentinel: fail-open tras 3 bloqueos sin resolver — revisa kj sentinel status con tu usuario");
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
236
|
+
save(state);
|
|
237
|
+
console.error("kj sentinel: el turno NO puede terminar con el metodo en rojo:\\n" + v.map((x) => "- " + x).join("\\n") + "\\nResuelve las violaciones (o pide a tu usuario el escape) y termina de nuevo. Estado: kj sentinel status");
|
|
238
|
+
process.exit(2);
|
|
239
|
+
} catch { /* fail open */ }
|
|
240
|
+
process.exit(0);
|
|
241
|
+
});
|
|
242
|
+
`;
|
|
243
|
+
|
|
244
|
+
const PRETOOL_BODY = `#!/usr/bin/env node
|
|
245
|
+
// kj sentinel pretooluse gate (KJC-TSK-0714) — managed by \`kj harden\`.
|
|
246
|
+
// Consults the session method state BEFORE the tool runs: the rule fires
|
|
247
|
+
// before the damage, not in the post-mortem. Exit 2 blocks (stderr says the
|
|
248
|
+
// remediation); read-only tools are never wired here; every honored escape
|
|
249
|
+
// is recorded as an auditable event. Fails OPEN on anything unexpected.
|
|
250
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
251
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
252
|
+
import { spawnSync } from "node:child_process";
|
|
253
|
+
import { homedir } from "node:os";
|
|
254
|
+
import { CODE, TESTS, ROOT, BASE_BRANCHES, CARD, branchOf, foreignLane, load, violations, recordEscape, pendingMoves, pendingText } from "./sentinel-lib.mjs";
|
|
255
|
+
const EDIT_TOOLS = ["Write", "Edit", "MultiEdit", "NotebookEdit"];
|
|
256
|
+
const PUBLISH = /\\bnpm\\s+publish\\b|\\bfirebase\\s+deploy\\b|\\bgh\\s+release\\s+create\\b/;
|
|
257
|
+
const PUSH = /\\bgit\\s+push\\b/;
|
|
258
|
+
const PROTECTED = /\\.claude\\/settings\\.json\\b|\\.karajan\\/(hooks|harness)\\//;
|
|
259
|
+
let raw = "";
|
|
260
|
+
process.stdin.on("data", (d) => { raw += d; });
|
|
261
|
+
process.stdin.on("end", () => {
|
|
262
|
+
try {
|
|
263
|
+
const { session_id: sid = "default", tool_name: tool, tool_input: input = {} } = JSON.parse(raw);
|
|
264
|
+
// Self-protection (KJC-TSK-0715) rules run BEFORE any escape, including
|
|
265
|
+
// KJ_SENTINEL_OFF: the sentinel is not dismantled from inside a session —
|
|
266
|
+
// only the human, editing outside it.
|
|
267
|
+
if (EDIT_TOOLS.includes(tool)) {
|
|
268
|
+
const target = input.file_path || input.notebook_path;
|
|
269
|
+
const relT = target ? relative(ROOT, String(target)).replaceAll("\\\\", "/") : "";
|
|
270
|
+
if (relT && PROTECTED.test(relT)) {
|
|
271
|
+
console.error("kj sentinel: ese fichero es parte del supervisor (" + relT + ") — solo el humano desmonta el sentinel, editalo fuera de la sesion.");
|
|
272
|
+
process.exit(2);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Any Bash that NAMES the supervisor's files is denied — a write-verb
|
|
276
|
+
// blocklist is bypassable (cp, dd, one-liners), and reading them is what
|
|
277
|
+
// the Read/Grep tools are for. Shell indirection (variables, globs,
|
|
278
|
+
// substitution) cannot be resolved from command text: that vector is
|
|
279
|
+
// caught by the Stop gate's checksum verification, which blocks the turn.
|
|
280
|
+
if (tool === "Bash" && PROTECTED.test(String(input.command || "").replaceAll("\\\\", "/"))) {
|
|
281
|
+
console.error("kj sentinel: los ficheros del supervisor no se tocan desde Bash — consultalos con la tool Read/Grep; solo el humano los modifica, fuera de la sesion.");
|
|
282
|
+
process.exit(2);
|
|
283
|
+
}
|
|
284
|
+
if (process.env.KJ_SENTINEL_OFF === "1") process.exit(0);
|
|
285
|
+
// KJC-BUG-0142: cada deny anuncia su escape como prefijo del comando
|
|
286
|
+
// (KJ_ALLOW_X=1 cmd), pero el hook corre con el env del HOST — el
|
|
287
|
+
// prefijo jamas llegaba a process.env y el escape era inalcanzable
|
|
288
|
+
// (deadlock real del gate de release: la landing solo se pone verde
|
|
289
|
+
// tras el deploy y el deploy estaba bloqueado por el mismo gate). Un
|
|
290
|
+
// escape es una excepcion CONSCIENTE y auditada, no un boundary de
|
|
291
|
+
// seguridad (security sigue sin escape y la autoprotecion corre antes),
|
|
292
|
+
// asi que el TEXTO del comando es fuente valida: solo asignaciones
|
|
293
|
+
// LITERALES al inicio (tokens VAR=VAL antes del primer verbo) y solo
|
|
294
|
+
// sobre un comando SIMPLE — con ;|& $() backtick o salto de linea el
|
|
295
|
+
// prefijo shell no alcanzaria a los comandos posteriores (catch de
|
|
296
|
+
// codex: "X=1 true && npm publish" escaparia sin serlo), asi que el
|
|
297
|
+
// escape por texto se rechaza y queda la via env de siempre.
|
|
298
|
+
const CMD_TEXT = tool === "Bash" ? String(input.command || "") : "";
|
|
299
|
+
const ESC_TAB = String.fromCharCode(9);
|
|
300
|
+
const ESC_NL = String.fromCharCode(10);
|
|
301
|
+
const ESC_BT = String.fromCharCode(96);
|
|
302
|
+
const escOn = (name) => {
|
|
303
|
+
if (process.env[name] === "1") return true;
|
|
304
|
+
let found = false;
|
|
305
|
+
let i = 0;
|
|
306
|
+
while (i < CMD_TEXT.length) {
|
|
307
|
+
while (i < CMD_TEXT.length && (CMD_TEXT[i] === " " || CMD_TEXT[i] === ESC_TAB)) i += 1;
|
|
308
|
+
const start = i;
|
|
309
|
+
while (i < CMD_TEXT.length && CMD_TEXT[i] !== " " && CMD_TEXT[i] !== ESC_TAB && CMD_TEXT[i] !== ESC_NL) i += 1;
|
|
310
|
+
const tok = CMD_TEXT.slice(start, i);
|
|
311
|
+
const eq = tok.indexOf("=");
|
|
312
|
+
if (eq <= 0 || !/^[A-Z][A-Z0-9_]*$/.test(tok.slice(0, eq))) break;
|
|
313
|
+
if (tok === name + "=1") found = true;
|
|
314
|
+
}
|
|
315
|
+
if (!found) return false;
|
|
316
|
+
for (const c of CMD_TEXT) {
|
|
317
|
+
if (c === ";" || c === "|" || c === "&" || c === ESC_NL || c === ESC_BT) return false;
|
|
318
|
+
if (c === "$" || c === "(" || c === ")") return false;
|
|
319
|
+
}
|
|
320
|
+
return true;
|
|
321
|
+
};
|
|
322
|
+
// IDN-B (KJC-TSK-0763, ADR 0005): identity lock — gh and mutating git never
|
|
323
|
+
// run under an account other than the one THIS CLONE declared (the incident:
|
|
324
|
+
// a gh comment without switch went out as the client account). Deny BEFORE
|
|
325
|
+
// the damage with the literal remedy; never auto-switch; undeclared = closed.
|
|
326
|
+
if (tool === "Bash" && CMD_TEXT.length > 0) {
|
|
327
|
+
const idFile = join(ROOT, ".karajan", "identity.local.yml");
|
|
328
|
+
const idLine = (key) => {
|
|
329
|
+
if (!existsSync(idFile)) return null;
|
|
330
|
+
const line = readFileSync(idFile, "utf8").split(ESC_NL).find((l) => l.startsWith(key + ":"));
|
|
331
|
+
const v = line ? line.slice(key.length + 1).trim() : "";
|
|
332
|
+
return v.length > 0 ? v : null;
|
|
333
|
+
};
|
|
334
|
+
const declaredGh = idLine("gh_user");
|
|
335
|
+
const declaredEmail = idLine("git_email");
|
|
336
|
+
const ghHostsPath = join(process.env.GH_CONFIG_DIR || join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "gh"), "hosts.yml");
|
|
337
|
+
const activeGh = () => {
|
|
338
|
+
if (!existsSync(ghHostsPath)) return null;
|
|
339
|
+
const lines = readFileSync(ghHostsPath, "utf8").split(ESC_NL);
|
|
340
|
+
const start = lines.findIndex((l) => l.startsWith("github.com:"));
|
|
341
|
+
if (start < 0) return null;
|
|
342
|
+
for (let k = start + 1; k < lines.length; k += 1) {
|
|
343
|
+
if (lines[k].length > 0 && lines[k][0] !== " " && lines[k][0] !== ESC_TAB) break;
|
|
344
|
+
const t = lines[k].trim();
|
|
345
|
+
if (t.startsWith("user:")) { const u = t.slice(5).trim(); return u.length > 0 ? u : null; }
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
};
|
|
349
|
+
const words = (seg) => seg.trim().split(" ").filter((w) => w.length > 0 && w !== ESC_TAB);
|
|
350
|
+
const headOf = (ws) => { let k = 0; while (k < ws.length && ws[k].indexOf("=") > 0 && ws[k].indexOf("=") < ws[k].length) k += 1; return ws.slice(k); };
|
|
351
|
+
// Wrapper shells hide the payload in a string (review catch): scan inside.
|
|
352
|
+
const WRAPPERS =["sh", "bash", "zsh", "dash", "eval", "env", "xargs", "sudo", "nohup", "time", "exec"];
|
|
353
|
+
let switchedTo = null;
|
|
354
|
+
const problems = [];
|
|
355
|
+
// $( and backticks are separators too (review catch): their payload is its own segment.
|
|
356
|
+
const SEPS = [";", "&&", "||", "|", "$(", String.fromCharCode(96), "(", ")"];
|
|
357
|
+
const bare = (w) => w.replaceAll(String.fromCharCode(39), "").replaceAll(String.fromCharCode(34), "");
|
|
358
|
+
const scan = (text, depth) => { if (depth > 3) return; for (const seg of SEPS.reduce((acc, s) => acc.flatMap((l) => l.split(s)), text.split(ESC_NL))) checkSeg(headOf(words(seg)), depth, words(seg)); };
|
|
359
|
+
const checkSeg = (ws, depth, raw) => {
|
|
360
|
+
if (ws.length === 0) return;
|
|
361
|
+
if (WRAPPERS.includes(ws[0])) { // wrapper args have arity (sudo -u bob): scan from the first gh/git/wrapper token (review catch)
|
|
362
|
+
const j = ws.findIndex((w, i) => i > 0 && ["gh", "git", ...WRAPPERS].includes(bare(w)));
|
|
363
|
+
if (j > 0) scan(ws.slice(j).map(bare).join(" "), depth + 1);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (ws[0] === "gh") {
|
|
367
|
+
if (ws[1] === "auth") {
|
|
368
|
+
if (ws[2] === "switch") { const u = ws.indexOf("--user"); if (u > 0 && ws[u + 1]) switchedTo = ws[u + 1]; }
|
|
369
|
+
return; // session commands ARE the remedy — never blocked
|
|
370
|
+
}
|
|
371
|
+
if (!declaredGh) { problems.push("gh without a declared identity for this clone — run: kj identity set"); return; }
|
|
372
|
+
const eff = switchedTo || activeGh();
|
|
373
|
+
if (eff !== declaredGh) problems.push("gh as '" + (eff || "no session") + "' but this clone is declared as '" + declaredGh + "' — prefix the command: gh auth switch --user " + declaredGh + " && ...");
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (ws[0] === "git") {
|
|
377
|
+
// Global options (-C, -c, --git-dir, --work-tree...) are NOT re-implemented: they
|
|
378
|
+
// are forwarded verbatim to git var, so git resolves identity exactly as the real
|
|
379
|
+
// command would (review catches: -C cumulative, --git-dir, --opt=value forms).
|
|
380
|
+
const TWO = ["-c", "-C", "--git-dir", "--work-tree", "--namespace", "--super-prefix", "--exec-path", "--config-env"];
|
|
381
|
+
let k = 1;
|
|
382
|
+
while (k < ws.length && ws[k].startsWith("-")) k += TWO.includes(ws[k]) && ws[k + 1] ? 2 : 1;
|
|
383
|
+
const globals = ws.slice(1, k).map(bare);
|
|
384
|
+
const sub = ws[k];
|
|
385
|
+
if (sub === "push") { // push authenticates with the gh session, not user.email (review catch)
|
|
386
|
+
if (!declaredGh) { problems.push("git push without a declared identity for this clone — run: kj identity set"); return; }
|
|
387
|
+
const eff = switchedTo || activeGh();
|
|
388
|
+
if (eff !== declaredGh) problems.push("git push with gh session '" + (eff || "no session") + "' but this clone is declared as '" + declaredGh + "' — prefix: gh auth switch --user " + declaredGh + " && ...");
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
// IDN-B2: anything that authors or rewrites commits carries an identity (review catch).
|
|
392
|
+
if (!["commit", "tag", "merge", "rebase", "cherry-pick", "am", "revert"].includes(sub)) return; // am: the COMMITTER is still this user
|
|
393
|
+
if (!declaredEmail) { problems.push("git " + sub + " without a declared identity for this clone — run: kj identity set"); return; }
|
|
394
|
+
// Let GIT resolve author and committer under the command's own environment
|
|
395
|
+
// (review catch: user.email, -c, GIT_*_EMAIL, EMAIL, GIT_CONFIG_*, includeIf are
|
|
396
|
+
// its business): git var is the single source of truth.
|
|
397
|
+
const assigns = raw.slice(0, raw.length - ws.length).filter((w) => w.indexOf("=") > 0);
|
|
398
|
+
const env = Object.assign({}, process.env);
|
|
399
|
+
for (const w of assigns) env[w.slice(0, w.indexOf("="))] = bare(w.slice(w.indexOf("=") + 1));
|
|
400
|
+
const ident = (v) => {
|
|
401
|
+
const r = spawnSync("git", [...globals, "var", v], { cwd: ROOT, encoding: "utf8", env });
|
|
402
|
+
const s = r.status === 0 ? String(r.stdout) : "";
|
|
403
|
+
const a = s.indexOf("<");
|
|
404
|
+
const b = s.indexOf(">");
|
|
405
|
+
return a >= 0 && b > a ? s.slice(a + 1, b) : null;
|
|
406
|
+
};
|
|
407
|
+
const ai = ws.findIndex((w) => w.startsWith("--author"));
|
|
408
|
+
const authorRaw = ai < 0 ? null : ws[ai].includes("=") ? ws[ai].slice(ws[ai].indexOf("=") + 1) : ws.slice(ai + 1).join(" ");
|
|
409
|
+
const authorMail = authorRaw === null ? null : bare(authorRaw.includes("<") ? authorRaw.slice(authorRaw.indexOf("<") + 1, authorRaw.indexOf(">")) : authorRaw.trim());
|
|
410
|
+
const mails = [authorMail ?? ident("GIT_AUTHOR_IDENT"), ident("GIT_COMMITTER_IDENT")]; // --author overrides the author (review catch); the committer is always git's
|
|
411
|
+
const offender = mails.find((m) => m !== null && m !== declaredEmail) ?? (mails[0] === null ? "none" : null);
|
|
412
|
+
if (offender !== null) problems.push("git " + sub + " as '" + offender + "' but this clone is declared as '" + declaredEmail + "' — use: git -c user.email=" + declaredEmail + " " + sub + " ... (or git config user.email " + declaredEmail + ")");
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
scan(CMD_TEXT, 0);
|
|
416
|
+
if (problems.length > 0) {
|
|
417
|
+
if (escOn("KJ_ALLOW_IDENTITY")) { recordEscape(sid, "KJ_ALLOW_IDENTITY", tool); }
|
|
418
|
+
else {
|
|
419
|
+
console.error("kj sentinel: identity lock (ADR 0005) —" + problems.map((p) => ESC_NL + "- " + p).join("") + ESC_NL + "(KJ_ALLOW_IDENTITY=1 = excepcion consciente, queda registrada)");
|
|
420
|
+
process.exit(2);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// MONO-0 (KJC-TSK-0737, ADR 0002): cada sesion muta solo SU worktree;
|
|
425
|
+
// otro carril del MISMO repo se deniega ANTES del dano. Lectura libre.
|
|
426
|
+
const laneOf = (p) => {
|
|
427
|
+
// Sube al primer ancestro EXISTENTE (un subdir aun no creado escapaba).
|
|
428
|
+
let d = String(p);
|
|
429
|
+
if (!(existsSync(d) && statSync(d).isDirectory())) d = dirname(d);
|
|
430
|
+
while (d && !existsSync(d)) {
|
|
431
|
+
const up = dirname(d);
|
|
432
|
+
if (up === d) return null;
|
|
433
|
+
d = up;
|
|
434
|
+
}
|
|
435
|
+
// Canonicaliza (reviewer catch: un symlink local hacia otro carril
|
|
436
|
+
// esquivaba la clasificacion por ruta literal).
|
|
437
|
+
try { d = realpathSync(d); } catch { /* se clasifica tal cual */ }
|
|
438
|
+
return d ? foreignLane(d) : null;
|
|
439
|
+
};
|
|
440
|
+
const laneDeny = (what, lane) => {
|
|
441
|
+
if (escOn("KJ_ALLOW_CROSS_LANE")) { recordEscape(sid, "KJ_ALLOW_CROSS_LANE", tool); return false; }
|
|
442
|
+
console.error("kj sentinel: " + what + " vive en otro carril (" + lane + ") de este repo — cada sesion muta solo SU worktree (MONO-0). Cruce deliberado: KJ_ALLOW_CROSS_LANE=1, queda registrado.");
|
|
443
|
+
return true;
|
|
444
|
+
};
|
|
445
|
+
if (EDIT_TOOLS.includes(tool)) {
|
|
446
|
+
const target = input.file_path || input.notebook_path;
|
|
447
|
+
const lane = target ? laneOf(target) : null;
|
|
448
|
+
if (lane && laneDeny(String(target), lane)) process.exit(2);
|
|
449
|
+
}
|
|
450
|
+
if (tool === "Bash") {
|
|
451
|
+
const cmd = String(input.command || "");
|
|
452
|
+
// Deny-unless-known-read-only: un allowlist de verbos mutadores era
|
|
453
|
+
// esquivable (dd, interpretes). Solo un comando SIMPLE de lectura (sin
|
|
454
|
+
// encadenar ni redirigir) puede nombrar otro carril; leer carriles es
|
|
455
|
+
// trabajo de las tools Read/Grep, como con los ficheros del supervisor.
|
|
456
|
+
// Solo LITERALES: expansión/sustitución en un comando "de lectura"
|
|
457
|
+
// puede ejecutar cualquier cosa (reviewer catch: grep foo $(rm ...)).
|
|
458
|
+
// (find fuera: -delete/-exec mutan — reviewer catch; sus tokens de
|
|
459
|
+
// carril los caza el escaner de abajo.)
|
|
460
|
+
const READONLY = /^[ \\t]*(grep|rg|cat|head|tail|less|ls|wc|diff|stat|file|du|tree|git (log|show|diff|status|blame))\\b[^;|&<>$\`(){}\\n\\r]*$/;
|
|
461
|
+
if (!READONLY.test(cmd)) {
|
|
462
|
+
// cd/pushd invalida TODO razonamiento textual de rutas posteriores
|
|
463
|
+
// (carrera de bypasses confirmada en review: destino bare, con $,
|
|
464
|
+
// relativas post-cd...): en un comando NO-read-only, cambiar de
|
|
465
|
+
// directorio se deniega conservadoramente — el remedio es no
|
|
466
|
+
// necesitarlo (git -C, npm --prefix, rutas absolutas).
|
|
467
|
+
if (/(^|[;&|(\\s])(cd|pushd)([ \\t]|$)/.test(cmd)) {
|
|
468
|
+
if (escOn("KJ_ALLOW_CROSS_LANE")) { recordEscape(sid, "KJ_ALLOW_CROSS_LANE", tool); }
|
|
469
|
+
else {
|
|
470
|
+
console.error("kj sentinel: cd/pushd en un comando mutador — las rutas posteriores no son verificables por el guard de carriles (MONO-0); usa git -C / npm --prefix / rutas ABSOLUTAS sin cd (o KJ_ALLOW_CROSS_LANE=1, queda registrado).");
|
|
471
|
+
process.exit(2);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// Absolute AND dot-relative tokens (../lane escapaba al primer scan —
|
|
475
|
+
// reviewer catch); relative se resuelve contra el cwd de la sesion.
|
|
476
|
+
// EVERY token (un tope seria un bypass); dedup acota el coste git.
|
|
477
|
+
// Fallo de solomon (seguridad no arbitrable) + carrera de bypasses:
|
|
478
|
+
// la expansion ($, backtick) solo se tolera en segmentos que son
|
|
479
|
+
// asignaciones puras o runners del toolchain propio (npm/pnpm/yarn/
|
|
480
|
+
// vitest/jest/kj/gh/echo/printf, git sin -C/--git-dir/--work-tree) —
|
|
481
|
+
// sus argumentos son datos. En herramientas genericas de fichero o
|
|
482
|
+
// interpretes el objetivo queda oculto al texto => deny. Los paths
|
|
483
|
+
// LITERALES (tambien dentro de sustituciones) los escanea el bucle
|
|
484
|
+
// de tokens de abajo; el residuo (expansion anidada en segmentos
|
|
485
|
+
// runner) queda documentado como en la regla de ficheros PROTECTED.
|
|
486
|
+
if (/>{1,2}[ \\t]*["']?[\\$\`]/.test(cmd)) {
|
|
487
|
+
if (escOn("KJ_ALLOW_CROSS_LANE")) { recordEscape(sid, "KJ_ALLOW_CROSS_LANE", tool); }
|
|
488
|
+
else {
|
|
489
|
+
console.error("kj sentinel: redireccion con destino tras variable/sustitucion — el guard de carriles no puede verificarlo (MONO-0); usa una ruta LITERAL (o KJ_ALLOW_CROSS_LANE=1, queda registrado).");
|
|
490
|
+
process.exit(2);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// Sustitucion de comandos ($(...) o backticks) EJECUTA su contenido
|
|
494
|
+
// en cualquier posicion, y una ruta entrecomillada con espacios es
|
|
495
|
+
// invisible al tokenizador: ambas => deny conservador (endpoint
|
|
496
|
+
// pedido por el reviewer; friccion asumida, el remedio es literal).
|
|
497
|
+
// KJC-BUG-0140: pares de comillas REALES, no regex sobre el texto —
|
|
498
|
+
// la comilla de CIERRE de un string + un redirect literal + la de
|
|
499
|
+
// apertura del siguiente creaban la ilusion de ruta con espacios y
|
|
500
|
+
// denegaban el flujo estandar commit -m "..." > log && echo "ok".
|
|
501
|
+
// Se recorre alternando estado (doctrina del parseCommand del motor):
|
|
502
|
+
// solo el CONTENIDO realmente entrecomillado con slash y espacio
|
|
503
|
+
// deniega — incluidas comillas a mitad de token (scp host:"/a b/x")
|
|
504
|
+
// y la comilla sin cerrar (no verificable).
|
|
505
|
+
// ...y por TOKEN completo, no por span: spans adyacentes concatenan
|
|
506
|
+
// ("dir with"" spaces/file" es UNA ruta) — catch de codex.
|
|
507
|
+
const quotedPathWithSpaces = (s) => {
|
|
508
|
+
// Un backslash pegado a comilla o blanco (comilla escapada,
|
|
509
|
+
// espacio escapado) altera donde CIERRA el string o esconde
|
|
510
|
+
// espacios: opaco => deny (doctrina del motor) — catch de codex.
|
|
511
|
+
// Caracteres construidos con fromCharCode: CERO escaping en este
|
|
512
|
+
// template — fuente y artefacto identicos, nada que malinterpretar
|
|
513
|
+
// (el doble-escaping confundio cuatro reviews; solomon desestimo
|
|
514
|
+
// la primera y las demas se volvieron ilegibles igual).
|
|
515
|
+
// El punto escapado de un sed y similares siguen libres.
|
|
516
|
+
const BS = String.fromCharCode(92);
|
|
517
|
+
const TAB = String.fromCharCode(9);
|
|
518
|
+
const NL = String.fromCharCode(10);
|
|
519
|
+
for (let i = s.indexOf(BS); i !== -1; i = s.indexOf(BS, i + 1)) {
|
|
520
|
+
const n = s[i + 1] || "";
|
|
521
|
+
if (n === '"' || n === "'" || n === " " || n === TAB) return true;
|
|
522
|
+
}
|
|
523
|
+
let q = null;
|
|
524
|
+
let tok = "";
|
|
525
|
+
let hadQuote = false;
|
|
526
|
+
const flush = () => {
|
|
527
|
+
const bad = hadQuote && tok.includes("/") && tok.includes(" ");
|
|
528
|
+
tok = "";
|
|
529
|
+
hadQuote = false;
|
|
530
|
+
return bad;
|
|
531
|
+
};
|
|
532
|
+
for (const ch of s) {
|
|
533
|
+
if (q) {
|
|
534
|
+
if (ch === q) q = null;
|
|
535
|
+
else tok += ch;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (ch === '"' || ch === "'") {
|
|
539
|
+
q = ch;
|
|
540
|
+
hadQuote = true;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (ch === " " || ch === TAB || ch === NL || ch === ";" || ch === "|" || ch === "&" || ch === "<" || ch === ">" || ch === "(" || ch === ")") {
|
|
544
|
+
if (flush()) return true;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
tok += ch;
|
|
548
|
+
}
|
|
549
|
+
if (q !== null) return true; // comilla sin cerrar: no verificable
|
|
550
|
+
return flush();
|
|
551
|
+
};
|
|
552
|
+
if (/\\$\\(|\`/.test(cmd) || quotedPathWithSpaces(cmd)) {
|
|
553
|
+
if (escOn("KJ_ALLOW_CROSS_LANE")) { recordEscape(sid, "KJ_ALLOW_CROSS_LANE", tool); }
|
|
554
|
+
else {
|
|
555
|
+
console.error("kj sentinel: sustitucion de comandos o ruta entrecomillada con espacios en un comando mutador — no verificable por el guard de carriles (MONO-0); usa valores/rutas LITERALES sin sustitucion (o KJ_ALLOW_CROSS_LANE=1, queda registrado).");
|
|
556
|
+
process.exit(2);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const SAFE_EXP_SEG = /^([A-Za-z_][A-Za-z0-9_]*=[^ \\t]*[ \\t]*)*((npm|pnpm|yarn|vitest|jest|kj|gh|echo|printf|true|test)\\b|git[ \\t](?![^\\n]*(-C[ \\t]|--git-dir|--work-tree)))[^;|&\\n]*$|^[A-Za-z_][A-Za-z0-9_]*=[^;|&\\n]*$/;
|
|
560
|
+
const segs = cmd.split(/&&|\\|\\||[;|\\n]/).map((s) => s.trim()).filter(Boolean);
|
|
561
|
+
if (!segs.every((s) => !/[\\$]/.test(s) || SAFE_EXP_SEG.test(s))) {
|
|
562
|
+
if (escOn("KJ_ALLOW_CROSS_LANE")) { recordEscape(sid, "KJ_ALLOW_CROSS_LANE", tool); }
|
|
563
|
+
else {
|
|
564
|
+
console.error("kj sentinel: expansion de shell en una herramienta generica de fichero/interprete — el objetivo no es verificable por el guard de carriles (MONO-0); usa rutas LITERALES o un runner del toolchain (o KJ_ALLOW_CROSS_LANE=1, queda registrado).");
|
|
565
|
+
process.exit(2);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// ...incluidas relativas A PELO (los carriles kj viven en
|
|
569
|
+
// .kj/worktrees/). Un falso token (s/a/b/) resuelve al arbol propio
|
|
570
|
+
// y foreignLane da null — solo deniega lo que CAE en otro carril.
|
|
571
|
+
const seen = new Set();
|
|
572
|
+
for (const m of cmd.matchAll(/(?:^|[\\s"'=;(><])((?:\\.\\.?\\/)[^\\s"'\`;|&)]*|\\/[^\\s"'\`;|&)]+|~\\/[^\\s"'\`;|&)]+|[A-Za-z0-9_.@-]+\\/[^\\s"'\`;|&)]+)/g)) {
|
|
573
|
+
const t = m[1].startsWith("~/") ? (process.env.HOME || "~") + m[1].slice(1) : m[1];
|
|
574
|
+
const abs = resolve(t);
|
|
575
|
+
if (seen.has(abs)) continue;
|
|
576
|
+
seen.add(abs);
|
|
577
|
+
const lane = laneOf(abs);
|
|
578
|
+
if (lane && laneDeny(m[1], lane)) process.exit(2);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
// KJC-TSK-0734 (PL-B): con .karajan/policy.yml presente, la evaluacion
|
|
583
|
+
// la hace el MOTOR via kj policy eval --strict (exit 2 = deny, contrato
|
|
584
|
+
// PL-A); los defaults del supervisor viven en el motor y el check
|
|
585
|
+
// PROTECTED inline de arriba queda como red fail-closed si kj falta.
|
|
586
|
+
// Fail CLOSED por defecto (catches de codex; doctrina KJC-BUG-0095: un
|
|
587
|
+
// gate no se cae en silencio): solo exit 0 permite. kj inejecutable =
|
|
588
|
+
// deny duro (el remedio es restaurar kj); cualquier otro fallo de
|
|
589
|
+
// evaluacion = deny con escape humano registrable — la sesion no queda
|
|
590
|
+
// presa de un typo de YAML, pero abrirla es decision del usuario.
|
|
591
|
+
if ((EDIT_TOOLS.includes(tool) || tool === "Bash") && existsSync(resolve(ROOT, ".karajan", "policy.yml"))) {
|
|
592
|
+
// PL-C: el rol que ACTÚA (KJ_POLICY_ROLE, sembrado por los runners de
|
|
593
|
+
// kj run en los subprocesos) — el anfitrión-brain evalúa como coder.
|
|
594
|
+
const actorRole = process.env.KJ_POLICY_ROLE || "coder";
|
|
595
|
+
const pres = spawnSync("kj", ["policy", "eval", "--strict", "--role", actorRole, "--tool", tool, "--input", JSON.stringify(input)], { cwd: ROOT, encoding: "utf8" });
|
|
596
|
+
if (pres.error || pres.status === null) {
|
|
597
|
+
console.error("kj sentinel: .karajan/policy.yml declara enforcement pero kj no es ejecutable — la policy no se puede evaluar; restaura kj en el PATH (npm i -g @karajan-family/code, o el nombre legacy karajan-code) o retira la policy conscientemente.");
|
|
598
|
+
process.exit(2);
|
|
599
|
+
}
|
|
600
|
+
if (pres.status === 2) {
|
|
601
|
+
let v = null;
|
|
602
|
+
try { v = JSON.parse(String(pres.stdout || "").trim().split("\\n").pop()); } catch { /* mensaje generico */ }
|
|
603
|
+
const secure = !!(v && v.class === "security");
|
|
604
|
+
if (!secure && escOn("KJ_ALLOW_POLICY")) { recordEscape(sid, "KJ_ALLOW_POLICY", tool); }
|
|
605
|
+
else {
|
|
606
|
+
console.error("kj sentinel: policy deny [" + ((v && v.rule_id) || "policy") + "] " + ((v && v.reason) || "la tool call viola la policy del proyecto") + (secure ? " [security — sin escape ni arbitraje]" : " (KJ_ALLOW_POLICY=1 = excepcion consciente, queda registrada; el commit exigira ademas KJ_POLICY_REASON)"));
|
|
607
|
+
process.exit(2);
|
|
608
|
+
}
|
|
609
|
+
} else if (pres.status !== 0) {
|
|
610
|
+
if (escOn("KJ_ALLOW_POLICY")) { recordEscape(sid, "KJ_ALLOW_POLICY", tool); }
|
|
611
|
+
else {
|
|
612
|
+
console.error("kj sentinel: kj policy eval fallo (exit " + pres.status + ") — la policy declarada no se pudo evaluar, deny por defecto; diagnostica con kj policy check y corrige .karajan/policy.yml fuera de la sesion (o KJ_ALLOW_POLICY=1 = excepcion consciente, queda registrada).");
|
|
613
|
+
process.exit(2);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (EDIT_TOOLS.includes(tool)) {
|
|
618
|
+
const file = input.file_path || input.notebook_path;
|
|
619
|
+
const rel = file ? relative(ROOT, String(file)).replaceAll("\\\\", "/") : "";
|
|
620
|
+
if (rel && CODE.test(rel) && !TESTS.test(rel)) {
|
|
621
|
+
const branch = branchOf();
|
|
622
|
+
const why = !branch ? null : BASE_BRANCHES.has(branch) ? "base" : !CARD.test(branch) ? "nocard" : null;
|
|
623
|
+
if (why) {
|
|
624
|
+
if (escOn("KJ_ALLOW_NO_CARD")) { recordEscape(sid, "KJ_ALLOW_NO_CARD", tool); process.exit(0); }
|
|
625
|
+
console.error(why === "base"
|
|
626
|
+
? "kj sentinel: no se editan fuentes en la rama base '" + branch + "' — crea la card (kj hu add) y la rama: git checkout -b feat/<CARD-ID>-descripcion. (KJ_ALLOW_NO_CARD=1 = excepcion consciente, queda registrada)"
|
|
627
|
+
: "kj sentinel: la rama '" + branch + "' no referencia ninguna card — crea/mueve la card a running (kj hu add | kj hu move) y usa una rama feat/<CARD-ID>-descripcion. (KJ_ALLOW_NO_CARD=1 = excepcion consciente, queda registrada)");
|
|
628
|
+
process.exit(2);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (tool === "Bash") {
|
|
633
|
+
const cmd = String(input.command || "");
|
|
634
|
+
// KJC-TSK-0765 board-sync: a merge leaves its card PENDING in the tracker
|
|
635
|
+
// and nothing advances (commit, new PR, another merge; push and the end of
|
|
636
|
+
// the turn go through violations()) until the card is moved. The board is
|
|
637
|
+
// true at all times or the method stops — never a rule in the agent's memory.
|
|
638
|
+
const MERGE = /\\bgh\\s+pr\\s+merge(\\s+(\\d+))?/;
|
|
639
|
+
const ADVANCE = /\\bgit\\s+commit\\b|\\bgit\\s+push\\b|\\bgh\\s+pr\\s+create\\b/; // push also falls under violations() — explicit here too (review catch)
|
|
640
|
+
const mergeM = cmd.match(MERGE);
|
|
641
|
+
const pend = pendingMoves(load().sessions?.[sid]);
|
|
642
|
+
if (pend.length > 0 && (mergeM || ADVANCE.test(cmd))) {
|
|
643
|
+
if (escOn("KJ_ALLOW_BOARD")) { recordEscape(sid, "KJ_ALLOW_BOARD", tool); }
|
|
644
|
+
else {
|
|
645
|
+
console.error("kj sentinel: board-sync — el metodo no avanza con cards mergeadas sin mover:\\n" + pend.map((p) => "- " + pendingText(p)).join("\\n") + "\\n(KJ_ALLOW_BOARD=1 = excepcion consciente, queda registrada)");
|
|
646
|
+
process.exit(2);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
// (the pending entry itself is recorded by the PostToolUse hook, only once
|
|
650
|
+
// gh confirms "merged pull request #N" — a failed merge never blocks)
|
|
651
|
+
if (PUBLISH.test(cmd)) {
|
|
652
|
+
if (escOn("KJ_ALLOW_RELEASE")) { recordEscape(sid, "KJ_ALLOW_RELEASE", tool); process.exit(0); }
|
|
653
|
+
const res = spawnSync("kj", ["release", "check", "--json"], { cwd: ROOT, encoding: "utf8" });
|
|
654
|
+
if (res.error || res.status === null) process.exit(0);
|
|
655
|
+
if (res.status !== 0) {
|
|
656
|
+
let items = "";
|
|
657
|
+
try { items = (JSON.parse(res.stdout).checks || []).filter((c) => !c.ok).map((c) => "\\n- " + c.name + ": " + c.detail).join(""); } catch { /* raw output */ }
|
|
658
|
+
console.error("kj sentinel: release check en ROJO — no se publica ni despliega hasta resolverlo:" + (items || "\\n- corre kj release check para el detalle") + "\\n(KJ_ALLOW_RELEASE=1 = excepcion consciente, queda registrada)");
|
|
659
|
+
process.exit(2);
|
|
660
|
+
}
|
|
661
|
+
} else if (PUSH.test(cmd)) {
|
|
662
|
+
const v = violations(load().sessions?.[sid], branchOf());
|
|
663
|
+
if (v.length) {
|
|
664
|
+
console.error("kj sentinel: git push con el metodo en rojo:\\n" + v.map((x) => "- " + x).join("\\n") + "\\nResuelve antes de empujar. Estado: kj sentinel status");
|
|
665
|
+
process.exit(2);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
} catch { /* fail open */ }
|
|
670
|
+
process.exit(0);
|
|
671
|
+
});
|
|
672
|
+
`;
|
|
673
|
+
|
|
674
|
+
const SCRIPT_BODIES = {
|
|
675
|
+
"sentinel-lib.mjs": LIB_BODY,
|
|
676
|
+
"posttooluse.mjs": POST_BODY,
|
|
677
|
+
"stop.mjs": STOP_BODY,
|
|
678
|
+
"pretooluse-sentinel.mjs": PRETOOL_BODY,
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Tamper detection (KJC-TSK-0715): compare the on-disk harness scripts with
|
|
683
|
+
* what THIS kj install would write. The root of trust is the installed kj
|
|
684
|
+
* package — outside the project tree, beyond the session's tool reach —
|
|
685
|
+
* because nothing under .karajan can vouch for itself.
|
|
686
|
+
*/
|
|
687
|
+
export function verifySentinelScripts({ projectDir } = {}) {
|
|
688
|
+
const dir = join(projectDir || resolveSentinelRoot(), ".karajan", "harness");
|
|
689
|
+
const mismatched = [];
|
|
690
|
+
for (const [name, body] of Object.entries(SCRIPT_BODIES)) {
|
|
691
|
+
try {
|
|
692
|
+
if (readFileSync(join(dir, name), "utf8") !== body) mismatched.push(name);
|
|
693
|
+
} catch {
|
|
694
|
+
mismatched.push(name);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return { ok: mismatched.length === 0, mismatched };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** Write the sentinel scripts (shared lib + state writer + gates) and wire them. */
|
|
701
|
+
export function installSentinelHooks({ projectDir = process.cwd(), logger = console } = {}) {
|
|
702
|
+
const [lib, post, stop, pre] = Object.entries(SCRIPT_BODIES).map(([name, body]) =>
|
|
703
|
+
writeHarnessScript(projectDir, name, body),
|
|
704
|
+
);
|
|
705
|
+
const { wired } = mergeClaudeHooks({
|
|
706
|
+
projectDir,
|
|
707
|
+
logger,
|
|
708
|
+
entries: [
|
|
709
|
+
{ event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit", script: "pretooluse-sentinel.mjs" },
|
|
710
|
+
{ event: "PreToolUse", matcher: "Bash", script: "pretooluse-sentinel.mjs" },
|
|
711
|
+
{ event: "PostToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit", script: "posttooluse.mjs" },
|
|
712
|
+
// KJC-TSK-0765 board-sync: the merge (Bash) and the tracker move (MCP
|
|
713
|
+
// update_card) are observed AFTER the fact — found live: with only the
|
|
714
|
+
// edit matchers wired, the board-sync recorder never ran in production.
|
|
715
|
+
{ event: "PostToolUse", matcher: "Bash", script: "posttooluse.mjs" },
|
|
716
|
+
{ event: "PostToolUse", matcher: "mcp__.*__update_card", script: "posttooluse.mjs" },
|
|
717
|
+
{ event: "Stop", script: "stop.mjs" },
|
|
718
|
+
],
|
|
719
|
+
});
|
|
720
|
+
return { scripts: [lib, post, stop, pre], wired };
|
|
721
|
+
}
|