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/scripts/dual-publish.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Uso:
|
|
8
8
|
* node scripts/dual-publish.mjs --pack-only # genera+verifica el tarball scoped
|
|
9
9
|
* node scripts/dual-publish.mjs --publish --otp=XXX # publica el scoped (tras publicar el legacy)
|
|
10
|
+
* node scripts/dual-publish.mjs --publish --provenance --no-verify # desde CI (ADR 0035), tras --pack-only en otro job
|
|
10
11
|
*
|
|
11
12
|
* El swap del name es EN SITIO con restauración en finally: un crash a mitad
|
|
12
13
|
* no deja el manifest renombrado. La verificación es el MISMO verify-pack
|
|
@@ -43,6 +44,22 @@ export async function withScopedName(repoRoot, fn) {
|
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
/** KJC-TSK-1359 (ADR 0035): npm publish arguments; from CI, provenance and no OTP. */
|
|
48
|
+
export function publishArgs(args) {
|
|
49
|
+
const otp = args.find((a) => a.startsWith("--otp="))?.slice(6);
|
|
50
|
+
return ["publish", "--ignore-scripts", ...(args.includes("--provenance") ? ["--provenance"] : []), ...(otp ? [`--otp=${otp}`] : [])];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* KJC-TSK-1359: the CI publish job holds the OIDC token and installs nothing, so it skips
|
|
55
|
+
* verify-pack (which installs the tarball and runs its scripts); the verify job ran --pack-only.
|
|
56
|
+
*/
|
|
57
|
+
export function shouldVerify(args) {
|
|
58
|
+
if (!args.includes("--no-verify")) return true;
|
|
59
|
+
if (!args.includes("--publish")) throw new Error("dual-publish: --no-verify only goes with --publish; --pack-only always verifies");
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
46
63
|
// argv[1] normalizado (catch de codex): node lo absolutiza hoy, pero el
|
|
47
64
|
// guard no debe depender de ese detalle de implementación.
|
|
48
65
|
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
|
|
@@ -50,13 +67,12 @@ if (isMain) {
|
|
|
50
67
|
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
51
68
|
const args = process.argv.slice(2);
|
|
52
69
|
const publish = args.includes("--publish");
|
|
53
|
-
const otp = args.find((a) => a.startsWith("--otp="))?.slice(6);
|
|
54
70
|
await withScopedName(repoRoot, async () => {
|
|
55
71
|
console.log(`dual-publish: manifest → ${SCOPED_NAME} (temporal)`);
|
|
56
72
|
// La E2E real: empaqueta el scoped, instálalo aislado y corre kj.
|
|
57
|
-
execFileSync("node", [join(repoRoot, "scripts", "verify-pack.mjs")], { cwd: repoRoot, stdio: "inherit" });
|
|
73
|
+
if (shouldVerify(args)) execFileSync("node", [join(repoRoot, "scripts", "verify-pack.mjs")], { cwd: repoRoot, stdio: "inherit" });
|
|
58
74
|
if (publish) {
|
|
59
|
-
const pubArgs =
|
|
75
|
+
const pubArgs = publishArgs(args);
|
|
60
76
|
console.log(`dual-publish: npm ${pubArgs.join(" ").replace(/--otp=\S+/, "--otp=***")}`);
|
|
61
77
|
execFileSync("npm", pubArgs, { cwd: repoRoot, stdio: "inherit" });
|
|
62
78
|
} else {
|
package/scripts/postinstall.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - Idempotent: safe to run many times.
|
|
8
8
|
* - Never fails hard (exits 0 even on error) so `npm install` is not blocked.
|
|
9
9
|
*/
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
10
11
|
import fs from "node:fs/promises";
|
|
11
12
|
import os from "node:os";
|
|
12
13
|
import path from "node:path";
|
|
@@ -38,10 +39,22 @@ const CLAUDE_JSON_PATH = path.join(os.homedir(), ".claude.json");
|
|
|
38
39
|
const isPackageDirHome = (home) => /node_modules[\\/]karajan-code[\\/]\.karajan[\\/]?$/.test(home)
|
|
39
40
|
|| path.resolve(home) === path.join(ROOT_DIR, ".karajan");
|
|
40
41
|
|
|
42
|
+
// KJC-BUG-0404: the .karajan of a git working tree is a project's own state (ADR 0032). Taken as
|
|
43
|
+
// the home, every project's home data (audits, stories, board) landed in that repo.
|
|
44
|
+
function isProjectKarajanDir(home) {
|
|
45
|
+
const dir = path.resolve(home);
|
|
46
|
+
if (path.basename(dir) !== ".karajan" || path.dirname(dir) === os.homedir()) return false;
|
|
47
|
+
if (!existsSync(path.join(path.dirname(dir), ".git"))) return false;
|
|
48
|
+
process.stderr.write(`karajan-mcp: ${dir} is a project's .karajan, not a Karajan home; ignoring it\n`);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const usableHome = (home) => Boolean(home) && !isPackageDirHome(home) && !isProjectKarajanDir(home);
|
|
53
|
+
|
|
41
54
|
/** The home a previous registration carried, if the user (not this script) set it. */
|
|
42
55
|
function homeFromExistingEntry(entry) {
|
|
43
56
|
const previous = entry?.env?.KARAJAN_HOME || entry?.env?.KJ_HOME;
|
|
44
|
-
return
|
|
57
|
+
return usableHome(previous) ? previous : null;
|
|
45
58
|
}
|
|
46
59
|
|
|
47
60
|
/**
|
|
@@ -58,7 +71,7 @@ async function resolveKjHome(existingEntry) {
|
|
|
58
71
|
const names = Object.keys(registry.instances || {});
|
|
59
72
|
if (names.length > 0) {
|
|
60
73
|
const first = registry.instances[names.includes("default") ? "default" : names[0]];
|
|
61
|
-
if (first?.kjHome) return first.kjHome;
|
|
74
|
+
if (usableHome(first?.kjHome)) return first.kjHome;
|
|
62
75
|
}
|
|
63
76
|
} catch {
|
|
64
77
|
// No registry yet — use default
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP smoke for verify-pack (KJC-TSK-0844) — the karajan-mcp server of the
|
|
3
3
|
* INSTALLED tarball is started over stdio with the SDK client and exercised
|
|
4
|
-
* with
|
|
4
|
+
* with kj_config and kj_code, no LLM involved; kj_run, retired in 5.0
|
|
5
|
+
* (KJC-TSK-1318), must answer with its retirement notice. The MCP surface
|
|
5
6
|
* was only proved by tests with every seam mocked and dogfooded by no
|
|
6
7
|
* development session; KJC-BUG-0175 (taskFile promised, never read) and
|
|
7
8
|
* KJC-BUG-0176 (kjHome declared, ignored) both shipped through that gap.
|
|
@@ -17,7 +18,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
|
17
18
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
18
19
|
|
|
19
20
|
const FATAL = [/Missing required field/i, /Config file not found/i, /taskFile read failed/i, /Unknown tool/i];
|
|
20
|
-
const TOOLS = ["
|
|
21
|
+
const TOOLS = ["kj_config", "kj_code"];
|
|
21
22
|
|
|
22
23
|
/** The text of a tool answer, and whether it is a failure of the contract (not of the environment). */
|
|
23
24
|
export function classifyAnswer(tool, text) {
|
|
@@ -58,15 +59,18 @@ export async function mcpSmoke({ serverPath, kjHome, projectDir, taskFile, env =
|
|
|
58
59
|
const listed = new Set((await client.listTools()).tools.map((t) => t.name));
|
|
59
60
|
for (const t of TOOLS) if (!listed.has(t)) findings.push(`${t} is not listed by the server`);
|
|
60
61
|
if (findings.length > 0) return { ok: false, findings, answers };
|
|
61
|
-
// A project that never ran has no run log: that answer is the contract, not a refusal.
|
|
62
|
-
const status = await call("kj_status", { kjHome, projectDir, lines: 5 });
|
|
63
|
-
if (status.gated && !/no active run log/i.test(status.reason)) findings.push(`kj_status refused: ${status.reason}`);
|
|
64
62
|
const config = await call("kj_config", { kjHome, json: true });
|
|
65
63
|
if (config.gated) findings.push(`kj_config refused: ${config.reason}`);
|
|
66
64
|
// The task travels ONLY as a file: the handler must read it before any
|
|
67
|
-
// gate. The
|
|
65
|
+
// gate. The coder is a name no machine has, so the gate stops the call
|
|
68
66
|
// before a single LLM token, on a laptop with real agents as much as in CI.
|
|
69
|
-
await call("
|
|
67
|
+
const code = await call("kj_code", { kjHome, projectDir, taskFile, coder: "kj-verify-stub" });
|
|
68
|
+
// Its gate: the bootstrap on a runner with no agent CLI (CI), the unknown coder anywhere else.
|
|
69
|
+
// Both come after the taskFile is read; an answer that is not a refusal ran something.
|
|
70
|
+
if (!code.gated || !/Unsupported agent: kj-verify-stub|BOOTSTRAP FAILED/.test(code.reason)) findings.push(`kj_code did not stop at its gate with the stub coder: ${code.reason ?? "it answered ok"}`);
|
|
71
|
+
// KJC-TSK-1318: a retired tool says so and points at the migration guide.
|
|
72
|
+
const retired = await call("kj_run", { kjHome, projectDir, task: "x" });
|
|
73
|
+
if (!/^kj run was retired in 5\.0/.test(retired.reason ?? "")) findings.push(`kj_run did not answer with its retirement notice: ${retired.reason}`);
|
|
70
74
|
} catch (err) {
|
|
71
75
|
findings.push(`MCP session failed: ${err.message}`);
|
|
72
76
|
} finally {
|
package/scripts/verify-pack.mjs
CHANGED
|
@@ -253,10 +253,10 @@ try {
|
|
|
253
253
|
// (a symlinked virtual store), so it can break resolution of the bundled
|
|
254
254
|
// karajan-core the way npm packaging breakage did before (KJC-BUG-0082/0086).
|
|
255
255
|
// Verify a pnpm install resolves the JS tree and `kj --version` boots — every
|
|
256
|
-
// release catches a pnpm packaging regression. (pnpm also blocks
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
256
|
+
// release catches a pnpm packaging regression. (pnpm also blocks node-pty's
|
|
257
|
+
// native build by default, so the HU Board terminal needs `pnpm approve-builds`;
|
|
258
|
+
// that's surfaced as a doctor warning, KJC-BUG-0092 / KJC-TSK-1353 — not a gate
|
|
259
|
+
// failure, since it's inherent to pnpm.) Auto-skips without pnpm.
|
|
260
260
|
// KJC-BUG-0135: `npm run` leaks npm_config_* into the child env, and pnpm
|
|
261
261
|
// maps those variables onto its own config — which can re-anchor it to THIS
|
|
262
262
|
// repo (7-aug incident: the repo's node_modules converted to pnpm layout,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// KJC-TSK-0472 — Per-project audit history (sqlite at .karajan/audit-history.db).
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import fs from "node:fs";
|
|
4
|
-
|
|
4
|
+
// KJC-TSK-0618 (SQL-D): karajan-core's adapter over node:sqlite, built into Node.
|
|
5
|
+
import { openDatabase } from "karajan-core/db";
|
|
5
6
|
|
|
6
7
|
const SCHEMA_VERSION = 1;
|
|
7
8
|
|
|
@@ -12,7 +13,7 @@ export function getAuditHistoryDbPath(projectDir) {
|
|
|
12
13
|
export function openAuditHistoryDb(projectDir) {
|
|
13
14
|
const dbPath = getAuditHistoryDbPath(projectDir);
|
|
14
15
|
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
15
|
-
const db =
|
|
16
|
+
const db = openDatabase(dbPath);
|
|
16
17
|
db.pragma("journal_mode = WAL");
|
|
17
18
|
migrate(db);
|
|
18
19
|
return db;
|
|
@@ -93,6 +93,68 @@ function resolveKnipBin() {
|
|
|
93
93
|
return path.join(knipRoot, "bin", "knip.js");
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
// The ephemeral knip config for the entrypoints kj can vouch for. A project with
|
|
97
|
+
// its OWN knip config already decided — it is respected. Declaring is
|
|
98
|
+
// best-effort: on any error the default scan still runs.
|
|
99
|
+
async function declareEntrypoints(projectDir, config) {
|
|
100
|
+
try {
|
|
101
|
+
const eps = await resolveProjectEntrypoints(projectDir, config);
|
|
102
|
+
if (eps && !(await hasOwnKnipConfig(projectDir))) {
|
|
103
|
+
const cfgPath = path.join(await fs.mkdtemp(path.join(os.tmpdir(), "kj-knip-")), "knip.json");
|
|
104
|
+
await fs.writeFile(cfgPath, JSON.stringify(buildEphemeralKnipConfig(eps)));
|
|
105
|
+
return { configArgs: ["--config", cfgPath], declaredEntrypoints: eps };
|
|
106
|
+
}
|
|
107
|
+
} catch { /* declaring is best-effort; the default scan still runs */ }
|
|
108
|
+
return { configArgs: [], declaredEntrypoints: null };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function runKnip(args, projectDir, logger) {
|
|
112
|
+
try {
|
|
113
|
+
const { stdout } = await execFileAsync(
|
|
114
|
+
process.execPath,
|
|
115
|
+
args,
|
|
116
|
+
{ cwd: projectDir, timeout: SCAN_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 },
|
|
117
|
+
);
|
|
118
|
+
return { raw: stdout };
|
|
119
|
+
} catch (err) {
|
|
120
|
+
// knip exits non-zero when issues are found; with --no-exit-code that
|
|
121
|
+
// should not happen, but old configs might still trigger it. If the
|
|
122
|
+
// process produced JSON on stdout we still try to parse it.
|
|
123
|
+
const raw = err?.stdout || "";
|
|
124
|
+
if (raw) return { raw };
|
|
125
|
+
logger?.warn?.(`audit/dead-exports: knip failed (${err?.message || err})`);
|
|
126
|
+
return { failure: { available: false, reason: `knip run failed: ${err?.shortMessage || err?.message || err}` } };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// One knip file entry → its unused exports, then its unused types.
|
|
131
|
+
function deadExportIssuesOf(fi) {
|
|
132
|
+
const file = fi.file || fi.filePath || "";
|
|
133
|
+
if (!file) return [];
|
|
134
|
+
const exportIssues = (fi.exports || []).map((ex) => ({
|
|
135
|
+
tool: "knip",
|
|
136
|
+
rule: "unused-exports",
|
|
137
|
+
component: file,
|
|
138
|
+
path: file,
|
|
139
|
+
line: ex.line || 1,
|
|
140
|
+
col: ex.col,
|
|
141
|
+
name: ex.name,
|
|
142
|
+
severity: "MINOR",
|
|
143
|
+
message: `Unused export \`${ex.name}\` in ${file}`,
|
|
144
|
+
}));
|
|
145
|
+
const typeIssues = (fi.types || []).map((t) => ({
|
|
146
|
+
tool: "knip",
|
|
147
|
+
rule: "unused-types",
|
|
148
|
+
component: file,
|
|
149
|
+
path: file,
|
|
150
|
+
line: t.line || 1,
|
|
151
|
+
name: t.name,
|
|
152
|
+
severity: "MINOR",
|
|
153
|
+
message: `Unused type \`${t.name}\` in ${file}`,
|
|
154
|
+
}));
|
|
155
|
+
return [...exportIssues, ...typeIssues];
|
|
156
|
+
}
|
|
157
|
+
|
|
96
158
|
/**
|
|
97
159
|
* Collect dead-exports findings for a project.
|
|
98
160
|
*
|
|
@@ -116,36 +178,12 @@ export async function collectDeadExports(projectDir, stack, config = {}, logger
|
|
|
116
178
|
}
|
|
117
179
|
|
|
118
180
|
const args = [knipBin, "--reporter", "json", "--no-progress", "--no-exit-code"];
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const eps = await resolveProjectEntrypoints(projectDir, config);
|
|
122
|
-
// a project with its OWN knip config already decided — it is respected
|
|
123
|
-
if (eps && !(await hasOwnKnipConfig(projectDir))) {
|
|
124
|
-
const cfgPath = path.join(await fs.mkdtemp(path.join(os.tmpdir(), "kj-knip-")), "knip.json");
|
|
125
|
-
await fs.writeFile(cfgPath, JSON.stringify(buildEphemeralKnipConfig(eps)));
|
|
126
|
-
args.push("--config", cfgPath);
|
|
127
|
-
declaredEntrypoints = eps;
|
|
128
|
-
}
|
|
129
|
-
} catch { /* declaring is best-effort; the default scan still runs */ }
|
|
181
|
+
const { configArgs, declaredEntrypoints } = await declareEntrypoints(projectDir, config);
|
|
182
|
+
args.push(...configArgs);
|
|
130
183
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
process.execPath,
|
|
135
|
-
args,
|
|
136
|
-
{ cwd: projectDir, timeout: SCAN_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 },
|
|
137
|
-
);
|
|
138
|
-
raw = stdout;
|
|
139
|
-
} catch (err) {
|
|
140
|
-
// knip exits non-zero when issues are found; with --no-exit-code that
|
|
141
|
-
// should not happen, but old configs might still trigger it. If the
|
|
142
|
-
// process produced JSON on stdout we still try to parse it.
|
|
143
|
-
raw = err?.stdout || "";
|
|
144
|
-
if (!raw) {
|
|
145
|
-
logger?.warn?.(`audit/dead-exports: knip failed (${err?.message || err})`);
|
|
146
|
-
return { available: false, reason: `knip run failed: ${err?.shortMessage || err?.message || err}` };
|
|
147
|
-
}
|
|
148
|
-
}
|
|
184
|
+
const run = await runKnip(args, projectDir, logger);
|
|
185
|
+
if (run.failure) return run.failure;
|
|
186
|
+
const { raw } = run;
|
|
149
187
|
|
|
150
188
|
let report;
|
|
151
189
|
try {
|
|
@@ -158,36 +196,7 @@ export async function collectDeadExports(projectDir, stack, config = {}, logger
|
|
|
158
196
|
const unusedFiles = Array.isArray(report.files) ? report.files : [];
|
|
159
197
|
const fileIssues = Array.isArray(report.issues) ? report.issues : [];
|
|
160
198
|
|
|
161
|
-
const deadExportIssues =
|
|
162
|
-
for (const fi of fileIssues) {
|
|
163
|
-
const file = fi.file || fi.filePath || "";
|
|
164
|
-
if (!file) continue;
|
|
165
|
-
for (const ex of fi.exports || []) {
|
|
166
|
-
deadExportIssues.push({
|
|
167
|
-
tool: "knip",
|
|
168
|
-
rule: "unused-exports",
|
|
169
|
-
component: file,
|
|
170
|
-
path: file,
|
|
171
|
-
line: ex.line || 1,
|
|
172
|
-
col: ex.col,
|
|
173
|
-
name: ex.name,
|
|
174
|
-
severity: "MINOR",
|
|
175
|
-
message: `Unused export \`${ex.name}\` in ${file}`,
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
for (const t of fi.types || []) {
|
|
179
|
-
deadExportIssues.push({
|
|
180
|
-
tool: "knip",
|
|
181
|
-
rule: "unused-types",
|
|
182
|
-
component: file,
|
|
183
|
-
path: file,
|
|
184
|
-
line: t.line || 1,
|
|
185
|
-
name: t.name,
|
|
186
|
-
severity: "MINOR",
|
|
187
|
-
message: `Unused type \`${t.name}\` in ${file}`,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
}
|
|
199
|
+
const deadExportIssues = fileIssues.flatMap(deadExportIssuesOf);
|
|
191
200
|
|
|
192
201
|
const unusedFileIssues = unusedFiles.map((f) => ({
|
|
193
202
|
tool: "knip",
|
|
@@ -40,6 +40,7 @@ export function formatDeterministicSummary(ctx) {
|
|
|
40
40
|
if (ctx.osvFindings) lines.push(...formatOsvBlock(ctx.osvFindings));
|
|
41
41
|
if (ctx.semgrepFindings) lines.push(...formatSemgrepBlock(ctx.semgrepFindings));
|
|
42
42
|
if (ctx.infraFindings) lines.push(...formatInfraBlock(ctx.infraFindings));
|
|
43
|
+
if (ctx.npmSignatures) lines.push(...formatNpmSignaturesBlock(ctx.npmSignatures));
|
|
43
44
|
if (ctx.circularDeps) lines.push(...formatCircularDepsBlock(ctx.circularDeps));
|
|
44
45
|
if (ctx.deadExports) lines.push(...formatDeadExportsBlock(ctx.deadExports));
|
|
45
46
|
if (ctx.injectionFindings) lines.push(...formatInjectionBlock(ctx.injectionFindings));
|
|
@@ -159,6 +160,22 @@ function formatSemgrepBlock(semgrepFindings) {
|
|
|
159
160
|
|
|
160
161
|
// INF-C (KJC-TSK-0760): checkov misconfigs — same shape as the semgrep
|
|
161
162
|
// block; a not-applicable repo (no infra markers) declares it in one line.
|
|
163
|
+
// KJC-TSK-1253: an invalid signature is a tampered package; a missing one, a package the registry cannot vouch for.
|
|
164
|
+
function formatNpmSignaturesBlock(sigs) {
|
|
165
|
+
if (!sigs.available) return ["### npm signatures", `- Status: not available — ${sigs.reason}`, ""];
|
|
166
|
+
const lines = ["### npm signatures"];
|
|
167
|
+
const list = (title, pkgs) => {
|
|
168
|
+
if (!pkgs.length) return;
|
|
169
|
+
lines.push(` - ${title} (${pkgs.length}):`);
|
|
170
|
+
for (const p of pkgs.slice(0, MAX_SAMPLE_SEMGREP_PER_SEVERITY * 3)) lines.push(` - ${p.name}@${p.version}`);
|
|
171
|
+
};
|
|
172
|
+
if (!sigs.invalid.length && !sigs.missing.length) lines.push("- Every installed dependency has a valid registry signature");
|
|
173
|
+
list("Invalid signature", sigs.invalid);
|
|
174
|
+
list("No signature", sigs.missing);
|
|
175
|
+
lines.push("");
|
|
176
|
+
return lines;
|
|
177
|
+
}
|
|
178
|
+
|
|
162
179
|
function formatInfraBlock(infraFindings) {
|
|
163
180
|
if (!infraFindings.available) {
|
|
164
181
|
return ["### Infra misconfigurations (checkov)", `- Status: not available — ${infraFindings.reason || "checkov not installed"}`, ""];
|
|
@@ -350,17 +367,20 @@ function formatWebperfBlock(webperf) {
|
|
|
350
367
|
*/
|
|
351
368
|
export function deterministicContextHasFindings(ctx) {
|
|
352
369
|
if (!ctx) return false;
|
|
353
|
-
|
|
354
|
-
if (ctx.basalCost?.unusedDependencies?.unused?.length > 0) return true;
|
|
355
|
-
if (ctx.sonarFindings?.available && (ctx.sonarFindings.total ?? 0) > 0) return true;
|
|
356
|
-
if (ctx.sonarFindings?.qualityGate?.status === "ERROR") return true;
|
|
357
|
-
if (ctx.osvFindings?.available && (ctx.osvFindings.total ?? 0) > 0) return true;
|
|
358
|
-
if (ctx.semgrepFindings?.available && (ctx.semgrepFindings.total ?? 0) > 0) return true;
|
|
359
|
-
if (ctx.infraFindings?.available && (ctx.infraFindings.total ?? 0) > 0) return true;
|
|
360
|
-
if (ctx.circularDeps?.available && (ctx.circularDeps.total ?? 0) > 0) return true;
|
|
361
|
-
if (ctx.deadExports?.available && (ctx.deadExports.total ?? 0) > 0) return true;
|
|
362
|
-
if (ctx.growthDelta && (Math.abs(ctx.growthDelta.lines || 0) > 100 || Math.abs(ctx.growthDelta.deps || 0) > 0)) return true;
|
|
363
|
-
if (ctx.aiSlop?.available && (ctx.aiSlop.total ?? 0) > 0) return true;
|
|
364
|
-
if (ctx.envKeys?.available && ((ctx.envKeys.divergent?.length ?? 0) + (ctx.envKeys.incomplete?.length ?? 0)) > 0) return true;
|
|
365
|
-
return false;
|
|
370
|
+
return FINDING_SIGNALS.some((signal) => signal(ctx));
|
|
366
371
|
}
|
|
372
|
+
|
|
373
|
+
// Sections that report a `total` once collected.
|
|
374
|
+
const COUNTED_SECTIONS = ["sonarFindings", "osvFindings", "semgrepFindings", "infraFindings", "circularDeps", "deadExports", "aiSlop"];
|
|
375
|
+
const hasCount = (section) => Boolean(section?.available) && (section.total ?? 0) > 0;
|
|
376
|
+
const grewNoticeably = (delta) => Boolean(delta) && (Math.abs(delta.lines || 0) > 100 || Math.abs(delta.deps || 0) > 0);
|
|
377
|
+
const envKeysDiverge = (envKeys) => Boolean(envKeys?.available) && ((envKeys.divergent?.length ?? 0) + (envKeys.incomplete?.length ?? 0)) > 0;
|
|
378
|
+
|
|
379
|
+
const FINDING_SIGNALS = [
|
|
380
|
+
(ctx) => ctx.basalCost?.deadExports?.length > 0,
|
|
381
|
+
(ctx) => ctx.basalCost?.unusedDependencies?.unused?.length > 0,
|
|
382
|
+
(ctx) => ctx.sonarFindings?.qualityGate?.status === "ERROR",
|
|
383
|
+
(ctx) => COUNTED_SECTIONS.some((key) => hasCount(ctx[key])),
|
|
384
|
+
(ctx) => grewNoticeably(ctx.growthDelta),
|
|
385
|
+
(ctx) => envKeysDiverge(ctx.envKeys),
|
|
386
|
+
];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-1253: npm registry signatures and provenance of the installed dependencies, for
|
|
3
|
+
* `kj audit` and `kj audit --security`. `npm audit signatures` checks every installed package
|
|
4
|
+
* against the registry's signing keys (and its Sigstore provenance when published with it):
|
|
5
|
+
* an invalid signature means a tampered package, a missing one a package the registry cannot
|
|
6
|
+
* vouch for. Same best-effort shape as osv-findings: when it does not apply or cannot run, the
|
|
7
|
+
* section says why and the audit goes on.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { runGoverned } from "../utils/tool-governor.js";
|
|
12
|
+
|
|
13
|
+
const TIMEOUT_MS = 60_000;
|
|
14
|
+
const pick = (list) => (Array.isArray(list) ? list.map((p) => ({ name: p?.name ?? "?", version: p?.version ?? "?" })) : []);
|
|
15
|
+
|
|
16
|
+
/** @returns {Promise<{available: boolean, reason?: string, invalid?: {name, version}[], missing?: {name, version}[]}>} */
|
|
17
|
+
export async function collectNpmSignatures(projectDir, logger = null) {
|
|
18
|
+
if (!existsSync(join(projectDir, "package-lock.json")) || !existsSync(join(projectDir, "node_modules"))) {
|
|
19
|
+
return { available: false, reason: "not an installed npm project (needs package-lock.json and node_modules)" };
|
|
20
|
+
}
|
|
21
|
+
// Same kill-switch as the other scanners: never from the unit suite without the opt-in.
|
|
22
|
+
if (process.env.VITEST && process.env.KJ_ALLOW_REAL_SCANS !== "1") {
|
|
23
|
+
return { available: false, reason: "real scans disabled under the test runner (set KJ_ALLOW_REAL_SCANS=1 to opt in)" };
|
|
24
|
+
}
|
|
25
|
+
let stdout;
|
|
26
|
+
try {
|
|
27
|
+
({ stdout } = await runGoverned("npm", ["audit", "signatures", "--json"], { cwd: projectDir, timeout: TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 }));
|
|
28
|
+
} catch (err) {
|
|
29
|
+
// npm exits non-zero when a signature is invalid or missing: its JSON is still the answer.
|
|
30
|
+
if (err.code === "ENOENT") return { available: false, reason: "npm not found in PATH" };
|
|
31
|
+
if (err.killed) return { available: false, reason: `npm audit signatures timed out after ${TIMEOUT_MS}ms` };
|
|
32
|
+
stdout = err.stdout;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(stdout);
|
|
36
|
+
return { available: true, invalid: pick(parsed.invalid), missing: pick(parsed.missing) };
|
|
37
|
+
} catch {
|
|
38
|
+
logger?.warn?.("npm audit signatures: output not parseable as JSON");
|
|
39
|
+
return { available: false, reason: "npm audit signatures output not parseable as JSON" };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-1255: kj audit --sbom <file>. The CycloneDX SBOM of an npm project's production
|
|
3
|
+
* dependencies, as npm itself builds it from the lockfile (no node_modules needed). Written
|
|
4
|
+
* atomically: a failed run leaves no file, never a half one.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { rename, writeFile } from "node:fs/promises";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
import { runGoverned } from "../utils/tool-governor.js";
|
|
10
|
+
|
|
11
|
+
const TIMEOUT_MS = 60_000;
|
|
12
|
+
const firstLine = (text) => String(text ?? "").trim().split("\n")[0];
|
|
13
|
+
|
|
14
|
+
/** @returns {Promise<{file: string, components: number}>} */
|
|
15
|
+
export async function writeSbom({ projectDir, file, run = runGoverned }) {
|
|
16
|
+
if (!existsSync(join(projectDir, "package-lock.json"))) {
|
|
17
|
+
throw new Error("--sbom needs an npm project with package-lock.json; other ecosystems are not covered yet");
|
|
18
|
+
}
|
|
19
|
+
let stdout;
|
|
20
|
+
try {
|
|
21
|
+
({ stdout } = await run("npm", ["sbom", "--sbom-format", "cyclonedx", "--omit", "dev", "--package-lock-only"], { cwd: projectDir, timeout: TIMEOUT_MS, maxBuffer: 64 * 1024 * 1024 }));
|
|
22
|
+
} catch (err) {
|
|
23
|
+
let why = firstLine(err.stderr) || err.message;
|
|
24
|
+
if (err.code === "ENOENT") why = "npm not found in PATH";
|
|
25
|
+
else if (err.killed) why = `timed out after ${TIMEOUT_MS}ms`;
|
|
26
|
+
throw new Error(`npm sbom failed: ${why}`, { cause: err });
|
|
27
|
+
}
|
|
28
|
+
let doc;
|
|
29
|
+
try { doc = JSON.parse(stdout); } catch { doc = null; }
|
|
30
|
+
if (doc?.bomFormat !== "CycloneDX") throw new Error("npm sbom output is not a CycloneDX document");
|
|
31
|
+
const target = resolve(projectDir, file);
|
|
32
|
+
await writeFile(`${target}.tmp`, stdout);
|
|
33
|
+
await rename(`${target}.tmp`, target);
|
|
34
|
+
return { file: target, components: Array.isArray(doc.components) ? doc.components.length : 0 };
|
|
35
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent token check (KJC-TSK-1268, ADR 0037).
|
|
3
|
+
*
|
|
4
|
+
* The agent works with whatever token `gh` uses. ADR 0037 asks for a fine-grained token without
|
|
5
|
+
* Administration or Deployments, so the agent can neither change branch protection nor approve
|
|
6
|
+
* a protected environment such as npm-publish. Two probes that change nothing tell:
|
|
7
|
+
* - reading the default branch's protection needs Administration;
|
|
8
|
+
* - reviewing pending deployments of a run that does not exist is refused (403) without
|
|
9
|
+
* Deployments write, and answers 404/422 with it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
13
|
+
|
|
14
|
+
import { STRATEGY } from "./types.js";
|
|
15
|
+
|
|
16
|
+
const FIX = "Give the agent a fine-grained token with no Administration, Environments, Secrets, Variables or Deployments "
|
|
17
|
+
+ "(github.com/settings/personal-access-tokens/new), load it with `gh auth login --with-token`, revoke the old one "
|
|
18
|
+
+ "and do admin acts in the browser (ADR 0037)";
|
|
19
|
+
|
|
20
|
+
const OWNER_REPO = /^\/?([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/;
|
|
21
|
+
|
|
22
|
+
/** owner/repo of a GitHub remote, or null: exact host github.com, or an ssh alias github.com-<name>. */
|
|
23
|
+
export function parseGithubRepo(url) {
|
|
24
|
+
const text = String(url ?? "").trim();
|
|
25
|
+
const scp = /^[\w.-]+@github\.com(?:-[\w.-]+)?:(.+)$/.exec(text);
|
|
26
|
+
let path = scp?.[1];
|
|
27
|
+
if (!path) {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = new URL(text);
|
|
30
|
+
if (!["https:", "ssh:"].includes(parsed.protocol) || !/^github\.com(?:-[\w.-]+)?$/.test(parsed.hostname)) return null;
|
|
31
|
+
if (parsed.protocol === "https:" && parsed.hostname !== "github.com") return null;
|
|
32
|
+
path = parsed.pathname;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const match = OWNER_REPO.exec(path);
|
|
38
|
+
return match ? `${match[1]}/${match[2]}` : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function defaultOriginUrl(projectDir) {
|
|
42
|
+
try {
|
|
43
|
+
return execFileSync("git", ["remote", "get-url", "origin"], { cwd: projectDir, encoding: "utf8", timeout: 10_000 });
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `gh api` as data: the HTTP status gh reports and the JSON body, success or not. */
|
|
50
|
+
async function defaultGh(args) {
|
|
51
|
+
try {
|
|
52
|
+
return { status: 200, body: JSON.parse(execFileSync("gh", ["api", ...args], { encoding: "utf8", timeout: 20_000, stdio: "pipe" }) || "{}") };
|
|
53
|
+
} catch (err) {
|
|
54
|
+
const status = Number(/\(HTTP (\d{3})\)/.exec(String(err.stderr ?? ""))?.[1]);
|
|
55
|
+
if (!status) throw err;
|
|
56
|
+
let body = {};
|
|
57
|
+
try { body = JSON.parse(String(err.stdout ?? "") || "{}"); } catch { /* gh printed no JSON body */ }
|
|
58
|
+
return { status, body };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** true/false for the answers each probe expects; anything else (401, 5xx...) cannot be read. */
|
|
63
|
+
function hasAdmin({ status, body }) {
|
|
64
|
+
if (status === 200) return true;
|
|
65
|
+
if (status === 404) return /not protected/i.test(body?.message ?? "");
|
|
66
|
+
if (status === 403) return false;
|
|
67
|
+
throw new Error(`branch protection probe answered HTTP ${status}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function canApprove({ status }) {
|
|
71
|
+
if (status === 404 || status === 422) return true;
|
|
72
|
+
if (status === 403) return false;
|
|
73
|
+
throw new Error(`deployment review probe answered HTTP ${status}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function defaultBranchOf(gh, repo) {
|
|
77
|
+
const { status, body } = await gh([`repos/${repo}`]);
|
|
78
|
+
if (status !== 200 || !body?.default_branch) throw new Error(`reading ${repo} answered HTTP ${status}`);
|
|
79
|
+
return body.default_branch;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createAgentTokenCheck({ projectDir = process.cwd(), originUrl = () => defaultOriginUrl(projectDir), gh = defaultGh } = {}) {
|
|
83
|
+
return {
|
|
84
|
+
name: "agent-token",
|
|
85
|
+
label: "agent's GitHub token (no admin power)",
|
|
86
|
+
strategy: STRATEGY.MANUAL,
|
|
87
|
+
async detect() {
|
|
88
|
+
const repo = parseGithubRepo(originUrl());
|
|
89
|
+
if (!repo) return { ok: true, severity: "info", detail: "no GitHub origin: nothing to check" };
|
|
90
|
+
try {
|
|
91
|
+
const branch = await defaultBranchOf(gh, repo);
|
|
92
|
+
const powers = [];
|
|
93
|
+
if (hasAdmin(await gh([`repos/${repo}/branches/${encodeURIComponent(branch)}/protection`]))) {
|
|
94
|
+
powers.push("it holds the Administration permission (it reads branch protection; ADR 0037 allows it at no level, since write changes protection and settings)");
|
|
95
|
+
}
|
|
96
|
+
const probe = ["-X", "POST", `repos/${repo}/actions/runs/1/pending_deployments`, "-F", "environment_ids[]=0", "-f", "state=rejected", "-f", "comment=kj doctor probe"];
|
|
97
|
+
if (canApprove(await gh(probe))) powers.push("it can approve protected environments (Deployments write)");
|
|
98
|
+
if (powers.length === 0) return { ok: true, severity: "info", detail: `the token gh uses has no admin power over ${repo}` };
|
|
99
|
+
return { ok: false, severity: "warn", detail: `the token gh uses on ${repo}: ${powers.join("; ")}`, fix: FIX };
|
|
100
|
+
} catch (err) {
|
|
101
|
+
const reason = `${err?.code ?? ""} ${err?.stderr ?? ""} ${err?.message ?? err}`;
|
|
102
|
+
// No gh, or gh with no token: the agent holds no GitHub power to check.
|
|
103
|
+
if (/ENOENT|gh auth login|not logged in/i.test(reason)) return { ok: true, severity: "info", detail: "gh has no GitHub token here: nothing to check" };
|
|
104
|
+
return { ok: false, severity: "warn", detail: `could not check the token gh uses: ${String(err?.message || err).split("\n")[0]}`, fix: FIX };
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function getAgentTokenChecks({ projectDir } = {}) {
|
|
111
|
+
return [createAgentTokenCheck({ projectDir })];
|
|
112
|
+
}
|
package/src/checks/ai-surface.js
CHANGED
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
12
|
import os from "node:os";
|
|
13
13
|
import path from "node:path";
|
|
14
|
+
import { claudeConfigFile, codexHome } from "../utils/agent-home.js";
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
// KJC-TSK-1344: claude and codex configs of the account CLAUDE_CONFIG_DIR / CODEX_HOME select.
|
|
17
|
+
const sources = (projectDir, home, env = process.env) => [
|
|
16
18
|
{ file: path.join(projectDir, ".mcp.json"), format: "json" },
|
|
17
|
-
{ file:
|
|
18
|
-
{ file: path.join(home,
|
|
19
|
+
{ file: claudeConfigFile({ home, env }), format: "json" },
|
|
20
|
+
{ file: path.join(codexHome({ home, env }), "config.toml"), format: "toml" },
|
|
19
21
|
{ file: path.join(home, ".gemini", "settings.json"), format: "json" },
|
|
20
22
|
];
|
|
21
23
|
|
|
@@ -34,9 +36,9 @@ const tomlServerNames = (text) =>
|
|
|
34
36
|
new Set([...text.matchAll(/^\s*\[mcp_servers\.["']?([^\]"']+)["']?\]/gm)].map((m) => m[1]));
|
|
35
37
|
|
|
36
38
|
/** Sorted `name (config-file)` entries for every MCP reachable in this project. */
|
|
37
|
-
export function collectAiSurface({ projectDir = process.cwd(), home = os.homedir() } = {}) {
|
|
39
|
+
export function collectAiSurface({ projectDir = process.cwd(), home = os.homedir(), env = process.env } = {}) {
|
|
38
40
|
const surface = new Set();
|
|
39
|
-
for (const { file, format } of sources(projectDir, home)) {
|
|
41
|
+
for (const { file, format } of sources(projectDir, home, env)) {
|
|
40
42
|
if (!existsSync(file)) continue;
|
|
41
43
|
let text;
|
|
42
44
|
try { text = readFileSync(file, "utf8"); } catch { continue; }
|
|
@@ -50,11 +52,11 @@ export function collectAiSurface({ projectDir = process.cwd(), home = os.homedir
|
|
|
50
52
|
* Diff the current surface against the last-seen snapshot and persist the
|
|
51
53
|
* new state. First run records the baseline silently.
|
|
52
54
|
*/
|
|
53
|
-
export function checkAiSurface({ projectDir = process.cwd(), home = os.homedir(), statePath = path.join(os.homedir(), ".karajan", "ai-surface.json"), extraSurface = [] } = {}) {
|
|
55
|
+
export function checkAiSurface({ projectDir = process.cwd(), home = os.homedir(), env = process.env, statePath = path.join(os.homedir(), ".karajan", "ai-surface.json"), extraSurface = [] } = {}) {
|
|
54
56
|
// KJC-TSK-0728: extraSurface carries entries the (async) caller collected
|
|
55
57
|
// outside config files — e.g. observed agent CLIs as "grok (cli)" — so
|
|
56
58
|
// they ride the same snapshot and the same "NEW since last check" drift.
|
|
57
|
-
const surface = [...new Set([...collectAiSurface({ projectDir, home }), ...extraSurface])].sort();
|
|
59
|
+
const surface = [...new Set([...collectAiSurface({ projectDir, home, env }), ...extraSurface])].sort();
|
|
58
60
|
let state = {};
|
|
59
61
|
try { state = JSON.parse(readFileSync(statePath, "utf8")); } catch { /* first run or corrupt → baseline */ }
|
|
60
62
|
const prev = Array.isArray(state[projectDir]) ? state[projectDir] : null;
|