karajan-code 4.70.4 → 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/dir-setup.js +34 -1
- package/src/checks/native-build.js +21 -39
- package/src/checks/runner.js +128 -142
- package/src/cli/advanced-commands.js +6 -6
- package/src/cli/register-meta.js +42 -82
- package/src/cli/register-pipeline.js +8 -143
- package/src/cli/register-plan.js +9 -100
- package/src/cli/retired-commands.js +49 -0
- package/src/cli.js +18 -5
- package/src/commands/agent-run.js +40 -26
- package/src/commands/audit.js +127 -124
- package/src/commands/board.js +5 -9
- package/src/commands/check.js +29 -16
- package/src/commands/doctor.js +2 -0
- package/src/commands/env.js +176 -150
- package/src/commands/go.js +71 -53
- package/src/commands/harden.js +90 -81
- package/src/commands/hu-start.js +52 -0
- package/src/commands/hu.js +288 -221
- package/src/commands/identity.js +111 -78
- package/src/commands/init-atril.js +47 -0
- package/src/commands/init.js +11 -40
- package/src/commands/install-tools.js +70 -85
- package/src/commands/main-guard.js +78 -0
- package/src/commands/policy.js +226 -184
- package/src/commands/qmd.js +27 -21
- package/src/commands/rag.js +75 -48
- package/src/commands/review-gate.js +421 -341
- 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/secrets.js +36 -0
- package/src/config.js +1 -0
- package/src/environment/adr.js +16 -2
- package/src/git/karajan-label.js +20 -8
- package/src/guards/main-guard.js +54 -0
- package/src/harden/hook-templates.js +16 -1
- package/src/harden/sentinel/posttooluse.mjs +18 -6
- package/src/harden/sentinel/pretooluse-sentinel.mjs +64 -36
- package/src/harden/sentinel/sentinel-lib.mjs +129 -26
- package/src/harden/sentinel/sentinel-shell.mjs +5 -16
- package/src/harden/sentinel/sessionstart.mjs +3 -2
- package/src/harden/sentinel/stop.mjs +5 -4
- package/src/harden/workflow-templates.js +1 -1
- package/src/hu/board-lookup.js +29 -0
- package/src/hu/gh-identity.js +23 -0
- package/src/hu/github-board.js +4 -2
- package/src/hu/lane-prune.js +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 +1 -35
- package/src/mcp/handlers/management-handlers.js +2 -42
- package/src/mcp/server-handlers.js +16 -22
- package/src/mcp/tools.js +1 -196
- package/src/plan/lane-plans.js +54 -0
- package/src/plan/plan-store.js +7 -4
- package/src/policy/add.js +26 -12
- package/src/policy/adr-number-check.js +31 -0
- package/src/prompts/audit.js +299 -339
- package/src/prompts/card-context.js +10 -5
- package/src/prompts/reviewer.js +5 -2
- package/src/rag/auto-update.js +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/gate-gitignore.js +23 -4
- package/src/review/gate-hook.js +12 -4
- package/src/review/one-shot-review.js +45 -35
- package/src/review/publish-pr.js +14 -2
- package/src/review/repo-docs.js +73 -0
- package/src/review/repo-stack.js +59 -0
- package/src/review/reviewer-fallback.js +19 -4
- package/src/review/seal-commit.js +29 -0
- package/src/review/sonar-pregate.js +59 -44
- package/src/review/third-party-review.js +90 -16
- package/src/review/third-party-size.js +43 -0
- package/src/review/third-party-skills.js +54 -0
- package/src/review/verify-pr.js +1 -24
- package/src/roles/audit-role.js +106 -125
- package/src/skills/atril-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/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}`, "",
|
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
|
+
}
|
|
@@ -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.
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
import console from "node:console";
|
|
8
8
|
import process from "node:process";
|
|
9
9
|
import { dirname, join, relative, resolve } from "node:path";
|
|
10
|
-
import { appendFileSync, existsSync, readFileSync
|
|
10
|
+
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { doc, CODE, TESTS, ROOT, BASE_BRANCHES, CARD, STATE, branchOf,
|
|
14
|
+
import { doc, CODE, TESTS, ROOT, BASE_BRANCHES, CARD, STATE, branchOf, enterLane, holdLane, laneFor, laneRefusal, laneTop, load, save, session, boardGate, pendingMoves, pendingText, stagedVerdict } from "./sentinel-lib.mjs";
|
|
15
15
|
// KJC-TSK-0915 (ADR 0014): the shell reader is a real, unit-tested module copied here as is.
|
|
16
16
|
import { commitStaging, isGhIssueCreate, isPublishCommand, isReadOnlyCommand, optionValues, shellSegments, stripInertQuotes, stripTextOptionValues, stripTextOnlyCommandValues } from "./sentinel-shell.mjs";
|
|
17
17
|
import { DISCARD_VERBS, discardOf, foreignLost, removedFiles } from "./sentinel-discard.mjs";
|
|
@@ -26,7 +26,8 @@ let raw = "";
|
|
|
26
26
|
process.stdin.on("data", (d) => { raw += d; });
|
|
27
27
|
process.stdin.on("end", () => {
|
|
28
28
|
try {
|
|
29
|
-
const { session_id: sid = "default", tool_name: tool, tool_input: input = {}, transcript_path: transcript = null } = JSON.parse(raw);
|
|
29
|
+
const { session_id: sid = "default", tool_name: tool, tool_input: input = {}, transcript_path: transcript = null, cwd = null } = JSON.parse(raw);
|
|
30
|
+
enterLane(load(), sid, cwd);
|
|
30
31
|
// KJC-TSK-0920 (SNT-E): every deny ends with the same line, whatever gate it was.
|
|
31
32
|
// KJC-TSK-0968 (HUM-E, ADR 0018): and leaves a record next to the state, with the
|
|
32
33
|
// gate's own words: kj handoff takes the why from here, never from the agent.
|
|
@@ -50,7 +51,7 @@ process.stdin.on("end", () => {
|
|
|
50
51
|
// hasOwn + defineProperty: a file named __proto__ or toString is a path, not a prototype key.
|
|
51
52
|
if (Object.hasOwn(touch, relT)) return;
|
|
52
53
|
// --ignored: a user's ignored .env is theirs too (git clean -x deletes it).
|
|
53
|
-
const gs = spawnSync("git", ["-C",
|
|
54
|
+
const gs = spawnSync("git", ["-C", laneTop(), "status", "--porcelain", "--ignored", "--", relT], { encoding: "utf8" });
|
|
54
55
|
const value = gs.status === 0 && String(gs.stdout).trim() === "" ? "clean" : "dirty";
|
|
55
56
|
Object.defineProperty(touch, relT, { value, enumerable: true, writable: true, configurable: true });
|
|
56
57
|
save(st);
|
|
@@ -60,7 +61,9 @@ process.stdin.on("end", () => {
|
|
|
60
61
|
// only the human, editing outside it.
|
|
61
62
|
if (EDIT_TOOLS.includes(tool)) {
|
|
62
63
|
const target = input.file_path || input.notebook_path;
|
|
63
|
-
|
|
64
|
+
// ADR 0034: judged in the lane the write lands in (its supervisor, its config).
|
|
65
|
+
const targetTop = target ? laneFor(target) : laneTop();
|
|
66
|
+
const relT = target ? relative(targetTop, String(target)).replaceAll("\\", "/") : "";
|
|
64
67
|
if (relT && PROTECTED.test(relT)) {
|
|
65
68
|
console.error("karajan sentinel: ese fichero es parte del supervisor (" + relT + ") — solo el humano desmonta el sentinel, editalo fuera de la sesion." + doc("supervisor"));
|
|
66
69
|
process.exit(2);
|
|
@@ -74,7 +77,8 @@ process.stdin.on("end", () => {
|
|
|
74
77
|
console.error("karajan sentinel: " + relT + " es configuracion de gobierno (politicas, gates, exclusiones): la cambia tu usuario, no la sesion. Si un gate te parece injusto, proponselo a tu usuario o usa kj report-issue." + doc("governance"));
|
|
75
78
|
process.exit(2);
|
|
76
79
|
}
|
|
77
|
-
|
|
80
|
+
// The ledger is keyed by path in the session's lane: a write the lane guard will deny records nothing.
|
|
81
|
+
if (targetTop === laneTop() && relT && !relT.startsWith("..") && !relT.startsWith("/")) noteFirstTouch(relT);
|
|
78
82
|
}
|
|
79
83
|
// KJC-BUG-0261: rm / git rm is the session touching those files, noted before they go.
|
|
80
84
|
if (tool === "Bash") for (const words of shellSegments(String(input.command || ""))) for (const f of removedFiles(words, ROOT)) noteFirstTouch(f);
|
|
@@ -202,9 +206,9 @@ process.stdin.on("end", () => {
|
|
|
202
206
|
};
|
|
203
207
|
const declaredGh = idLine("gh_user");
|
|
204
208
|
const ghHostsPath = join(process.env.GH_CONFIG_DIR || join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "gh"), "hosts.yml");
|
|
205
|
-
const activeGh = () => {
|
|
206
|
-
if (!existsSync(
|
|
207
|
-
const lines = readFileSync(
|
|
209
|
+
const activeGh = (hostsPath = ghHostsPath) => {
|
|
210
|
+
if (!existsSync(hostsPath)) return null;
|
|
211
|
+
const lines = readFileSync(hostsPath, "utf8").split(ESC_NL);
|
|
208
212
|
const start = lines.findIndex((l) => l.startsWith("github.com:"));
|
|
209
213
|
if (start < 0) return null;
|
|
210
214
|
for (let k = start + 1; k < lines.length; k += 1) {
|
|
@@ -216,6 +220,19 @@ process.stdin.on("end", () => {
|
|
|
216
220
|
};
|
|
217
221
|
const words = (seg) => seg.trim().split(" ").filter((w) => w.length > 0 && w !== ESC_TAB);
|
|
218
222
|
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); };
|
|
223
|
+
// KJC-BUG-0459: `GH_CONFIG_DIR=<dir> gh ...` reads <dir>, not the hook's config.
|
|
224
|
+
const hostsOf = (ws) => {
|
|
225
|
+
const a = ws.slice(0, ws.length - headOf(ws).length).findLast((w) => w.startsWith("GH_CONFIG_DIR="));
|
|
226
|
+
if (!a) return null; // no assignment of its own (not the same as one naming the default dir)
|
|
227
|
+
const d = bare(a.slice("GH_CONFIG_DIR=".length)).replace(/^(~|\$HOME|\$\{HOME\})(?=\/|$)/, homedir());
|
|
228
|
+
return join(d, "hosts.yml");
|
|
229
|
+
};
|
|
230
|
+
// KJC-BUG-0459: a per-account gh (gh-tribbu, gh-ox: ADR 0040). The accounts guide makes
|
|
231
|
+
// gh-<account> run gh with GH_CONFIG_DIR=~/.config/gh-<account>: its config is READ, the
|
|
232
|
+
// wrapper is never executed by the hook.
|
|
233
|
+
const wrapperName = (w) => w.slice(w.lastIndexOf("/") + 1);
|
|
234
|
+
const isGhWrapper = (w) => /^gh-[A-Za-z0-9_.-]+$/.test(wrapperName(w));
|
|
235
|
+
const wrapperHosts = (w) => join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), wrapperName(w), "hosts.yml");
|
|
219
236
|
// Wrapper shells hide the payload in a string (review catch): scan inside.
|
|
220
237
|
const WRAPPERS =["sh", "bash", "zsh", "dash", "eval", "env", "xargs", "sudo", "nohup", "time", "exec"];
|
|
221
238
|
let switchedTo = null;
|
|
@@ -223,12 +240,21 @@ process.stdin.on("end", () => {
|
|
|
223
240
|
// $( and backticks are separators too (review catch): their payload is its own segment.
|
|
224
241
|
const SEPS = [";", "&&", "||", "|", "$(", String.fromCharCode(96), "(", ")"];
|
|
225
242
|
const bare = (w) => w.replaceAll(String.fromCharCode(39), "").replaceAll(String.fromCharCode(34), "");
|
|
226
|
-
|
|
227
|
-
const
|
|
243
|
+
// A rescanned payload inherits the config resolved outside it (`GH_CONFIG_DIR=<dir> env gh ...`).
|
|
244
|
+
const scan = (text, depth, inherited = null) => {
|
|
245
|
+
if (depth > 3) return;
|
|
246
|
+
for (const seg of SEPS.reduce((acc, s) => acc.flatMap((l) => l.split(s)), text.split(ESC_NL))) {
|
|
247
|
+
checkSeg(headOf(words(seg)), depth, hostsOf(words(seg)) ?? inherited);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
// hostsPath: the config an explicit GH_CONFIG_DIR assignment selects, or null without one.
|
|
251
|
+
const checkSeg = (ws, depth, hostsPath = null) => {
|
|
228
252
|
if (ws.length === 0) return;
|
|
229
253
|
if (WRAPPERS.includes(ws[0])) { // wrapper args have arity (sudo -u bob): scan from the first gh/wrapper token (review catch)
|
|
230
|
-
const j = ws.findIndex((w, i) => i > 0 && ["gh", ...WRAPPERS].includes(bare(w)));
|
|
231
|
-
|
|
254
|
+
const j = ws.findIndex((w, i) => i > 0 && (["gh", ...WRAPPERS].includes(bare(w)) || isGhWrapper(bare(w))));
|
|
255
|
+
// `env GH_CONFIG_DIR=<dir> gh ...`: the assignments before the command go with it (review catch).
|
|
256
|
+
const assigns = ws.slice(1, Math.max(j, 1)).map(bare).filter((w) => /^[A-Za-z_][A-Za-z0-9_]*=/.test(w));
|
|
257
|
+
if (j > 0) scan([...assigns, ...ws.slice(j).map(bare)].join(" "), depth + 1, hostsPath);
|
|
232
258
|
return;
|
|
233
259
|
}
|
|
234
260
|
if (ws[0] === "gh") {
|
|
@@ -237,8 +263,15 @@ process.stdin.on("end", () => {
|
|
|
237
263
|
return; // session commands ARE the remedy — never blocked
|
|
238
264
|
}
|
|
239
265
|
if (!declaredGh) { problems.push("gh without a declared identity for this clone — run: kj identity set"); return; }
|
|
240
|
-
const eff = switchedTo || activeGh();
|
|
241
|
-
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 + " && ...");
|
|
266
|
+
const eff = hostsPath ? activeGh(hostsPath) : (switchedTo || activeGh());
|
|
267
|
+
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 + " && ... (or use the gh of that account, e.g. gh-<account>)");
|
|
268
|
+
}
|
|
269
|
+
if (isGhWrapper(ws[0])) {
|
|
270
|
+
if (ws[1] === "auth") return; // its session commands are the remedy too
|
|
271
|
+
if (!declaredGh) { problems.push(ws[0] + " without a declared identity for this clone — run: kj identity set"); return; }
|
|
272
|
+
// Its own config even with an inline GH_CONFIG_DIR: the wrapper sets that variable itself.
|
|
273
|
+
const eff = activeGh(wrapperHosts(ws[0]));
|
|
274
|
+
if (eff !== declaredGh) problems.push(wrapperName(ws[0]) + " runs as '" + (eff || "no session") + "' (" + wrapperHosts(ws[0]) + ") but this clone is declared as '" + declaredGh + "' — use the gh of that account");
|
|
242
275
|
}
|
|
243
276
|
// KJC-TSK-1085 (ADR 0015): git commit/tag/rebase/push are no longer read here:
|
|
244
277
|
// pre-commit checks the author, pre-push every pushed commit and the gh session.
|
|
@@ -249,24 +282,18 @@ process.stdin.on("end", () => {
|
|
|
249
282
|
process.exit(2);
|
|
250
283
|
}
|
|
251
284
|
}
|
|
252
|
-
// MONO-0 (KJC-TSK-
|
|
253
|
-
//
|
|
285
|
+
// MONO-0 (ADR 0002) under ADR 0034 (KJC-TSK-1244): a write in another lane of the SAME
|
|
286
|
+
// repo is allowed while no other live session holds that lane; the session then holds it.
|
|
287
|
+
// A held lane is denied BEFORE the damage. Reads are free.
|
|
288
|
+
// Another lane of this repo => its toplevel; the session's own lane or no repo => null.
|
|
254
289
|
const laneOf = (p) => {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (!(existsSync(d) && statSync(d).isDirectory())) d = dirname(d);
|
|
258
|
-
while (d && !existsSync(d)) {
|
|
259
|
-
const up = dirname(d);
|
|
260
|
-
if (up === d) return null;
|
|
261
|
-
d = up;
|
|
262
|
-
}
|
|
263
|
-
// Canonicaliza (reviewer catch: un symlink local hacia otro carril
|
|
264
|
-
// esquivaba la clasificacion por ruta literal).
|
|
265
|
-
try { d = realpathSync(d); } catch { /* se clasifica tal cual */ }
|
|
266
|
-
return d ? foreignLane(d) : null;
|
|
290
|
+
const top = laneFor(p);
|
|
291
|
+
return top === laneTop() ? null : top;
|
|
267
292
|
};
|
|
268
293
|
const laneDeny = (what, lane) => {
|
|
269
|
-
|
|
294
|
+
const why = holdLane(lane);
|
|
295
|
+
if (!why) return false;
|
|
296
|
+
console.error("karajan sentinel: " + what + " vive en otro carril (" + lane + ") de este repo " + why + (laneRefusal() ? " " + laneRefusal() : "") + ". Sin escape (ADR 0015): un cruce deliberado lo hace tu usuario en su terminal." + doc("cross-lane"));
|
|
270
297
|
return true;
|
|
271
298
|
};
|
|
272
299
|
if (EDIT_TOOLS.includes(tool)) {
|
|
@@ -523,10 +550,12 @@ process.stdin.on("end", () => {
|
|
|
523
550
|
}
|
|
524
551
|
} catch { /* no report or unreadable — the Steward is not adopted here */ }
|
|
525
552
|
const file = input.file_path || input.notebook_path;
|
|
526
|
-
|
|
553
|
+
// ADR 0034: the card, the branch and the RAG of the lane the write lands in.
|
|
554
|
+
const top = file ? laneFor(file) : laneTop();
|
|
555
|
+
const rel = file ? relative(top, String(file)).replaceAll("\\", "/") : "";
|
|
527
556
|
// KJC-BUG-0279 (#1981): outside the repository there is no source to govern.
|
|
528
557
|
if (rel && rel !== ".." && !rel.startsWith("../") && !rel.startsWith("/") && CODE.test(rel) && !TESTS.test(rel)) {
|
|
529
|
-
const branch = branchOf();
|
|
558
|
+
const branch = branchOf(top);
|
|
530
559
|
const why = !branch ? null : BASE_BRANCHES.has(branch) ? "base" : !CARD.test(branch) ? "nocard" : null;
|
|
531
560
|
// ADR 0015 (KJC-TSK-0925): no escape. Without a card there is no work.
|
|
532
561
|
if (why) {
|
|
@@ -549,7 +578,7 @@ process.stdin.on("end", () => {
|
|
|
549
578
|
// Solo se pregunta a git en el camino lento, cuando el gate iba a
|
|
550
579
|
// denegar: exit 1 = git responde que no lo conoce; cualquier otro
|
|
551
580
|
// resultado se comporta como hasta ahora y bloquea.
|
|
552
|
-
const fresh = () => !existsSync(String(file)) || spawnSync("git", ["-C",
|
|
581
|
+
const fresh = () => !existsSync(String(file)) || spawnSync("git", ["-C", top, "ls-files", "--error-unmatch", "--", rel], { encoding: "utf8" }).status === 1;
|
|
553
582
|
// A path outside this tree is another lane's business (the lane guard
|
|
554
583
|
// above rules on it) and the ledger only holds in-repo paths.
|
|
555
584
|
// KJC-BUG-0190: with nothing indexed, no query can cover anything —
|
|
@@ -570,7 +599,7 @@ process.stdin.on("end", () => {
|
|
|
570
599
|
// esa: un indice vacio o por detras si tiene arreglo (kj rag index)
|
|
571
600
|
// y sigue bloqueando, porque si no el gate desapareceria en
|
|
572
601
|
// cualquier proyecto sin indexar. Nada legible = se bloquea.
|
|
573
|
-
const cov = spawnSync("kj", ["rag", "covers", rel, "--json"], { cwd:
|
|
602
|
+
const cov = spawnSync("kj", ["rag", "covers", rel, "--json"], { cwd: top, encoding: "utf8" });
|
|
574
603
|
let blind = null;
|
|
575
604
|
let absent = null;
|
|
576
605
|
try {
|
|
@@ -640,8 +669,7 @@ process.stdin.on("end", () => {
|
|
|
640
669
|
// tail that hid its failure, let a commit through with no verdict at all.
|
|
641
670
|
const plan = commitStaging(cmd);
|
|
642
671
|
if (plan.commits) {
|
|
643
|
-
|
|
644
|
-
const verdict = stagedVerdict(plan.dir ? resolve(ROOT, plan.dir) : ROOT);
|
|
672
|
+
const verdict = stagedVerdict(plan.dir ? resolve(cwd || ROOT, plan.dir) : (cwd || ROOT));
|
|
645
673
|
if (verdict.gate && plan.stages) {
|
|
646
674
|
console.error("karajan sentinel: stagea y commitea en comandos separados — el veredicto se comprueba sobre lo que ya esta stageado, y este comando stagea a la vez que commitea (git add/rm/mv, -a o rutas en git commit). Stagea por nombre, pasa kj review --staged y luego git commit." + doc("commit-verdict"));
|
|
647
675
|
process.exit(2);
|