karajan-code 4.70.3 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +6 -14
- package/packages/hu-board/package.json +1 -1
- package/packages/hu-board/public/app.js +4 -18
- package/packages/hu-board/public/index.html +5 -2
- package/packages/hu-board/public/styles.css +26 -0
- package/packages/hu-board/public/utils/api.js +0 -74
- package/packages/hu-board/public/utils/board-tour.js +62 -0
- package/packages/hu-board/public/utils/board-view.js +49 -144
- package/packages/hu-board/public/utils/command-launcher.js +8 -70
- package/packages/hu-board/public/utils/formatters.js +10 -0
- package/packages/hu-board/public/utils/governance-view.js +13 -1
- package/packages/hu-board/public/utils/hu-actions.js +17 -68
- package/packages/hu-board/public/utils/init-listeners.js +7 -8
- package/packages/hu-board/public/utils/log-panel.js +3 -16
- package/packages/hu-board/public/utils/maggle-mode.js +2 -12
- package/packages/hu-board/public/utils/modals.js +7 -0
- package/packages/hu-board/public/utils/project-picker-view.js +1 -1
- package/packages/hu-board/public/utils/server-push.js +3 -22
- package/packages/hu-board/public/utils/shared-board-notice.js +33 -0
- package/packages/hu-board/public/utils/story-detail-view.js +5 -19
- package/packages/hu-board/src/cleanup-zombies.js +0 -1
- package/packages/hu-board/src/command-runner.js +36 -25
- package/packages/hu-board/src/db-connection.js +282 -0
- package/packages/hu-board/src/db-paths.js +113 -0
- package/packages/hu-board/src/db-queries.js +188 -0
- package/packages/hu-board/src/db-tombstones.js +51 -0
- package/packages/hu-board/src/db.js +28 -619
- package/packages/hu-board/src/ephemeral-cleaner.js +2 -3
- package/packages/hu-board/src/hu-zombie-reaper.js +5 -1
- package/packages/hu-board/src/plan-mutations.js +3 -153
- package/packages/hu-board/src/plans-service.js +55 -0
- package/packages/hu-board/src/routes/api/commands.js +122 -0
- package/packages/hu-board/src/routes/api/plans.js +55 -0
- package/packages/hu-board/src/routes/api/projects.js +267 -0
- package/packages/hu-board/src/routes/api/prompts.js +135 -0
- package/packages/hu-board/src/routes/api/sessions.js +95 -0
- package/packages/hu-board/src/routes/api/stories.js +183 -0
- package/packages/hu-board/src/routes/api/system.js +241 -0
- package/packages/hu-board/src/routes/api/tombstones.js +38 -0
- package/packages/hu-board/src/routes/api.js +24 -1371
- package/packages/hu-board/src/routes/governance.js +23 -1
- package/packages/hu-board/src/routes/lanes.js +40 -0
- package/packages/hu-board/src/routes/rag.js +4 -3
- package/packages/hu-board/src/server.js +20 -34
- package/packages/hu-board/src/sync-files.js +318 -0
- package/packages/hu-board/src/sync-project.js +99 -0
- package/packages/hu-board/src/sync.js +22 -566
- package/packages/hu-board/src/zombie-reaper.js +1 -1
- package/scripts/dual-publish.mjs +19 -3
- package/scripts/postinstall.js +15 -2
- package/scripts/verify-pack-mcp.mjs +11 -7
- package/scripts/verify-pack.mjs +4 -4
- package/src/audit/audit-history.js +3 -2
- package/src/audit/dead-exports.js +68 -59
- package/src/audit/deterministic-summary.js +33 -13
- package/src/audit/npm-signatures.js +41 -0
- package/src/audit/sbom.js +35 -0
- package/src/checks/agent-token.js +112 -0
- package/src/checks/ai-surface.js +9 -7
- package/src/checks/config-files.js +8 -7
- package/src/checks/dir-setup.js +34 -1
- package/src/checks/native-build.js +21 -39
- package/src/checks/runner.js +128 -142
- package/src/cli/advanced-commands.js +6 -6
- package/src/cli/register-meta.js +42 -82
- package/src/cli/register-pipeline.js +8 -140
- package/src/cli/register-plan.js +9 -100
- package/src/cli/retired-commands.js +49 -0
- package/src/cli.js +18 -5
- package/src/commands/agent-run.js +40 -26
- package/src/commands/audit.js +127 -124
- package/src/commands/board.js +5 -9
- package/src/commands/check.js +29 -16
- package/src/commands/doctor.js +2 -0
- package/src/commands/env.js +176 -148
- package/src/commands/go.js +76 -55
- package/src/commands/harden.js +105 -86
- package/src/commands/hu-start.js +52 -0
- package/src/commands/hu.js +288 -221
- package/src/commands/identity.js +111 -78
- package/src/commands/init-atril.js +47 -0
- package/src/commands/init.js +11 -40
- package/src/commands/install-tools.js +70 -85
- package/src/commands/main-guard.js +78 -0
- package/src/commands/policy.js +226 -184
- package/src/commands/qmd.js +27 -21
- package/src/commands/rag.js +75 -48
- package/src/commands/review-gate.js +422 -339
- package/src/commands/start.js +4 -5
- package/src/commands/steward.js +96 -47
- package/src/commands/tournament.js +46 -37
- package/src/commands/worktree.js +94 -25
- package/src/config/loader.js +34 -17
- package/src/config/overrides.js +2 -1
- package/src/config/secrets.js +36 -0
- package/src/config.js +1 -0
- package/src/environment/adr.js +16 -2
- package/src/environment/board-access.js +11 -9
- package/src/git/karajan-label.js +20 -8
- package/src/guards/main-guard.js +54 -0
- package/src/harden/branch-protection.js +36 -3
- package/src/harden/hook-templates.js +16 -1
- package/src/harden/sentinel/posttooluse.mjs +18 -6
- package/src/harden/sentinel/pretooluse-sentinel.mjs +81 -35
- package/src/harden/sentinel/sentinel-lib.mjs +149 -23
- package/src/harden/sentinel/sentinel-shell.mjs +64 -0
- package/src/harden/sentinel/sessionstart.mjs +3 -2
- package/src/harden/sentinel/stop.mjs +5 -4
- package/src/harden/workflow-engine.js +12 -1
- package/src/harden/workflow-templates.js +1 -1
- package/src/hu/board-lookup.js +29 -0
- package/src/hu/gh-identity.js +23 -0
- package/src/hu/github-board.js +4 -2
- package/src/hu/lane-prune.js +15 -7
- package/src/hu/lane-setup.js +65 -0
- package/src/hu/lanes.js +66 -0
- package/src/hu/team-board-mirror.js +3 -2
- package/src/infrastructure/command-runner.js +1 -1
- package/src/infrastructure/file-system-service.js +1 -1
- package/src/mcp/handlers/direct-handlers.js +2 -292
- package/src/mcp/handlers/hu-handlers.js +44 -55
- package/src/mcp/handlers/management-handlers.js +2 -42
- package/src/mcp/server-handlers.js +16 -22
- package/src/mcp/tools.js +5 -200
- package/src/plan/lane-plans.js +54 -0
- package/src/plan/plan-store.js +7 -4
- package/src/policy/add.js +26 -12
- package/src/policy/adr-number-check.js +31 -0
- package/src/prompts/audit.js +299 -339
- package/src/prompts/card-context.js +10 -5
- package/src/prompts/reviewer.js +5 -2
- package/src/rag/auto-update.js +56 -43
- package/src/rag/bootstrap-index.js +28 -54
- package/src/rag/migrate.js +1 -1
- package/src/rag/onnx-fallback.js +2 -2
- package/src/rag/pid-lease.js +70 -0
- package/src/rag/project-store.js +2 -12
- package/src/review/card-first.js +57 -46
- package/src/review/dual-review.js +5 -4
- package/src/review/gate-gitignore.js +23 -4
- package/src/review/gate-hook.js +47 -0
- package/src/review/one-shot-review.js +51 -37
- package/src/review/publish-pr.js +18 -3
- package/src/review/repo-docs.js +73 -0
- package/src/review/repo-stack.js +59 -0
- package/src/review/reviewer-fallback.js +34 -3
- package/src/review/seal-commit.js +29 -0
- package/src/review/sonar-pregate.js +59 -44
- package/src/review/third-party-review.js +90 -16
- package/src/review/third-party-size.js +43 -0
- package/src/review/third-party-skills.js +54 -0
- package/src/review/verify-pr.js +2 -26
- package/src/roles/audit-role.js +106 -125
- package/src/skills/atril-skills.js +5 -4
- package/src/sonar/scanner.js +116 -101
- package/src/steward/invariants.js +5 -4
- package/src/steward/phantom-sweep.js +66 -0
- package/src/tournament/judge.js +60 -53
- package/src/utils/agent-home.js +33 -0
- package/src/utils/gh-account.js +37 -0
- package/src/utils/git.js +0 -13
- package/src/utils/home-migration.js +7 -1
- package/src/utils/main-checkout.js +53 -0
- package/src/utils/qmd-collection.js +3 -1
- package/src/utils/welcome.js +3 -3
- package/templates/{skills → atril/commands}/kj-architect.md +5 -0
- package/templates/{skills → atril/commands}/kj-audit.md +5 -0
- package/templates/{skills → atril/commands}/kj-board.md +5 -0
- package/templates/{skills → atril/commands}/kj-code.md +5 -0
- package/templates/{skills → atril/commands}/kj-csv-transform.md +5 -0
- package/templates/{skills → atril/commands}/kj-data-report.md +5 -0
- package/templates/{skills → atril/commands}/kj-discover.md +5 -0
- package/templates/{skills → atril/commands}/kj-rag-query.md +5 -0
- package/templates/{skills → atril/commands}/kj-review.md +5 -0
- package/templates/{skills → atril/commands}/kj-security.md +5 -0
- package/templates/{skills → atril/commands}/kj-sonar.md +5 -0
- package/templates/{skills → atril/commands}/kj-sql-analysis.md +5 -0
- package/templates/{skills → atril/commands}/kj-test.md +5 -0
- package/packages/hu-board/public/utils/run-launcher.js +0 -169
- package/packages/hu-board/src/run-tracker.js +0 -121
- package/src/activity-log.js +0 -59
- package/src/agents/host-agent.js +0 -38
- package/src/agents/model-registry.js +0 -142
- package/src/audit/plan-adherence.js +0 -224
- package/src/autonomy/arbiter-role.js +0 -71
- package/src/autonomy/arbiter.js +0 -22
- package/src/autonomy/decider.js +0 -18
- package/src/autonomy/decision-resolver.js +0 -58
- package/src/autonomy/outcome-report.js +0 -55
- package/src/autonomy/stage-ask.js +0 -30
- package/src/brain/decision-tracker.js +0 -125
- package/src/brain/decisor.js +0 -243
- package/src/brain/solomon-consult.js +0 -171
- package/src/brain/standby-scheduler.js +0 -8
- package/src/budget/comparison.js +0 -96
- package/src/canvas/parse-md.js +0 -238
- package/src/canvas/schema.js +0 -101
- package/src/canvas/to-flat.js +0 -85
- package/src/canvas/validate.js +0 -163
- package/src/ci/dispatch.js +0 -99
- package/src/ci/index.js +0 -3
- package/src/ci/pr-diff.js +0 -26
- package/src/cli/register-standby.js +0 -26
- package/src/commands/architect.js +0 -111
- package/src/commands/autorun.js +0 -49
- package/src/commands/researcher.js +0 -54
- package/src/commands/resume.js +0 -72
- package/src/commands/review.js +0 -89
- package/src/commands/run.js +0 -185
- package/src/commands/standby.js +0 -117
- package/src/commands/status.js +0 -36
- package/src/commands/triage.js +0 -78
- package/src/commands/undo.js +0 -62
- package/src/domains/domain-loader.js +0 -245
- package/src/domains/domain-registry.js +0 -140
- package/src/domains/domain-synthesizer.js +0 -129
- package/src/git/automation.js +0 -433
- package/src/git/coder-commit-guard.js +0 -40
- package/src/git/hu-automation.js +0 -243
- package/src/git/hu-snapshot.js +0 -10
- package/src/git/symlink-targets.js +0 -57
- package/src/golden/asserter.js +0 -92
- package/src/golden/loader.js +0 -52
- package/src/golden/runner.js +0 -92
- package/src/golden/schema.js +0 -41
- package/src/guards/duplicate-members.js +0 -86
- package/src/guards/intent-guard.js +0 -161
- package/src/guards/perf-guard.js +0 -126
- package/src/guards/policy-guard.js +0 -90
- package/src/guards/policy-resolver.js +0 -85
- package/src/guards/secret-redactor.js +0 -71
- package/src/hu/acceptance-runner.js +0 -138
- package/src/hu/lazy-planner.js +0 -109
- package/src/hu/parallel-executor.js +0 -94
- package/src/hu/routing-metrics.js +0 -17
- package/src/hu/split-validator.js +0 -143
- package/src/hu/splitting-detector.js +0 -158
- package/src/hu/splitting-generator.js +0 -175
- package/src/hu/store.js +0 -394
- package/src/infrastructure/mocks.js +0 -182
- package/src/mcp/handlers/run-handler.js +0 -318
- package/src/mcp/sovereignty-guard.js +0 -113
- package/src/mcp/tool-arg-normalizers.js +0 -16
- package/src/mutate/reviewer-signal.js +0 -76
- package/src/orchestrator/agent-fallback.js +0 -81
- package/src/orchestrator/brain-coordinator.js +0 -186
- package/src/orchestrator/ci-integration.js +0 -139
- package/src/orchestrator/config-init.js +0 -426
- package/src/orchestrator/direct-actions.js +0 -217
- package/src/orchestrator/drivers/error-recovery.js +0 -287
- package/src/orchestrator/drivers/init-context.js +0 -238
- package/src/orchestrator/drivers/iteration-loop.js +0 -382
- package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +0 -69
- package/src/orchestrator/drivers/iteration-phases/guards.js +0 -91
- package/src/orchestrator/drivers/iteration-phases/handle-approved.js +0 -29
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +0 -125
- package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +0 -62
- package/src/orchestrator/drivers/post-loop.js +0 -404
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +0 -137
- package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +0 -47
- package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +0 -84
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +0 -393
- package/src/orchestrator/drivers/pre-loop.js +0 -524
- package/src/orchestrator/drivers/run-hu-batch.js +0 -468
- package/src/orchestrator/feedback-enrichment.js +0 -140
- package/src/orchestrator/feedback-queue.js +0 -151
- package/src/orchestrator/flow-control.js +0 -170
- package/src/orchestrator/flow-runner.js +0 -365
- package/src/orchestrator/fs-leak-detector.js +0 -228
- package/src/orchestrator/hu-scheduler.js +0 -114
- package/src/orchestrator/hu-sub-pipeline.js +0 -642
- package/src/orchestrator/iteration-gate.js +0 -73
- package/src/orchestrator/iteration-stages.js +0 -9
- package/src/orchestrator/parallel-limiter.js +0 -74
- package/src/orchestrator/pipeline-context.js +0 -72
- package/src/orchestrator/post-loop-stages.js +0 -468
- package/src/orchestrator/pre-loop-stages.js +0 -11
- package/src/orchestrator/preflight-checks.js +0 -487
- package/src/orchestrator/repair/repair-detector.js +0 -117
- package/src/orchestrator/repair/repair-runner.js +0 -142
- package/src/orchestrator/reviewer-fallback.js +0 -45
- package/src/orchestrator/role-output-compressor.js +0 -189
- package/src/orchestrator/session-journal.js +0 -308
- package/src/orchestrator/solomon-escalation.js +0 -331
- package/src/orchestrator/solomon-rules.js +0 -212
- package/src/orchestrator/stages/acceptance-coverage-stage.js +0 -47
- package/src/orchestrator/stages/architect-stage.js +0 -172
- package/src/orchestrator/stages/coder-stage.js +0 -506
- package/src/orchestrator/stages/domain-curator-stage.js +0 -82
- package/src/orchestrator/stages/format-stage.js +0 -61
- package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +0 -127
- package/src/orchestrator/stages/hu-reviewer-stage.js +0 -419
- package/src/orchestrator/stages/perf-stage.js +0 -140
- package/src/orchestrator/stages/planner-stage.js +0 -85
- package/src/orchestrator/stages/rag-context-stage.js +0 -80
- package/src/orchestrator/stages/rag-preload-decisor.js +0 -58
- package/src/orchestrator/stages/research-stage.js +0 -69
- package/src/orchestrator/stages/reviewer-stage.js +0 -431
- package/src/orchestrator/stages/sonar-stage.js +0 -439
- package/src/orchestrator/stages/stage-classes.js +0 -84
- package/src/orchestrator/stages/stage-executor.js +0 -155
- package/src/orchestrator/stages/suite-green-stage.js +0 -62
- package/src/orchestrator/stages/tdd-discipline-stage.js +0 -56
- package/src/orchestrator/stages/tool-judge-stage.js +0 -53
- package/src/orchestrator/stages/triage-stage.js +0 -172
- package/src/orchestrator/standby.js +0 -71
- package/src/orchestrator/verification-gate.js +0 -183
- package/src/orchestrator.js +0 -22
- package/src/plan/adr-generator.js +0 -170
- package/src/plan/adr-loader.js +0 -60
- package/src/plan/plan-executor.js +0 -100
- package/src/plan/plan-validation.js +0 -4
- package/src/prompts/acceptance-coverage.js +0 -32
- package/src/prompts/arbiter.js +0 -38
- package/src/prompts/architect.js +0 -103
- package/src/prompts/hu-reviewer.js +0 -244
- package/src/prompts/tool-correctness-judge.js +0 -31
- package/src/prompts/triage.js +0 -57
- package/src/rag/embedder.js +0 -2
- package/src/rag/embedders/_cloud-base.js +0 -2
- package/src/rag/embedders/cohere.js +0 -2
- package/src/rag/embedders/mistral.js +0 -2
- package/src/rag/embedders/onnx.js +0 -2
- package/src/rag/embedders/openai.js +0 -2
- package/src/rag/embedders/voyage.js +0 -2
- package/src/rag/rerank.js +0 -4
- package/src/rag/where-parser.js +0 -4
- package/src/repeat-detector.js +0 -88
- package/src/review/diff-generator.js +0 -112
- package/src/review/git-stash-helper.js +0 -104
- package/src/review/schema.js +0 -31
- package/src/review/scope-filter.js +0 -152
- package/src/review/snapshot-diff.js +0 -122
- package/src/review/tdd-discipline.js +0 -90
- package/src/review/tdd-policy.js +0 -69
- package/src/roles/acceptance-coverage-role.js +0 -78
- package/src/roles/architect-role.js +0 -77
- package/src/roles/coder-role.js +0 -113
- package/src/roles/commiter-role.js +0 -113
- package/src/roles/domain-curator-role.js +0 -134
- package/src/roles/hu-reviewer-role.js +0 -64
- package/src/roles/impeccable-role.js +0 -85
- package/src/roles/index.js +0 -18
- package/src/roles/karajan-brain-role.js +0 -134
- package/src/roles/perf-role.js +0 -122
- package/src/roles/planner-role.js +0 -151
- package/src/roles/refactorer-role.js +0 -30
- package/src/roles/repairer-role.js +0 -181
- package/src/roles/researcher-role.js +0 -70
- package/src/roles/reviewer-role.js +0 -115
- package/src/roles/security-role.js +0 -73
- package/src/roles/solomon-role.js +0 -271
- package/src/roles/sonar-role.js +0 -72
- package/src/roles/tester-role.js +0 -235
- package/src/roles/tool-correctness-judge-role.js +0 -78
- package/src/roles/triage-role.js +0 -99
- package/src/session/journal/decisions-writer.js +0 -200
- package/src/session/journal/iteration-logger.js +0 -194
- package/src/session/journal/summary-writer.js +0 -320
- package/src/session/journal/tree-writer.js +0 -164
- package/src/session/journal/write-all.js +0 -89
- package/src/session-cleanup.js +0 -116
- package/src/skills/semantic-detector.js +0 -167
- package/src/skills/skill-detector.js +0 -385
- package/src/sonar/cloud-scanner.js +0 -78
- package/src/tracker-bootstrap.js +0 -44
- package/src/types/index.js +0 -38
- package/src/types/policy.js +0 -43
- package/src/types/solomon.js +0 -87
- package/src/utils/async-lock.js +0 -31
- package/src/utils/budget.js +0 -229
- package/src/utils/cwd-confirm.js +0 -39
- package/src/utils/display/event-handlers.js +0 -403
- package/src/utils/display/formatters.js +0 -132
- package/src/utils/display/header.js +0 -47
- package/src/utils/display/session-summary.js +0 -118
- package/src/utils/display/solomon.js +0 -42
- package/src/utils/pricing.js +0 -29
- package/src/utils/quota-simulator.js +0 -56
- package/src/utils/role-assigner.js +0 -137
- package/src/utils/rtk-wrapper.js +0 -87
- package/src/utils/run-registry.js +0 -9
- package/src/utils/status-dashboard.js +0 -254
- package/src/utils/tool-call-extractor.js +0 -56
- package/src/webperf/devtools-detect.js +0 -112
- package/src/webperf/webperf-role-integration.js +0 -66
- package/templates/skills/kj-run.md +0 -73
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-1234 (ADR 0032): the project's kj.config.yml travels with the repo, so it must never
|
|
3
|
+
* hold a secret. It is judged by its parsed YAML (escaped keys, flow mappings, anchors and merge
|
|
4
|
+
* keys all resolved), never by its raw text. The parse is strict: a duplicated key fails, since a
|
|
5
|
+
* second empty value could hide a secret written before it.
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import yaml from "js-yaml";
|
|
9
|
+
|
|
10
|
+
export const SECRET_PATHS = [["sonarqube", "token"], ["sonarqube", "admin_password"], ["sonarcloud", "token"]];
|
|
11
|
+
|
|
12
|
+
/** @returns {string[]} the secret paths that hold a value (empty, null and absent are fine) */
|
|
13
|
+
export function configSecrets(text) {
|
|
14
|
+
const doc = yaml.load(text) ?? {};
|
|
15
|
+
return SECRET_PATHS
|
|
16
|
+
.filter(([section, key]) => {
|
|
17
|
+
const value = doc?.[section]?.[key];
|
|
18
|
+
return value !== undefined && value !== null && value !== "";
|
|
19
|
+
})
|
|
20
|
+
.map((path) => path.join("."));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** `kj config --check-secrets <file|->`: exit 1 naming each secret, or when the file cannot be read. */
|
|
24
|
+
export function checkSecretsCommand(file, { logger = console } = {}) {
|
|
25
|
+
let found;
|
|
26
|
+
try {
|
|
27
|
+
found = configSecrets(fs.readFileSync(file === "-" ? 0 : file, "utf8"));
|
|
28
|
+
} catch (err) {
|
|
29
|
+
logger.error(`kj: cannot check ${file} for secrets (${err.message})`);
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (found.length === 0) return;
|
|
34
|
+
logger.error(`kj: ${file} holds a secret (${found.join(", ")}): move it to .karajan/kj.config.local.yml or ~/.karajan`);
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
}
|
package/src/config.js
CHANGED
package/src/environment/adr.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import fs from "node:fs/promises";
|
|
8
8
|
import path from "node:path";
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
import { promisify } from "node:util";
|
|
9
11
|
|
|
10
12
|
const ADR_DIR = path.join(".karajan", "adrs");
|
|
11
13
|
// KJC-BUG-0272: whoever records a decision proposes it. Accepting is the user's:
|
|
@@ -28,12 +30,24 @@ export async function listAdrs(projectDir) {
|
|
|
28
30
|
return adrs;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
const realGit = (cwd) => async (args) => (await promisify(execFile)("git", args, { cwd })).stdout;
|
|
34
|
+
|
|
35
|
+
// HU-2792: numbers that open branches hold, read from the local origin/* refs
|
|
36
|
+
// (no network). Not a git repo, or no remotes: nothing taken, as before.
|
|
37
|
+
async function remoteAdrNumbers(run) {
|
|
38
|
+
try {
|
|
39
|
+
const refs = (await run(["for-each-ref", "--format=%(refname)", "refs/remotes/origin"])).split("\n").filter(Boolean);
|
|
40
|
+
const trees = await Promise.all(refs.map((ref) => run(["ls-tree", "--name-only", ref, `${ADR_DIR}/`]).catch(() => "")));
|
|
41
|
+
return trees.flatMap((t) => t.split("\n")).map((f) => /\/(\d{4})-[^/]*\.md$/.exec(f)?.[1]).filter(Boolean).map(Number);
|
|
42
|
+
} catch { return []; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function addAdr(projectDir, { title, decision, context = "", consequences = "" }, run = realGit(projectDir)) {
|
|
32
46
|
if (!title?.trim() || !decision?.trim()) {
|
|
33
47
|
throw new Error("kj adr add requires a title and --decision");
|
|
34
48
|
}
|
|
35
49
|
const existing = await listAdrs(projectDir);
|
|
36
|
-
const number = (existing.
|
|
50
|
+
const number = Math.max(0, ...existing.map((a) => a.number), ...await remoteAdrNumbers(run)) + 1;
|
|
37
51
|
const file = path.join(ADR_DIR, `${String(number).padStart(4, "0")}-${slugify(title)}.md`);
|
|
38
52
|
const body = [
|
|
39
53
|
`# ${title}`, "",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { existsSync, readFileSync } from "node:fs";
|
|
15
15
|
import os from "node:os";
|
|
16
16
|
import path from "node:path";
|
|
17
|
+
import { claudeConfigFile, codexHome } from "../utils/agent-home.js";
|
|
17
18
|
|
|
18
19
|
const TOKEN_CONVENTIONS = {
|
|
19
20
|
linear: ["LINEAR_API_KEY"],
|
|
@@ -23,18 +24,19 @@ const TOKEN_CONVENTIONS = {
|
|
|
23
24
|
asana: ["ASANA_ACCESS_TOKEN"],
|
|
24
25
|
};
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
// KJC-TSK-1344: claude and codex configs of the account CLAUDE_CONFIG_DIR / CODEX_HOME select.
|
|
28
|
+
function mcpConfigPaths(projectDir, home, env = process.env) {
|
|
27
29
|
return [
|
|
28
30
|
path.join(projectDir, ".mcp.json"),
|
|
29
|
-
|
|
30
|
-
path.join(home,
|
|
31
|
+
claudeConfigFile({ home, env }),
|
|
32
|
+
path.join(codexHome({ home, env }), "config.toml"),
|
|
31
33
|
path.join(home, ".gemini", "settings.json"),
|
|
32
34
|
];
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
function mcpConfigsMention(slug, projectDir, home) {
|
|
37
|
+
function mcpConfigsMention(slug, projectDir, home, env) {
|
|
36
38
|
const needle = slug.toLowerCase();
|
|
37
|
-
for (const p of mcpConfigPaths(projectDir, home)) {
|
|
39
|
+
for (const p of mcpConfigPaths(projectDir, home, env)) {
|
|
38
40
|
try {
|
|
39
41
|
if (existsSync(p) && readFileSync(p, "utf8").toLowerCase().includes(needle)) return p;
|
|
40
42
|
} catch { /* unreadable config — keep looking */ }
|
|
@@ -50,13 +52,13 @@ function mcpConfigsMention(slug, projectDir, home) {
|
|
|
50
52
|
*/
|
|
51
53
|
export function detectAvailableBoards({ projectDir = process.cwd(), env = process.env, home = os.homedir() } = {}) {
|
|
52
54
|
const boards = [{ value: "hu-board", label: "HU Board (bundled, zero setup)" }];
|
|
53
|
-
if (mcpConfigsMention("planning-game", projectDir, home)) {
|
|
55
|
+
if (mcpConfigsMention("planning-game", projectDir, home, env)) {
|
|
54
56
|
boards.push({ value: "planning-game", label: "Planning Game (MCP detected)" });
|
|
55
57
|
}
|
|
56
58
|
for (const [slug, envs] of Object.entries(TOKEN_CONVENTIONS)) {
|
|
57
59
|
const token = envs.find((k) => env[k]);
|
|
58
60
|
if (token) boards.push({ value: `external:${slug}`, label: `${slug} (token ${token})` });
|
|
59
|
-
else if (mcpConfigsMention(slug, projectDir, home)) boards.push({ value: `external:${slug}`, label: `${slug} (MCP detected)` });
|
|
61
|
+
else if (mcpConfigsMention(slug, projectDir, home, env)) boards.push({ value: `external:${slug}`, label: `${slug} (MCP detected)` });
|
|
60
62
|
}
|
|
61
63
|
return boards;
|
|
62
64
|
}
|
|
@@ -65,7 +67,7 @@ export function verifyBoardAccess({ config = {}, projectDir = process.cwd(), env
|
|
|
65
67
|
const backend = config.state_backend || "hu-board";
|
|
66
68
|
|
|
67
69
|
if (backend === "planning-game") {
|
|
68
|
-
const hit = mcpConfigsMention("planning-game", projectDir, home);
|
|
70
|
+
const hit = mcpConfigsMention("planning-game", projectDir, home, env);
|
|
69
71
|
if (hit) return { ok: true, backend, via: `planning-game MCP (${hit})` };
|
|
70
72
|
return {
|
|
71
73
|
ok: false, backend,
|
|
@@ -77,7 +79,7 @@ export function verifyBoardAccess({ config = {}, projectDir = process.cwd(), env
|
|
|
77
79
|
const name = String(config.board?.name || "").trim();
|
|
78
80
|
if (!name) return { ok: false, backend, needed: ["declare board.name in kj.config.yml (e.g. Linear, Trello, Jira, GitHub Issues)"] };
|
|
79
81
|
const slug = name.toLowerCase().split(/\s+/)[0];
|
|
80
|
-
const hit = mcpConfigsMention(slug, projectDir, home);
|
|
82
|
+
const hit = mcpConfigsMention(slug, projectDir, home, env);
|
|
81
83
|
if (hit) return { ok: true, backend, via: `${name} MCP detected (${hit})` };
|
|
82
84
|
const tokenEnvs = config.board?.token_env ? [config.board.token_env] : (TOKEN_CONVENTIONS[slug] || []);
|
|
83
85
|
const found = tokenEnvs.find((k) => env[k]);
|
package/src/git/karajan-label.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* KJC-TSK-1123 (#2315): every PR Karajan
|
|
2
|
+
* KJC-TSK-1123 (#2315): every PR Karajan governs carries the «Karajan» label, so
|
|
3
3
|
* a team can measure how much of its change goes through the governor. It means
|
|
4
4
|
* "went through Karajan", never "written by an AI": Karajan governs AIs, and the
|
|
5
5
|
* PR may be a person's work it reviewed. The signal lives only in the PR's
|
|
6
6
|
* metadata, never in commits or the PR body. Labelling never costs the PR: a
|
|
7
|
-
* failure is logged and the work goes on.
|
|
7
|
+
* failure is logged and the work goes on. KJC-BUG-0461: since 5.0 (no kj run opening PRs)
|
|
8
|
+
* kj review --publish-pr adds it, the step of the method that runs on every PR.
|
|
8
9
|
*/
|
|
9
10
|
import { runCommand } from "../utils/process.js";
|
|
10
11
|
|
|
@@ -12,20 +13,31 @@ export const KARAJAN_LABEL = "Karajan";
|
|
|
12
13
|
const LABEL_COLOR = "1f6feb";
|
|
13
14
|
const LABEL_DESCRIPTION = "Went through Karajan (governed and reviewed); not a claim that an AI wrote it";
|
|
14
15
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
16
|
+
/** The PR a URL or a bare number names: { repo: "owner/repo" or null for the repo gh runs in, number }. */
|
|
17
|
+
function prOf(ref) {
|
|
18
|
+
const url = /^https:\/\/[^/]+\/([^/]+\/[^/]+)\/pull\/(\d+)/.exec(String(ref ?? ""));
|
|
19
|
+
if (url) return { repo: url[1], number: url[2] };
|
|
20
|
+
return /^\d+$/.test(String(ref ?? "")) ? { repo: null, number: String(ref) } : null;
|
|
21
|
+
}
|
|
17
22
|
|
|
18
23
|
/**
|
|
19
24
|
* Add the label to the PR, creating it first when the repo lacks it.
|
|
25
|
+
* @param {{url: string|number}} args a PR URL, or its number in the repo gh runs in
|
|
20
26
|
* @returns {Promise<boolean>} true when the PR carries the label.
|
|
21
27
|
*/
|
|
22
28
|
export async function labelKarajanPr({ url, cwd = null, run = runCommand, warn = (msg) => process.stderr.write(`${msg}\n`) }) {
|
|
23
29
|
const opts = cwd ? { cwd } : {};
|
|
24
|
-
const
|
|
30
|
+
const pr = prOf(url);
|
|
31
|
+
if (!pr) {
|
|
32
|
+
warn(`kj: could not add the Karajan label to ${url}: not a pull request URL or number`);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
25
35
|
try {
|
|
26
|
-
// Fails when the label exists; the
|
|
27
|
-
await run("gh", ["label", "create", KARAJAN_LABEL, "--color", LABEL_COLOR, "--description", LABEL_DESCRIPTION, ...(repo ? ["--repo", repo] : [])], opts);
|
|
28
|
-
|
|
36
|
+
// Fails when the label exists; the call below says whether the PR got it.
|
|
37
|
+
await run("gh", ["label", "create", KARAJAN_LABEL, "--color", LABEL_COLOR, "--description", LABEL_DESCRIPTION, ...(pr.repo ? ["--repo", pr.repo] : [])], opts);
|
|
38
|
+
// KJC-BUG-0462: REST. `gh pr edit --add-label` asks GraphQL for the PR's classic project
|
|
39
|
+
// cards and fails since GitHub sunset Projects (classic), so no PR got the label.
|
|
40
|
+
const res = await run("gh", ["api", "-X", "POST", `repos/${pr.repo ?? "{owner}/{repo}"}/issues/${pr.number}/labels`, "-f", `labels[]=${KARAJAN_LABEL}`], opts);
|
|
29
41
|
if (res.exitCode === 0) return true;
|
|
30
42
|
warn(`kj: could not add the Karajan label to ${url}: ${(res.stderr || res.stdout || "").trim()}`);
|
|
31
43
|
} catch (err) {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* main-guard (KJC-TSK-1368, ADR 0037 layer 2): after a push to the protected branch, what
|
|
3
|
+
* reached it without its required checks, and whether the protection still enforces them on
|
|
4
|
+
* admins. It cannot prevent an abuse by someone holding the maintainer's credentials; it makes
|
|
5
|
+
* it visible. GitHub is reached through an injected api, so the logic runs without network.
|
|
6
|
+
*
|
|
7
|
+
* api.pullsOf(sha) → pull requests associated with a commit (GitHub's commits/{sha}/pulls)
|
|
8
|
+
* api.checksOf(sha) → [{ name, conclusion }] for a commit (check runs and statuses)
|
|
9
|
+
* api.protection() → the branch protection, or null when the branch is not protected
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// GitHub counts these as passing for a required check.
|
|
13
|
+
const PASSING = new Set(["success", "neutral", "skipped"]);
|
|
14
|
+
|
|
15
|
+
/** The pull request that merged this commit into the branch, if any. */
|
|
16
|
+
function mergedInto(pulls, branch) {
|
|
17
|
+
return pulls.find((p) => p.merged_at && p.base?.ref === branch) ?? null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function auditCommit(sha, { branch, requiredChecks, api }) {
|
|
21
|
+
const pull = mergedInto(await api.pullsOf(sha), branch);
|
|
22
|
+
if (!pull) return { sha, kind: "direct-push", detail: `no pull request merged into ${branch}` };
|
|
23
|
+
const results = await api.checksOf(pull.head.sha);
|
|
24
|
+
const passed = new Set(results.filter((c) => PASSING.has(c.conclusion)).map((c) => c.name));
|
|
25
|
+
const missing = requiredChecks.filter((name) => !passed.has(name));
|
|
26
|
+
if (missing.length === 0) return null;
|
|
27
|
+
return { sha, kind: "checks-skipped", pr: pull.number, mergedBy: pull.merged_by?.login ?? "unknown", missing };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** What changed in the protection against what is declared. */
|
|
31
|
+
function auditProtection(protection, { branch, requiredChecks }) {
|
|
32
|
+
if (!protection) return [`${branch} is not protected`];
|
|
33
|
+
const changes = [];
|
|
34
|
+
if (!protection.enforce_admins?.enabled) changes.push("enforce_admins is off: admins can merge past the required checks");
|
|
35
|
+
const rules = protection.required_status_checks ?? {};
|
|
36
|
+
const enforced = new Set([...(rules.contexts ?? []), ...(rules.checks ?? []).map((c) => c.context)]);
|
|
37
|
+
for (const name of requiredChecks) if (!enforced.has(name)) changes.push(`required check removed: ${name}`);
|
|
38
|
+
return changes;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {{ branch: string, commits: string[], requiredChecks: string[], api: object }} opts
|
|
43
|
+
* @returns {Promise<{ ok: boolean, merges: object[], protection: string[] }>}
|
|
44
|
+
*/
|
|
45
|
+
export async function auditMain({ branch, commits, requiredChecks, api }) {
|
|
46
|
+
if (!requiredChecks?.length) throw new Error("main-guard: declare at least one required check to audit against");
|
|
47
|
+
const merges = [];
|
|
48
|
+
for (const sha of commits) {
|
|
49
|
+
const finding = await auditCommit(sha, { branch, requiredChecks, api });
|
|
50
|
+
if (finding) merges.push(finding);
|
|
51
|
+
}
|
|
52
|
+
const protection = auditProtection(await api.protection(), { branch, requiredChecks });
|
|
53
|
+
return { ok: merges.length === 0 && protection.length === 0, merges, protection };
|
|
54
|
+
}
|
|
@@ -43,6 +43,34 @@ const addCommand = ({ missing, ...target }) =>
|
|
|
43
43
|
`gh api -X POST ${protectionPath(target)}/required_status_checks/contexts ${missing.map((c) => `-f ${shellQuote(`contexts[]=${c}`)}`).join(" ")}`;
|
|
44
44
|
|
|
45
45
|
const settingsUrl = ({ owner, repo }) => `https://github.com/${owner}/${repo}/settings/branches`;
|
|
46
|
+
const rulesUrl = ({ owner, repo }) => `https://github.com/${owner}/${repo}/settings/rules`;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* KJC-BUG-0449 (#2787): every rule that applies to the branch, rulesets included.
|
|
50
|
+
* [] when none applies (a 404 included); null when it cannot be read.
|
|
51
|
+
*/
|
|
52
|
+
async function branchRules({ owner, repo, branch }, run) {
|
|
53
|
+
let res;
|
|
54
|
+
try { res = await run("gh", ["api", `repos/${owner}/${repo}/rules/branches/${encodeURIComponent(branch)}`]); } catch { return null; }
|
|
55
|
+
if (res.exitCode !== 0) return /404/.test(`${res.stderr}${res.stdout}`) ? [] : null;
|
|
56
|
+
try {
|
|
57
|
+
const rules = JSON.parse(res.stdout || "[]");
|
|
58
|
+
return Array.isArray(rules) ? rules : null;
|
|
59
|
+
} catch { return null; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const rulesetChecks = (rules) => rules
|
|
63
|
+
.filter((r) => r?.type === "required_status_checks")
|
|
64
|
+
.flatMap((r) => (r.parameters?.required_status_checks ?? []).map((c) => c.context));
|
|
65
|
+
|
|
66
|
+
/** No classic protection: the rulesets decide, and they are fixed in their own page, never with a PUT. */
|
|
67
|
+
function rulesetAdvice({ rules, checks, target }) {
|
|
68
|
+
if (rules === null) return { status: "unverified", reason: "the branch rules could not be read", settings: rulesUrl(target), missing: checks };
|
|
69
|
+
if (rules.length === 0) return { status: "unprotected", command: protectCommand({ ...target, checks }) };
|
|
70
|
+
const required = new Set(rulesetChecks(rules));
|
|
71
|
+
const missing = checks.filter((c) => !required.has(c));
|
|
72
|
+
return missing.length > 0 ? { status: "ruleset-missing", missing, settings: rulesUrl(target) } : { status: "ok", via: "ruleset" };
|
|
73
|
+
}
|
|
46
74
|
|
|
47
75
|
/**
|
|
48
76
|
* What the base branch needs for the checks to be required, and how. A command
|
|
@@ -61,14 +89,18 @@ export async function protectionAdvice({ owner, repo, branch, checks, run = runC
|
|
|
61
89
|
return { status: "unverified", reason: err.message, settings: settingsUrl(target), missing: checks };
|
|
62
90
|
}
|
|
63
91
|
if (res.exitCode !== 0) {
|
|
64
|
-
if (/404|not protected/i.test(`${res.stderr}${res.stdout}`)) return {
|
|
92
|
+
if (/404|not protected/i.test(`${res.stderr}${res.stdout}`)) return rulesetAdvice({ rules: await branchRules(target, run), checks, target });
|
|
65
93
|
return { status: "unverified", reason: (res.stderr || res.stdout || "gh failed").trim(), settings: settingsUrl(target), missing: checks };
|
|
66
94
|
}
|
|
67
95
|
const protection = JSON.parse(res.stdout || "{}");
|
|
96
|
+
const rules = await branchRules(target, run);
|
|
97
|
+
const fromRules = rulesetChecks(rules ?? []);
|
|
68
98
|
const statusChecks = protection.required_status_checks;
|
|
69
|
-
|
|
70
|
-
const existing = new Set(statusChecks.contexts ?? []);
|
|
99
|
+
const existing = new Set([...(statusChecks?.contexts ?? []), ...fromRules]);
|
|
71
100
|
const missing = checks.filter((c) => !existing.has(c));
|
|
101
|
+
// Unreadable rules: a ruleset may require what the classic layer lacks, so nothing is "missing" for sure.
|
|
102
|
+
if (rules === null && missing.length > 0) return { status: "unverified", reason: "the branch rules could not be read", settings: rulesUrl(target), missing };
|
|
103
|
+
if (!statusChecks && fromRules.length === 0) return { status: "no-checks", settings: settingsUrl(target), missing: checks };
|
|
72
104
|
// KJC-BUG-0401: required checks an admin skips are not required for an agent holding an admin's token.
|
|
73
105
|
const bindAdmins = protection.enforce_admins?.enabled === false ? `gh api -X POST ${protectionPath(target)}/enforce_admins` : null;
|
|
74
106
|
if (missing.length > 0) return { status: "missing", missing, command: [addCommand({ ...target, missing }), bindAdmins].filter(Boolean).join(" && ") };
|
|
@@ -105,6 +137,7 @@ export async function adviseBranchProtection({ projectDir, branch, logger, run =
|
|
|
105
137
|
missing: `${branch} does not require ${advice.missing?.length} kj check(s) (${list}). An admin runs (kj never does):`,
|
|
106
138
|
"no-checks": `${branch} is protected but requires no status checks. An admin enables "Require status checks" and adds ${list} in:`,
|
|
107
139
|
unverified: `could not read the protection of ${branch} (${advice.reason}). An admin checks that ${list} are required in:`,
|
|
140
|
+
"ruleset-missing": `${branch} is protected by a ruleset that does not require ${advice.missing?.length} kj check(s) (${list}). An admin adds them to the ruleset (no second protection layer) in:`,
|
|
108
141
|
"admins-exempt": `${branch} requires the kj checks, but admins can merge without them, and an agent working with an admin's token would. An admin runs (kj never does):`,
|
|
109
142
|
}[advice.status];
|
|
110
143
|
logger.warn?.(` ⚠ branch protection: ${why}`);
|
|
@@ -109,9 +109,17 @@ function identityGuard(hook) {
|
|
|
109
109
|
" fi",
|
|
110
110
|
" kj_declared_gh=$(sed -n 's/^gh_user:[[:space:]]*//p' .karajan/identity.local.yml | head -n1)",
|
|
111
111
|
' kj_hosts="${GH_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/gh}/hosts.yml"',
|
|
112
|
+
// KJC-BUG-0459: a per-account gh (gh-tribbu, ADR 0040) as the credential helper is the account
|
|
113
|
+
// git pushes as. The accounts guide makes gh-<account> use ~/.config/gh-<account>: read it.
|
|
114
|
+
" kj_helper=$(git config --get-urlmatch credential.helper https://github.com 2>/dev/null)",
|
|
115
|
+
' case "$kj_helper" in',
|
|
116
|
+
" '!'*/gh-*' auth git-credential'*|'!'gh-*' auth git-credential'*)",
|
|
117
|
+
' kj_ghbin=${kj_helper#!}; kj_ghbin=${kj_ghbin%% *}',
|
|
118
|
+
' kj_hosts="${XDG_CONFIG_HOME:-$HOME/.config}/${kj_ghbin##*/}/hosts.yml" ;;',
|
|
119
|
+
" esac",
|
|
112
120
|
" kj_active_gh=$(awk '/^github.com:/{f=1;next} f&&/^[^[:space:]]/{f=0} f&&/^[[:space:]]*user:/{sub(/^[[:space:]]*user:[[:space:]]*/,\"\");print;exit}' \"$kj_hosts\" 2>/dev/null)",
|
|
113
121
|
' if [ -n "$kj_declared_gh" ] && [ "$kj_active_gh" != "$kj_declared_gh" ]; then',
|
|
114
|
-
' echo "kj harden: identity lock — gh session is ${kj_active_gh:-none} but this clone is declared as $kj_declared_gh — run: gh auth switch --user $kj_declared_gh"; exit 1',
|
|
122
|
+
' echo "kj harden: identity lock — gh session is ${kj_active_gh:-none} but this clone is declared as $kj_declared_gh — run: gh auth switch --user $kj_declared_gh (or set the gh of that account, e.g. gh-<account>, as the credential helper)"; exit 1',
|
|
115
123
|
" fi",
|
|
116
124
|
...tail,
|
|
117
125
|
];
|
|
@@ -147,6 +155,13 @@ export function hookBody(hook, cmds = {}, { baseBranch = null } = {}) {
|
|
|
147
155
|
"if git diff --cached --name-only --diff-filter=d | grep -qE '^(\\.claude/[^/]*\\.lock|\\.claude/scheduled_tasks\\.lock|\\.kj/|\\.karajan/identity\\.local\\.yml|\\.karajan/harness/sentinel-state\\.json)'; then",
|
|
148
156
|
" echo 'kj harden: agent runtime files are staged (.claude/*.lock, .kj/, identity.local.yml, sentinel-state.json) — unstage them: git restore --staged <path>'; exit 1",
|
|
149
157
|
"fi",
|
|
158
|
+
"# KJC-TSK-1234 (ADR 0032): the project's kj.config.yml travels with the repo, so its staged",
|
|
159
|
+
"# version is checked for secrets by kj, which parses the YAML (raw text can spell a key in",
|
|
160
|
+
"# too many ways). Without kj nothing can check it, so it does not enter.",
|
|
161
|
+
"if git diff --cached --name-only --diff-filter=d | grep -qx '.karajan/kj.config.yml'; then",
|
|
162
|
+
" command -v kj >/dev/null 2>&1 || { echo 'kj harden: .karajan/kj.config.yml is staged but kj is not installed to check it holds no secret'; exit 1; }",
|
|
163
|
+
" git show ':.karajan/kj.config.yml' | kj config --check-secrets - || { echo 'kj harden: keep tokens and passwords out of the shared kj.config.yml'; exit 1; }",
|
|
164
|
+
"fi",
|
|
150
165
|
"# KJC-TSK-1205 (ADR 0030): a tampered Sentinel is caught before each commit,",
|
|
151
166
|
"# not only when the turn ends. Only where it is installed; no kj, no check.",
|
|
152
167
|
"if [ -d .karajan/harness ] && command -v kj >/dev/null 2>&1; then",
|
|
@@ -7,13 +7,14 @@ import process from "node:process";
|
|
|
7
7
|
import { realpathSync } from "node:fs";
|
|
8
8
|
import { basename, dirname, join, relative } from "node:path";
|
|
9
9
|
import { spawnSync } from "node:child_process";
|
|
10
|
-
import { CODE, TESTS, ROOT, CARD, branchOf, load, save, session } from "./sentinel-lib.mjs";
|
|
10
|
+
import { CODE, TESTS, ROOT, CARD, branchOf, enterLane, laneFor, laneTop, load, save, session } from "./sentinel-lib.mjs";
|
|
11
11
|
import { remindersFor } from "./sentinel-reminders.mjs";
|
|
12
12
|
let raw = "";
|
|
13
13
|
process.stdin.on("data", (d) => { raw += d; });
|
|
14
14
|
process.stdin.on("end", () => {
|
|
15
15
|
try {
|
|
16
|
-
const { session_id: sid = "default", tool_name: tool, tool_input: input = {}, tool_response: response = {} } = JSON.parse(raw);
|
|
16
|
+
const { session_id: sid = "default", tool_name: tool, tool_input: input = {}, tool_response: response = {}, cwd = null } = JSON.parse(raw);
|
|
17
|
+
enterLane(load(), sid, cwd);
|
|
17
18
|
// KJC-TSK-0765 board-sync: a merge that gh CONFIRMS leaves its card pending
|
|
18
19
|
// in the tracker (the PreToolUse gate then refuses to advance until it is
|
|
19
20
|
// moved). Recorded here, after the fact, so a failed merge never blocks.
|
|
@@ -76,7 +77,10 @@ process.stdin.on("end", () => {
|
|
|
76
77
|
// Sources are read STRUCTURALLY, never through the stringified response:
|
|
77
78
|
// a JSON document (the MCP {content:[{text}]} parts, the CLI --json array)
|
|
78
79
|
// is parsed and its hits[].source / [].source collected.
|
|
79
|
-
|
|
80
|
+
// KJC-BUG-0440 (#2830): the MCP tool may wrap its answer again (the part's
|
|
81
|
+
// text is itself {content:[parts]}); unwrapped up to three levels.
|
|
82
|
+
const sourcesOf = (doc, depth = 0) => {
|
|
83
|
+
if (Array.isArray(doc?.content) && depth < 3) return doc.content.flatMap((p) => sourcesOf(parseJson(String(p?.text || "")), depth + 1));
|
|
80
84
|
const list = Array.isArray(doc) ? doc : Array.isArray(doc?.hits) ? doc.hits : [];
|
|
81
85
|
return list.map((h) => h?.source).filter((x) => typeof x === "string");
|
|
82
86
|
};
|
|
@@ -88,7 +92,7 @@ process.stdin.on("end", () => {
|
|
|
88
92
|
let found = parts.flatMap((p) => sourcesOf(parseJson(String(p?.text || ""))));
|
|
89
93
|
// KJC-BUG-0365 (#2208): an answer cut short (whole code chunks) is no
|
|
90
94
|
// longer valid JSON, but every "source" it carries still names a hit.
|
|
91
|
-
if (found.length === 0) found = [...text.matchAll(
|
|
95
|
+
if (found.length === 0) found = [...text.matchAll(/\\*"source\\*"\s*:\s*\\*"([^"\\]+)\\*"/g)].map((m) => m[1]);
|
|
92
96
|
recordRag(input.text, found, parts.map((p) => String(p?.text || "")).join(String.fromCharCode(10)));
|
|
93
97
|
process.exit(0);
|
|
94
98
|
}
|
|
@@ -184,10 +188,18 @@ process.stdin.on("end", () => {
|
|
|
184
188
|
const state = load();
|
|
185
189
|
const s = session(state, sid);
|
|
186
190
|
s.at = Date.now();
|
|
187
|
-
|
|
191
|
+
// ADR 0034 (KJC-TSK-1245): a write in another lane is recorded under that lane, relative to it,
|
|
192
|
+
// so the Stop gate judges it with that lane's branch and tree.
|
|
193
|
+
const top = laneFor(file);
|
|
194
|
+
const rec = top === laneTop() ? s : (((s.lanes ||= {})[top]) ||= { edited_sources: [], edited_tests: [] });
|
|
195
|
+
const rel = relative(top, file).replaceAll("\\", "/");
|
|
188
196
|
// KJC-BUG-0279 (#1981): a file outside the repository is not one of its sources.
|
|
189
197
|
const inside = rel && rel !== ".." && !rel.startsWith("../") && !rel.startsWith("/");
|
|
190
|
-
const
|
|
198
|
+
const bucketOf = () => {
|
|
199
|
+
if (TESTS.test(rel)) return rec.edited_tests;
|
|
200
|
+
return CODE.test(rel) ? rec.edited_sources : null;
|
|
201
|
+
};
|
|
202
|
+
const bucket = inside ? bucketOf() : null;
|
|
191
203
|
if (bucket && !bucket.includes(rel)) bucket.push(rel);
|
|
192
204
|
// KJC-TSK-0910: the branch size while it is written, with the CI budget
|
|
193
205
|
// (kj pr-size), said once per threshold crossed. Context, never a block.
|