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
package/src/commands/identity.js
CHANGED
|
@@ -21,91 +21,115 @@ import { activeGhUser, effectiveGitEmail } from "../identity/detect.js";
|
|
|
21
21
|
import { compareIdentity } from "../identity/compare.js";
|
|
22
22
|
import { createWizard, isTTY } from "../utils/wizard.js";
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
const projectDir = config?.projectDir || process.cwd();
|
|
26
|
-
const log = deps.log || console.log;
|
|
27
|
-
const tty = deps.isTTY || isTTY;
|
|
28
|
-
const detectOpts = { hostsPath: deps.hostsPath, gitFn: deps.gitFn };
|
|
29
|
-
const effective = {
|
|
30
|
-
gh_user: activeGhUser(detectOpts.hostsPath ? { hostsPath: detectOpts.hostsPath } : {}),
|
|
31
|
-
git_email: effectiveGitEmail(projectDir, detectOpts.gitFn ? { gitFn: detectOpts.gitFn } : {}),
|
|
32
|
-
};
|
|
24
|
+
const SIGNERS_FILE = ".karajan/supervisor-signers.json";
|
|
33
25
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
26
|
+
// Reading the effective identity never throws: both readers answer null when unknown.
|
|
27
|
+
const effectiveIdentity = (projectDir, deps) => ({
|
|
28
|
+
gh_user: activeGhUser(deps.hostsPath ? { hostsPath: deps.hostsPath } : {}),
|
|
29
|
+
git_email: effectiveGitEmail(projectDir, deps.gitFn ? { gitFn: deps.gitFn } : {}),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// KJC-TSK-0999 (HUM-B2, ADR 0018): a lost phone is replaced with the recovery
|
|
33
|
+
// code, never by reinstalling: its proof is the admission and the lost phone
|
|
34
|
+
// is not asked to sign. The code is checked BEFORE anything else happens.
|
|
35
|
+
// The lost phone is REVOKED by the same code: the one named with --replaces, or the one this machine had enrolled.
|
|
36
|
+
function recoveryOf({ projectDir, log, flags, deps }) {
|
|
37
|
+
const recovered = flags.recovery ? redeemRecoveryCode(flags.recovery, { projectDir }) : null;
|
|
38
|
+
if (!recovered) return { recovered: null, lost: null };
|
|
39
|
+
const lost = flags.replaces ?? (isPhoneEnrolled({ home: deps.home }) ? readEnrolledKey({ home: deps.home }) : null);
|
|
40
|
+
if (!lost) throw new Error("di qué móvil se pierde: --replaces <su clave pública> (esta máquina no tiene ninguno enrolado)");
|
|
41
|
+
log(`recuperación con el código: el móvil perdido (${lost.slice(0, 8)}…) queda revocado y no firma; el código gastado es la admisión del nuevo`);
|
|
42
|
+
return { recovered, lost };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Returns the recovery code the roster issues for the new key.
|
|
46
|
+
async function admitPhone(ctx) {
|
|
47
|
+
const { projectDir, log, kjVersion, flags, deps } = ctx;
|
|
48
|
+
// KJC-TSK-0966 (HUM-C, ADR 0018): enrolar es un acto del catálogo; hasta hoy
|
|
49
|
+
// solo lo frenaba el Sentinel, y un kj fuera de Claude no tiene Sentinel.
|
|
50
|
+
humanAct("phone-enroll", { env: deps.env ?? process.env, tty: deps.ttyHuman ?? process.stdout.isTTY, ancestry: deps.ancestry ?? {}, confirm: deps.confirm });
|
|
51
|
+
// KJC-TSK-0998 (HUM-D, ADR 0018): a phone of the roster signs the key that
|
|
52
|
+
// enters; the FIRST one has nobody to sign it (bootstrap) and runs on the four layers.
|
|
53
|
+
const key = validatePhonePublicKey(flags.publicKeyBase64);
|
|
54
|
+
const { recovered, lost } = recoveryOf(ctx);
|
|
55
|
+
const { signature } = await signAct("phone-enroll", {
|
|
56
|
+
projectDir, home: deps.home, kjVersion, logger: { warn: log, info: log },
|
|
57
|
+
deps: recovered ? { ...deps, phone: { enrolled: () => false } } : deps,
|
|
58
|
+
bootstrap: recovered !== null || readEnrolledKeys({ projectDir, home: deps.home }).size === 0,
|
|
59
|
+
files: [{ file: SIGNERS_FILE, sha256: createHash("sha256").update(key).digest("hex") }],
|
|
60
|
+
changes: [{ file: SIGNERS_FILE, status: "modified", added: 1, removed: 0, summary: `Entra la clave ${flags.label ?? "(sin etiqueta)"}`, diff: `+ ${key}`, truncated: false }],
|
|
61
|
+
});
|
|
62
|
+
// KJC-TSK-0831 (ADR 0010): the versioned roster, now with the admission CI
|
|
63
|
+
// verifies (HUM-B); the legacy key in ~/.karajan stays.
|
|
64
|
+
const { recoveryCode } = admitSigner({ projectDir, publicKey: key, label: flags.label ?? null, admission: recovered ?? signature, replaces: lost });
|
|
65
|
+
enrollPhone(key, { home: deps.home });
|
|
66
|
+
return recoveryCode;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// KJC-TSK-0822: enrola la clave PÚBLICA del móvil (la privada nunca toca esta máquina).
|
|
70
|
+
async function enrollPhoneAction(ctx) {
|
|
71
|
+
const { log } = ctx;
|
|
72
|
+
let recoveryCode;
|
|
73
|
+
try {
|
|
74
|
+
recoveryCode = await admitPhone(ctx);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
log(`kj identity enroll-phone: ${err.message}`);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
log("Móvil enrolado: la clave pública se añadió al padrón .karajan/supervisor-signers.json (commítealo) y a ~/.karajan/supervisor-phone.json. Sellar el supervisor pedirá la firma de un móvil del padrón.");
|
|
80
|
+
if (recoveryCode) {
|
|
81
|
+
log(`\nCÓDIGO DE RECUPERACIÓN, se enseña UNA sola vez. Guárdalo fuera de esta máquina:\n\n ${recoveryCode}\n`);
|
|
82
|
+
log("Sirve para enrolar un móvil nuevo si pierdes este: kj identity enroll-phone <clave nueva> --recovery <código>. Es de un solo uso: al usarlo se emite otro. Sin móvil y sin código no hay recuperación: el padrón se rehace por fuera de CI, a la vista de todos (ADR 0018).");
|
|
74
83
|
}
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
75
86
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
// KJC-TSK-0986 (HUM-F): the once-per-version invitation to enroll the phone,
|
|
88
|
+
// printed for the SessionStart hook (which cannot import kj) and for scripts.
|
|
89
|
+
function phoneInviteAction({ log, flags, deps }) {
|
|
90
|
+
// KJC-BUG-0360: the text depends on the level; at max (the default) the acts are refused.
|
|
91
|
+
const { level } = readSecurityLevel({ projectDir: deps.projectDir ?? process.cwd(), home: deps.home });
|
|
92
|
+
const text = phoneInvite({ home: deps.home, version: flags.version, level });
|
|
93
|
+
if (text) log(text);
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Resolves true when the binding may go ahead.
|
|
98
|
+
async function confirmBinding(candidate, { log, flags, deps }) {
|
|
99
|
+
const tty = deps.isTTY || isTTY;
|
|
100
|
+
if (tty() && !flags.yes) {
|
|
101
|
+
const wizard = (deps.makeWizard || createWizard)();
|
|
102
|
+
try {
|
|
103
|
+
const ok = await wizard.confirm(`Bind this clone to gh ${candidate.gh_user} / git ${candidate.git_email}?`, true);
|
|
104
|
+
if (!ok) log("kj identity: not declared.");
|
|
105
|
+
return ok;
|
|
106
|
+
} finally { wizard.close(); }
|
|
84
107
|
}
|
|
108
|
+
if (!flags.gh || !flags.email) {
|
|
109
|
+
// Proven live on day one: the active gh session had been flipped by
|
|
110
|
+
// ANOTHER session, and --yes bound the clone to the wrong account.
|
|
111
|
+
// Binding without confirmation is allowed, but never silent.
|
|
112
|
+
log("kj identity: WARNING — binding the ACTIVE session as-is without confirmation (another session may have switched it); prefer --gh/--email explicitly and verify with `kj identity show`.");
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
85
116
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const wizard = (deps.makeWizard || createWizard)();
|
|
94
|
-
try {
|
|
95
|
-
const ok = await wizard.confirm(`Bind this clone to gh ${candidate.gh_user} / git ${candidate.git_email}?`, true);
|
|
96
|
-
if (!ok) { log("kj identity: not declared."); return 1; }
|
|
97
|
-
} finally { wizard.close(); }
|
|
98
|
-
} else if (!flags.gh || !flags.email) {
|
|
99
|
-
// Proven live on day one: the active gh session had been flipped by
|
|
100
|
-
// ANOTHER session, and --yes bound the clone to the wrong account.
|
|
101
|
-
// Binding without confirmation is allowed, but never silent.
|
|
102
|
-
log("kj identity: WARNING — binding the ACTIVE session as-is without confirmation (another session may have switched it); prefer --gh/--email explicitly and verify with `kj identity show`.");
|
|
103
|
-
}
|
|
104
|
-
writeIdentity(projectDir, candidate);
|
|
105
|
-
log(`kj identity: declared gh ${candidate.gh_user} / git ${candidate.git_email} → .karajan/identity.local.yml (ignored by git).`);
|
|
106
|
-
return 0;
|
|
117
|
+
async function setAction(ctx) {
|
|
118
|
+
const { projectDir, log, flags, deps } = ctx;
|
|
119
|
+
const effective = effectiveIdentity(projectDir, deps);
|
|
120
|
+
const candidate = { gh_user: flags.gh || effective.gh_user, git_email: flags.email || effective.git_email };
|
|
121
|
+
if (!candidate.gh_user || !candidate.git_email) {
|
|
122
|
+
log(`kj identity: cannot declare — gh account: ${candidate.gh_user ?? "no session"}, git email: ${candidate.git_email ?? "none"}. Log in / set git config, or pass --gh <user> --email <email>.`);
|
|
123
|
+
return 1;
|
|
107
124
|
}
|
|
125
|
+
if (!(await confirmBinding(candidate, ctx))) return 1;
|
|
126
|
+
writeIdentity(projectDir, candidate);
|
|
127
|
+
log(`kj identity: declared gh ${candidate.gh_user} / git ${candidate.git_email} → .karajan/identity.local.yml (ignored by git).`);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
108
130
|
|
|
131
|
+
function showAction({ projectDir, log, deps }) {
|
|
132
|
+
const effective = effectiveIdentity(projectDir, deps);
|
|
109
133
|
const declared = readIdentity(projectDir);
|
|
110
134
|
const result = compareIdentity(declared, effective);
|
|
111
135
|
if (!declared) {
|
|
@@ -118,3 +142,12 @@ export async function identityCommand({ action = "show", config, kjVersion = "un
|
|
|
118
142
|
for (const m of result.mismatches) log(` → ${m.remedy}`);
|
|
119
143
|
return result.ok ? 0 : 2;
|
|
120
144
|
}
|
|
145
|
+
|
|
146
|
+
// Any other action shows the identity, as before.
|
|
147
|
+
const ACTIONS = { "enroll-phone": enrollPhoneAction, "phone-invite": phoneInviteAction, set: setAction };
|
|
148
|
+
|
|
149
|
+
export async function identityCommand({ action = "show", config, kjVersion = "unknown", flags = {}, deps = {} }) {
|
|
150
|
+
const ctx = { projectDir: config?.projectDir || process.cwd(), log: deps.log || console.log, kjVersion, flags, deps };
|
|
151
|
+
const run = Object.hasOwn(ACTIONS, action) ? ACTIONS[action] : showAction;
|
|
152
|
+
return run(ctx);
|
|
153
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-1219 (ADR 0021, step 15d): kj init installs Karajan's kj-* commands into atril's
|
|
3
|
+
* store under `.karajan/`, on demand (ADR 0021's default answer), instead of copying them into
|
|
4
|
+
* `.claude/commands/`. With rulesync, `atril sync` takes the bridge and the router rule to every
|
|
5
|
+
* CLI; without it nothing persistent is installed, and kj says how to get there.
|
|
6
|
+
*/
|
|
7
|
+
import { readdirSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { generate, install, sync } from "@karajan-family/atril";
|
|
10
|
+
import { ATRIL_ROOT } from "../skills/atril-skills.js";
|
|
11
|
+
import { detectRulesync } from "../utils/rulesync.js";
|
|
12
|
+
import { getTemplatesRoot } from "../utils/templates-root.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {{ projectDir: string, logger: { info: Function, warn: Function }, env?: object, offerRulesync?: boolean }} opts
|
|
16
|
+
* offerRulesync false: the user declined rulesync (--no-rulesync), so kj does not suggest it again
|
|
17
|
+
* @returns {{ installed: number, synced: boolean }}
|
|
18
|
+
*/
|
|
19
|
+
export function installKarajanCommands({ projectDir, logger, env = process.env, offerRulesync = true }) {
|
|
20
|
+
const catalog = join(getTemplatesRoot(), "atril", "commands");
|
|
21
|
+
const atril = { root: ATRIL_ROOT, projectDir, env };
|
|
22
|
+
let installed = 0;
|
|
23
|
+
let unchanged = 0;
|
|
24
|
+
for (const file of readdirSync(catalog).filter((name) => name.endsWith(".md")).toSorted()) {
|
|
25
|
+
try {
|
|
26
|
+
// install() answers "installed" or "unchanged" (same content already there) and throws on a refusal.
|
|
27
|
+
const { status } = install(join(catalog, file), { ...atril, kind: "command" });
|
|
28
|
+
if (status === "installed") installed += 1;
|
|
29
|
+
else unchanged += 1;
|
|
30
|
+
} catch (err) {
|
|
31
|
+
logger.warn(`kj: ${file} was not installed in ${ATRIL_ROOT}: ${err.message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
logger.info(`kj: ${installed} Karajan command(s) installed in ${ATRIL_ROOT}/commands, on demand (${unchanged} already there)`);
|
|
35
|
+
if (!detectRulesync(projectDir)) {
|
|
36
|
+
if (offerRulesync) logger.info(`kj: to reach every CLI through /atril, run rulesync init and then atril sync --root ${ATRIL_ROOT}`);
|
|
37
|
+
return { installed, synced: false };
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
for (const { action, path } of sync(atril).filter((step) => step.action !== "unchanged")) logger.info(`kj: atril ${action} .rulesync/${path}`);
|
|
41
|
+
if (!generate(atril)) logger.info("kj: run rulesync generate to reach your agents, or declare rulesync.generate: true in .karajan/atril.json");
|
|
42
|
+
} catch (err) {
|
|
43
|
+
logger.warn(`kj: atril sync failed (${err.message}); fix it and run atril sync --root ${ATRIL_ROOT}`);
|
|
44
|
+
return { installed, synced: false };
|
|
45
|
+
}
|
|
46
|
+
return { installed, synced: true };
|
|
47
|
+
}
|
package/src/commands/init.js
CHANGED
|
@@ -7,7 +7,7 @@ import { sonarUp, checkVmMaxMapCount } from "../sonar/manager.js";
|
|
|
7
7
|
import { ollamaUp, waitForOllamaReady, normalizeOllamaConfig } from "../rag/ollama-manager.js";
|
|
8
8
|
import { checkOllamaCapability, pullOllamaModel } from "../rag/ollama-capability.js";
|
|
9
9
|
import { exists, ensureDir } from "../utils/fs.js";
|
|
10
|
-
import { ensureContractBlockPresent } from "../review/gate-gitignore.js";
|
|
10
|
+
import { ensureContractBlockPresent, ensureGateTrackable } from "../review/gate-gitignore.js";
|
|
11
11
|
import { getKarajanHome } from "../utils/paths.js";
|
|
12
12
|
import { getTemplatesRoot } from "../utils/templates-root.js";
|
|
13
13
|
import { detectAvailableAgents } from "../utils/agent-detect.js";
|
|
@@ -28,6 +28,7 @@ import { registerQmdCollections } from "../utils/qmd-collection.js";
|
|
|
28
28
|
import { isGitRepo } from "../harden/harden-engine.js";
|
|
29
29
|
import { isPhoneEnrolled } from "../harden/phone-sign.js";
|
|
30
30
|
import { identityCommand } from "./identity.js";
|
|
31
|
+
import { installKarajanCommands } from "./init-atril.js";
|
|
31
32
|
import { hardenCommand } from "./harden.js";
|
|
32
33
|
import { contractChanges } from "../environment/contract-commit.js";
|
|
33
34
|
import { detectProjectStack } from "../utils/stack-detect.js";
|
|
@@ -680,54 +681,22 @@ async function bootstrapOllama({ flags, config, logger, interactive }) {
|
|
|
680
681
|
}
|
|
681
682
|
}
|
|
682
683
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
const commandsDir = path.join(projectDir, ".claude", "commands");
|
|
686
|
-
const skillsTemplateDir = path.join(getTemplatesRoot(), "skills");
|
|
687
|
-
|
|
684
|
+
// KJC-TSK-1219 (ADR 0021, step 15d): Karajan's commands go to atril's store, not .claude/commands.
|
|
685
|
+
async function installSkills(logger, interactive, offerRulesync) {
|
|
688
686
|
let doInstall = true;
|
|
689
687
|
if (interactive) {
|
|
690
688
|
const wizard = createWizard();
|
|
691
689
|
try {
|
|
692
|
-
doInstall = await wizard.confirm("Install Karajan
|
|
690
|
+
doInstall = await wizard.confirm("Install Karajan's commands (kj-code, kj-review, kj-audit...) into atril's store, on demand?", true);
|
|
693
691
|
} finally {
|
|
694
692
|
wizard.close();
|
|
695
693
|
}
|
|
696
694
|
}
|
|
697
|
-
|
|
698
695
|
if (!doInstall) {
|
|
699
|
-
logger.info("
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
await ensureDir(commandsDir);
|
|
704
|
-
|
|
705
|
-
let installed = 0;
|
|
706
|
-
try {
|
|
707
|
-
const files = await fs.readdir(skillsTemplateDir);
|
|
708
|
-
for (const file of files) {
|
|
709
|
-
if (!file.endsWith(".md")) continue;
|
|
710
|
-
const src = path.join(skillsTemplateDir, file);
|
|
711
|
-
const dest = path.join(commandsDir, file);
|
|
712
|
-
if (await exists(dest)) {
|
|
713
|
-
logger.info(` ${file} already exists — skipping`);
|
|
714
|
-
continue;
|
|
715
|
-
}
|
|
716
|
-
const content = await fs.readFile(src, "utf8");
|
|
717
|
-
await fs.writeFile(dest, content, "utf8");
|
|
718
|
-
installed += 1;
|
|
719
|
-
}
|
|
720
|
-
} catch (err) {
|
|
721
|
-
logger.warn(`Could not install skills: ${err.message}`);
|
|
696
|
+
logger.info("Karajan's commands skipped.");
|
|
722
697
|
return;
|
|
723
698
|
}
|
|
724
|
-
|
|
725
|
-
if (installed > 0) {
|
|
726
|
-
logger.info(`Installed ${installed} Karajan skill(s) in .claude/commands/`);
|
|
727
|
-
logger.info("Available as slash commands: /kj-run, /kj-code, /kj-review, /kj-test, /kj-security, /kj-discover, /kj-architect, /kj-sonar, /kj-rag-query");
|
|
728
|
-
} else {
|
|
729
|
-
logger.info("All skills already installed.");
|
|
730
|
-
}
|
|
699
|
+
installKarajanCommands({ projectDir: process.cwd(), logger, offerRulesync });
|
|
731
700
|
}
|
|
732
701
|
|
|
733
702
|
const GITIGNORE_ENTRIES = [
|
|
@@ -764,6 +733,8 @@ async function ensureGitignoreEntries(projectDir, logger) {
|
|
|
764
733
|
// review-gate/hooks/adrs the whole team inherits (KJC-TSK-0646).
|
|
765
734
|
const res = await ensureContractBlockPresent(projectDir);
|
|
766
735
|
if (res.changed) logger.info("Added the .karajan contract block to .gitignore (verdicts stay local; gate/hooks/adrs tracked)");
|
|
736
|
+
// ADR 0032: an existing project gets the block completed with what the team now shares.
|
|
737
|
+
else if ((await ensureGateTrackable(projectDir)).changed) logger.info("Completed the .karajan contract block in .gitignore with the team's shared state (ADR 0032)");
|
|
767
738
|
}
|
|
768
739
|
|
|
769
740
|
/**
|
|
@@ -876,7 +847,7 @@ export async function initCommand({ logger, flags = {} }) {
|
|
|
876
847
|
await ensureReviewRules(reviewRulesPath, logger);
|
|
877
848
|
await ensureCoderRules(coderRulesPath, logger);
|
|
878
849
|
await ensureGitignoreEntries(process.cwd(), logger);
|
|
879
|
-
await installSkills(logger, interactive);
|
|
850
|
+
await installSkills(logger, interactive, flags.rulesync !== false);
|
|
880
851
|
await bootstrapOllama({ flags, config, logger, interactive });
|
|
881
852
|
|
|
882
853
|
// Auto-detect project stack and preconfigure
|
|
@@ -1046,7 +1017,7 @@ export async function initCommand({ logger, flags = {} }) {
|
|
|
1046
1017
|
logger.info(hardened || skipHarden
|
|
1047
1018
|
? "✓ Karajan is set up in this project."
|
|
1048
1019
|
: "⚠ Karajan is set up EXCEPT the quality harness — without it there are no gates.");
|
|
1049
|
-
logger.info(
|
|
1020
|
+
logger.info(" Next: ask your agent for the work; it follows the method kj installed");
|
|
1050
1021
|
logger.info(" More: kj --help · kj doctor (check your setup)");
|
|
1051
1022
|
}
|
|
1052
1023
|
|
|
@@ -294,6 +294,74 @@ async function handleStandalone({ tool, standalone, manualUrl, dryRun, yes, logg
|
|
|
294
294
|
return { tool, action: "failed", via: "binary", error: r.error };
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
// Sonar: plan the container, then start it on a yes.
|
|
298
|
+
async function startSonar({ available, dryRun, yes, logger }) {
|
|
299
|
+
const result = await handleSonar({ available, dryRun });
|
|
300
|
+
if (result.action === "manual") logger.warn?.(`✗ sonar: ${result.reason}`);
|
|
301
|
+
else logger.info?.(`▸ sonar: ${result.command}`);
|
|
302
|
+
if (dryRun || result.action !== "ready") return result;
|
|
303
|
+
const proceed = yes || await promptYesNo(`Start SonarQube with: ${result.command}?`);
|
|
304
|
+
if (!proceed) return result;
|
|
305
|
+
const r = await runInstall(result.command);
|
|
306
|
+
result.action = r.ok ? "installed" : "failed";
|
|
307
|
+
if (!r.ok) result.error = r.error;
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function handleOptimizer({ tool, dryRun, yes, logger }) {
|
|
312
|
+
if (dryRun) {
|
|
313
|
+
logger.info?.(`▸ ${tool}: would install (init installer)`);
|
|
314
|
+
return { tool, action: "planned", command: `kj install-tools --only ${tool}` };
|
|
315
|
+
}
|
|
316
|
+
const proceed = yes || await promptYesNo(`Install ${tool}?`);
|
|
317
|
+
if (!proceed) return { tool, action: "declined" };
|
|
318
|
+
const install = await OPTIMIZER_INSTALLERS[tool]();
|
|
319
|
+
const r = await install(logger);
|
|
320
|
+
return r.ok ? { tool, action: "installed", version: r.version } : { tool, action: "failed", error: r.error };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// No package manager matched: try a standalone route (static binary for
|
|
324
|
+
// osv-scanner, a concrete command for semgrep) before giving up.
|
|
325
|
+
async function handleNoManager({ tool, hint, available, dryRun, yes, logger }) {
|
|
326
|
+
const standalone = resolveStandalone(tool, available);
|
|
327
|
+
if (standalone) return handleStandalone({ tool, standalone, manualUrl: hint.manualUrl, dryRun, yes, logger });
|
|
328
|
+
logger.warn?.(`✗ ${tool}: no automatic install route here. Run \`kj doctor\` for options, or install manually: ${hint.manualUrl}`);
|
|
329
|
+
return { tool, action: "manual", reason: "no compatible package manager found", manualUrl: hint.manualUrl, suggested: hint.command };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// semgrep / osv-scanner / lighthouse — package-manager driven.
|
|
333
|
+
async function handlePackageManaged(ctx) {
|
|
334
|
+
const { tool, available, dryRun, yes, logger } = ctx;
|
|
335
|
+
const hint = await getInstallHint(tool, available);
|
|
336
|
+
if (!hint.command || !hint.manager) return handleNoManager({ ...ctx, hint });
|
|
337
|
+
if (dryRun) {
|
|
338
|
+
logger.info?.(`▸ ${tool}: would run \`${hint.command}\` (manager: ${hint.manager})`);
|
|
339
|
+
return { tool, action: "dry-run", command: hint.command, manager: hint.manager };
|
|
340
|
+
}
|
|
341
|
+
const proceed = yes || await promptYesNo(`Install ${tool} with: ${hint.command}?`);
|
|
342
|
+
if (!proceed) {
|
|
343
|
+
logger.info?.(`⊘ ${tool}: declined`);
|
|
344
|
+
return { tool, action: "declined", command: hint.command };
|
|
345
|
+
}
|
|
346
|
+
logger.info?.(`▸ ${tool}: running \`${hint.command}\`...`);
|
|
347
|
+
const r = await runInstall(hint.command);
|
|
348
|
+
if (r.ok) {
|
|
349
|
+
logger.info?.(`✓ ${tool}: installed via ${hint.manager}`);
|
|
350
|
+
return { tool, action: "installed", command: hint.command, manager: hint.manager };
|
|
351
|
+
}
|
|
352
|
+
logger.warn?.(`✗ ${tool}: install failed (${r.error})`);
|
|
353
|
+
return { tool, action: "failed", command: hint.command, error: r.error };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Tools with their own route; any other one goes through the package manager.
|
|
357
|
+
const TOOL_HANDLERS = {
|
|
358
|
+
sonar: startSonar,
|
|
359
|
+
git: handleGit,
|
|
360
|
+
"agent-cli": handleAgentCli,
|
|
361
|
+
docker: handleDocker,
|
|
362
|
+
...Object.fromEntries(Object.keys(OPTIMIZER_INSTALLERS).map((tool) => [tool, handleOptimizer])),
|
|
363
|
+
};
|
|
364
|
+
|
|
297
365
|
/**
|
|
298
366
|
* Plan + (optionally execute) the install of every requested tool.
|
|
299
367
|
*
|
|
@@ -330,91 +398,8 @@ export async function installToolsCommand(opts = {}) {
|
|
|
330
398
|
continue;
|
|
331
399
|
}
|
|
332
400
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
results.push(result);
|
|
336
|
-
if (result.action === "manual") logger.warn?.(`✗ sonar: ${result.reason}`);
|
|
337
|
-
else logger.info?.(`▸ sonar: ${result.command}`);
|
|
338
|
-
if (!dryRun && result.action === "ready") {
|
|
339
|
-
const proceed = yes || await promptYesNo(`Start SonarQube with: ${result.command}?`);
|
|
340
|
-
if (proceed) {
|
|
341
|
-
const r = await runInstall(result.command);
|
|
342
|
-
result.action = r.ok ? "installed" : "failed";
|
|
343
|
-
if (!r.ok) result.error = r.error;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (OPTIMIZER_INSTALLERS[tool]) {
|
|
350
|
-
if (dryRun) {
|
|
351
|
-
results.push({ tool, action: "planned", command: `kj install-tools --only ${tool}` });
|
|
352
|
-
logger.info?.(`▸ ${tool}: would install (init installer)`);
|
|
353
|
-
continue;
|
|
354
|
-
}
|
|
355
|
-
const proceed = yes || await promptYesNo(`Install ${tool}?`);
|
|
356
|
-
if (!proceed) { results.push({ tool, action: "declined" }); continue; }
|
|
357
|
-
const install = await OPTIMIZER_INSTALLERS[tool]();
|
|
358
|
-
const r = await install(logger);
|
|
359
|
-
results.push(r.ok
|
|
360
|
-
? { tool, action: "installed", version: r.version }
|
|
361
|
-
: { tool, action: "failed", error: r.error });
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (tool === "git") {
|
|
366
|
-
const result = await handleGit({ available, dryRun, yes, logger });
|
|
367
|
-
results.push(result);
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (tool === "agent-cli") {
|
|
372
|
-
const result = await handleAgentCli({ available, dryRun, yes, logger });
|
|
373
|
-
results.push(result);
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (tool === "docker") {
|
|
378
|
-
const result = await handleDocker({ available, dryRun, yes, logger });
|
|
379
|
-
results.push(result);
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// semgrep / osv-scanner / lighthouse — package-manager driven.
|
|
384
|
-
const hint = await getInstallHint(tool, available);
|
|
385
|
-
if (!hint.command || !hint.manager) {
|
|
386
|
-
// No package manager matched — try a standalone route (static binary
|
|
387
|
-
// for osv-scanner, a concrete command for semgrep) before giving up.
|
|
388
|
-
const standalone = resolveStandalone(tool, available);
|
|
389
|
-
if (standalone) {
|
|
390
|
-
const result = await handleStandalone({ tool, standalone, manualUrl: hint.manualUrl, dryRun, yes, logger });
|
|
391
|
-
results.push(result);
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
394
|
-
results.push({ tool, action: "manual", reason: "no compatible package manager found", manualUrl: hint.manualUrl, suggested: hint.command });
|
|
395
|
-
logger.warn?.(`✗ ${tool}: no automatic install route here. Run \`kj doctor\` for options, or install manually: ${hint.manualUrl}`);
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
398
|
-
if (dryRun) {
|
|
399
|
-
results.push({ tool, action: "dry-run", command: hint.command, manager: hint.manager });
|
|
400
|
-
logger.info?.(`▸ ${tool}: would run \`${hint.command}\` (manager: ${hint.manager})`);
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
const proceed = yes || await promptYesNo(`Install ${tool} with: ${hint.command}?`);
|
|
404
|
-
if (!proceed) {
|
|
405
|
-
results.push({ tool, action: "declined", command: hint.command });
|
|
406
|
-
logger.info?.(`⊘ ${tool}: declined`);
|
|
407
|
-
continue;
|
|
408
|
-
}
|
|
409
|
-
logger.info?.(`▸ ${tool}: running \`${hint.command}\`...`);
|
|
410
|
-
const r = await runInstall(hint.command);
|
|
411
|
-
if (r.ok) {
|
|
412
|
-
results.push({ tool, action: "installed", command: hint.command, manager: hint.manager });
|
|
413
|
-
logger.info?.(`✓ ${tool}: installed via ${hint.manager}`);
|
|
414
|
-
} else {
|
|
415
|
-
results.push({ tool, action: "failed", command: hint.command, error: r.error });
|
|
416
|
-
logger.warn?.(`✗ ${tool}: install failed (${r.error})`);
|
|
417
|
-
}
|
|
401
|
+
const install = Object.hasOwn(TOOL_HANDLERS, tool) ? TOOL_HANDLERS[tool] : handlePackageManaged;
|
|
402
|
+
results.push(await install({ tool, available, dryRun, yes, logger }));
|
|
418
403
|
}
|
|
419
404
|
|
|
420
405
|
// KJC-TSK-0659 stop-on-sudo: anything that still needs the user's hands
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `kj main-guard` (KJC-TSK-1369, ADR 0037 layer 2): audits a push to the protected branch
|
|
3
|
+
* against GitHub and exits 1 with a markdown report when a merge skipped its required checks
|
|
4
|
+
* or the protection no longer enforces them on admins. The sealed workflow runs it on every
|
|
5
|
+
* push to main and posts the report as the alarm.
|
|
6
|
+
*
|
|
7
|
+
* Pull requests and check results are read with the token gh has (the workflow's GITHUB_TOKEN).
|
|
8
|
+
* Reading the branch protection needs Administration, so it uses MAIN_GUARD_PROTECTION_TOKEN,
|
|
9
|
+
* a read-only secret (the maintainer's choice, 2026-11-01).
|
|
10
|
+
*/
|
|
11
|
+
import { execFileSync } from "node:child_process";
|
|
12
|
+
|
|
13
|
+
import { auditMain } from "../guards/main-guard.js";
|
|
14
|
+
|
|
15
|
+
const NEW_BRANCH = /^0+$/;
|
|
16
|
+
|
|
17
|
+
const gitIn = (projectDir) => (args) => execFileSync("git", ["-C", projectDir, ...args], { encoding: "utf8" });
|
|
18
|
+
|
|
19
|
+
/** The commits the push put on the branch, newest first: its first-parent line. */
|
|
20
|
+
export function pushedCommits({ before, after, git }) {
|
|
21
|
+
if (!before || NEW_BRANCH.test(before)) return [after];
|
|
22
|
+
return git(["rev-list", "--first-parent", `${before}..${after}`]).split("\n").filter(Boolean);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** `gh api` as JSON; a token, when given, replaces gh's own for that call only. */
|
|
26
|
+
function ghJson(path, token) {
|
|
27
|
+
const env = token ? { ...process.env, GH_TOKEN: token } : process.env;
|
|
28
|
+
return JSON.parse(execFileSync("gh", ["api", path], { encoding: "utf8", env, stdio: ["ignore", "pipe", "pipe"] }));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** GitHub as main-guard's api. */
|
|
32
|
+
export function githubApi({ repo, branch, protectionToken }) {
|
|
33
|
+
return {
|
|
34
|
+
// The commit's pull list leaves merged_by out; a merged one is read in full to name who merged it.
|
|
35
|
+
pullsOf: async (sha) => ghJson(`repos/${repo}/commits/${sha}/pulls`).map((p) => (p.merged_at ? ghJson(`repos/${repo}/pulls/${p.number}`) : p)),
|
|
36
|
+
async checksOf(sha) {
|
|
37
|
+
const runs = ghJson(`repos/${repo}/commits/${sha}/check-runs?per_page=100`).check_runs ?? [];
|
|
38
|
+
const statuses = ghJson(`repos/${repo}/commits/${sha}/status`).statuses ?? [];
|
|
39
|
+
return [...runs.map((r) => ({ name: r.name, conclusion: r.conclusion })), ...statuses.map((s) => ({ name: s.context, conclusion: s.state }))];
|
|
40
|
+
},
|
|
41
|
+
async protection() {
|
|
42
|
+
if (!protectionToken) throw new Error("main-guard: MAIN_GUARD_PROTECTION_TOKEN is not set; the protection cannot be read");
|
|
43
|
+
try {
|
|
44
|
+
return ghJson(`repos/${repo}/branches/${encodeURIComponent(branch)}/protection`, protectionToken);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (/Branch not protected/i.test(`${err.stdout ?? ""}${err.stderr ?? ""}`)) return null;
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The alarm as markdown. */
|
|
54
|
+
export function renderReport(result, { repo, branch }) {
|
|
55
|
+
if (result.ok) return `main-guard: nothing reached ${branch} without its checks, and the protection of ${repo} is as declared.`;
|
|
56
|
+
const lines = [`## main-guard: ${repo}@${branch} needs a look`, ""];
|
|
57
|
+
for (const m of result.merges) {
|
|
58
|
+
lines.push(m.kind === "direct-push"
|
|
59
|
+
? `- \`${m.sha.slice(0, 7)}\`: direct push, ${m.detail}`
|
|
60
|
+
: `- \`${m.sha.slice(0, 7)}\`: PR #${m.pr} merged by @${m.mergedBy} without ${m.missing.join(", ")}`);
|
|
61
|
+
}
|
|
62
|
+
for (const change of result.protection) lines.push(`- protection: ${change}`);
|
|
63
|
+
return lines.join("\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @returns {Promise<number>} the exit code */
|
|
67
|
+
export async function runMainGuard({ repo, branch, before, after, require, git, audit = auditMain, api, print = console.log }) {
|
|
68
|
+
const commits = pushedCommits({ before, after, git });
|
|
69
|
+
const result = await audit({ branch, commits, requiredChecks: require, api });
|
|
70
|
+
print(renderReport(result, { repo, branch }));
|
|
71
|
+
return result.ok ? 0 : 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function mainGuardCommand({ flags, projectDir = process.cwd() }) {
|
|
75
|
+
const { repo, branch, before, after } = flags;
|
|
76
|
+
const api = githubApi({ repo, branch, protectionToken: process.env.MAIN_GUARD_PROTECTION_TOKEN });
|
|
77
|
+
process.exitCode = await runMainGuard({ repo, branch, before, after, require: flags.require, git: gitIn(projectDir), api });
|
|
78
|
+
}
|