karajan-code 4.70.4 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +6 -14
- package/packages/hu-board/package.json +1 -1
- package/packages/hu-board/public/app.js +4 -18
- package/packages/hu-board/public/index.html +5 -2
- package/packages/hu-board/public/styles.css +26 -0
- package/packages/hu-board/public/utils/api.js +0 -74
- package/packages/hu-board/public/utils/board-tour.js +62 -0
- package/packages/hu-board/public/utils/board-view.js +49 -144
- package/packages/hu-board/public/utils/command-launcher.js +8 -70
- package/packages/hu-board/public/utils/formatters.js +10 -0
- package/packages/hu-board/public/utils/governance-view.js +13 -1
- package/packages/hu-board/public/utils/hu-actions.js +17 -68
- package/packages/hu-board/public/utils/init-listeners.js +7 -8
- package/packages/hu-board/public/utils/log-panel.js +3 -16
- package/packages/hu-board/public/utils/maggle-mode.js +2 -12
- package/packages/hu-board/public/utils/modals.js +7 -0
- package/packages/hu-board/public/utils/project-picker-view.js +1 -1
- package/packages/hu-board/public/utils/server-push.js +3 -22
- package/packages/hu-board/public/utils/shared-board-notice.js +33 -0
- package/packages/hu-board/public/utils/story-detail-view.js +5 -19
- package/packages/hu-board/src/cleanup-zombies.js +0 -1
- package/packages/hu-board/src/command-runner.js +36 -25
- package/packages/hu-board/src/db-connection.js +282 -0
- package/packages/hu-board/src/db-paths.js +113 -0
- package/packages/hu-board/src/db-queries.js +188 -0
- package/packages/hu-board/src/db-tombstones.js +51 -0
- package/packages/hu-board/src/db.js +28 -619
- package/packages/hu-board/src/ephemeral-cleaner.js +2 -3
- package/packages/hu-board/src/hu-zombie-reaper.js +5 -1
- package/packages/hu-board/src/plan-mutations.js +3 -153
- package/packages/hu-board/src/plans-service.js +55 -0
- package/packages/hu-board/src/routes/api/commands.js +122 -0
- package/packages/hu-board/src/routes/api/plans.js +55 -0
- package/packages/hu-board/src/routes/api/projects.js +267 -0
- package/packages/hu-board/src/routes/api/prompts.js +135 -0
- package/packages/hu-board/src/routes/api/sessions.js +95 -0
- package/packages/hu-board/src/routes/api/stories.js +183 -0
- package/packages/hu-board/src/routes/api/system.js +241 -0
- package/packages/hu-board/src/routes/api/tombstones.js +38 -0
- package/packages/hu-board/src/routes/api.js +24 -1371
- package/packages/hu-board/src/routes/governance.js +23 -1
- package/packages/hu-board/src/routes/lanes.js +40 -0
- package/packages/hu-board/src/routes/rag.js +4 -3
- package/packages/hu-board/src/server.js +20 -34
- package/packages/hu-board/src/sync-files.js +318 -0
- package/packages/hu-board/src/sync-project.js +99 -0
- package/packages/hu-board/src/sync.js +22 -566
- package/packages/hu-board/src/zombie-reaper.js +1 -1
- package/scripts/dual-publish.mjs +19 -3
- package/scripts/postinstall.js +15 -2
- package/scripts/verify-pack-mcp.mjs +11 -7
- package/scripts/verify-pack.mjs +4 -4
- package/src/audit/audit-history.js +3 -2
- package/src/audit/dead-exports.js +68 -59
- package/src/audit/deterministic-summary.js +33 -13
- package/src/audit/npm-signatures.js +41 -0
- package/src/audit/sbom.js +35 -0
- package/src/checks/agent-token.js +112 -0
- package/src/checks/dir-setup.js +34 -1
- package/src/checks/native-build.js +21 -39
- package/src/checks/runner.js +128 -142
- package/src/cli/advanced-commands.js +6 -6
- package/src/cli/register-meta.js +42 -82
- package/src/cli/register-pipeline.js +8 -143
- package/src/cli/register-plan.js +9 -100
- package/src/cli/retired-commands.js +49 -0
- package/src/cli.js +18 -5
- package/src/commands/agent-run.js +40 -26
- package/src/commands/audit.js +127 -124
- package/src/commands/board.js +5 -9
- package/src/commands/check.js +29 -16
- package/src/commands/doctor.js +2 -0
- package/src/commands/env.js +176 -150
- package/src/commands/go.js +71 -53
- package/src/commands/harden.js +90 -81
- package/src/commands/hu-start.js +52 -0
- package/src/commands/hu.js +288 -221
- package/src/commands/identity.js +111 -78
- package/src/commands/init-atril.js +47 -0
- package/src/commands/init.js +11 -40
- package/src/commands/install-tools.js +70 -85
- package/src/commands/main-guard.js +78 -0
- package/src/commands/policy.js +226 -184
- package/src/commands/qmd.js +27 -21
- package/src/commands/rag.js +75 -48
- package/src/commands/review-gate.js +421 -341
- package/src/commands/start.js +4 -5
- package/src/commands/steward.js +96 -47
- package/src/commands/tournament.js +46 -37
- package/src/commands/worktree.js +94 -25
- package/src/config/loader.js +34 -17
- package/src/config/secrets.js +36 -0
- package/src/config.js +1 -0
- package/src/environment/adr.js +16 -2
- package/src/git/karajan-label.js +20 -8
- package/src/guards/main-guard.js +54 -0
- package/src/harden/hook-templates.js +16 -1
- package/src/harden/sentinel/posttooluse.mjs +18 -6
- package/src/harden/sentinel/pretooluse-sentinel.mjs +64 -36
- package/src/harden/sentinel/sentinel-lib.mjs +129 -26
- package/src/harden/sentinel/sentinel-shell.mjs +5 -16
- package/src/harden/sentinel/sessionstart.mjs +3 -2
- package/src/harden/sentinel/stop.mjs +5 -4
- package/src/harden/workflow-templates.js +1 -1
- package/src/hu/board-lookup.js +29 -0
- package/src/hu/gh-identity.js +23 -0
- package/src/hu/github-board.js +4 -2
- package/src/hu/lane-prune.js +15 -7
- package/src/hu/lane-setup.js +65 -0
- package/src/hu/lanes.js +66 -0
- package/src/hu/team-board-mirror.js +3 -2
- package/src/infrastructure/command-runner.js +1 -1
- package/src/infrastructure/file-system-service.js +1 -1
- package/src/mcp/handlers/direct-handlers.js +2 -292
- package/src/mcp/handlers/hu-handlers.js +1 -35
- package/src/mcp/handlers/management-handlers.js +2 -42
- package/src/mcp/server-handlers.js +16 -22
- package/src/mcp/tools.js +1 -196
- package/src/plan/lane-plans.js +54 -0
- package/src/plan/plan-store.js +7 -4
- package/src/policy/add.js +26 -12
- package/src/policy/adr-number-check.js +31 -0
- package/src/prompts/audit.js +299 -339
- package/src/prompts/card-context.js +10 -5
- package/src/prompts/reviewer.js +5 -2
- package/src/rag/auto-update.js +56 -43
- package/src/rag/bootstrap-index.js +28 -54
- package/src/rag/migrate.js +1 -1
- package/src/rag/onnx-fallback.js +2 -2
- package/src/rag/pid-lease.js +70 -0
- package/src/rag/project-store.js +2 -12
- package/src/review/card-first.js +57 -46
- package/src/review/gate-gitignore.js +23 -4
- package/src/review/gate-hook.js +12 -4
- package/src/review/one-shot-review.js +45 -35
- package/src/review/publish-pr.js +14 -2
- package/src/review/repo-docs.js +73 -0
- package/src/review/repo-stack.js +59 -0
- package/src/review/reviewer-fallback.js +19 -4
- package/src/review/seal-commit.js +29 -0
- package/src/review/sonar-pregate.js +59 -44
- package/src/review/third-party-review.js +90 -16
- package/src/review/third-party-size.js +43 -0
- package/src/review/third-party-skills.js +54 -0
- package/src/review/verify-pr.js +1 -24
- package/src/roles/audit-role.js +106 -125
- package/src/skills/atril-skills.js +5 -4
- package/src/sonar/scanner.js +116 -101
- package/src/steward/invariants.js +5 -4
- package/src/steward/phantom-sweep.js +66 -0
- package/src/tournament/judge.js +60 -53
- package/src/utils/gh-account.js +37 -0
- package/src/utils/git.js +0 -13
- package/src/utils/home-migration.js +7 -1
- package/src/utils/main-checkout.js +53 -0
- package/src/utils/qmd-collection.js +3 -1
- package/src/utils/welcome.js +3 -3
- package/templates/{skills → atril/commands}/kj-architect.md +5 -0
- package/templates/{skills → atril/commands}/kj-audit.md +5 -0
- package/templates/{skills → atril/commands}/kj-board.md +5 -0
- package/templates/{skills → atril/commands}/kj-code.md +5 -0
- package/templates/{skills → atril/commands}/kj-csv-transform.md +5 -0
- package/templates/{skills → atril/commands}/kj-data-report.md +5 -0
- package/templates/{skills → atril/commands}/kj-discover.md +5 -0
- package/templates/{skills → atril/commands}/kj-rag-query.md +5 -0
- package/templates/{skills → atril/commands}/kj-review.md +5 -0
- package/templates/{skills → atril/commands}/kj-security.md +5 -0
- package/templates/{skills → atril/commands}/kj-sonar.md +5 -0
- package/templates/{skills → atril/commands}/kj-sql-analysis.md +5 -0
- package/templates/{skills → atril/commands}/kj-test.md +5 -0
- package/packages/hu-board/public/utils/run-launcher.js +0 -169
- package/packages/hu-board/src/run-tracker.js +0 -121
- package/src/activity-log.js +0 -59
- package/src/agents/host-agent.js +0 -38
- package/src/agents/model-registry.js +0 -142
- package/src/audit/plan-adherence.js +0 -224
- package/src/autonomy/arbiter-role.js +0 -71
- package/src/autonomy/arbiter.js +0 -22
- package/src/autonomy/decider.js +0 -18
- package/src/autonomy/decision-resolver.js +0 -58
- package/src/autonomy/outcome-report.js +0 -55
- package/src/autonomy/stage-ask.js +0 -30
- package/src/brain/decision-tracker.js +0 -125
- package/src/brain/decisor.js +0 -243
- package/src/brain/solomon-consult.js +0 -171
- package/src/brain/standby-scheduler.js +0 -8
- package/src/budget/comparison.js +0 -96
- package/src/canvas/parse-md.js +0 -238
- package/src/canvas/schema.js +0 -101
- package/src/canvas/to-flat.js +0 -85
- package/src/canvas/validate.js +0 -163
- package/src/ci/dispatch.js +0 -99
- package/src/ci/index.js +0 -3
- package/src/ci/pr-diff.js +0 -26
- package/src/cli/register-standby.js +0 -26
- package/src/commands/architect.js +0 -111
- package/src/commands/autorun.js +0 -49
- package/src/commands/researcher.js +0 -54
- package/src/commands/resume.js +0 -72
- package/src/commands/review.js +0 -89
- package/src/commands/run.js +0 -185
- package/src/commands/standby.js +0 -117
- package/src/commands/status.js +0 -36
- package/src/commands/triage.js +0 -78
- package/src/commands/undo.js +0 -62
- package/src/domains/domain-loader.js +0 -245
- package/src/domains/domain-registry.js +0 -140
- package/src/domains/domain-synthesizer.js +0 -129
- package/src/git/automation.js +0 -433
- package/src/git/coder-commit-guard.js +0 -40
- package/src/git/hu-automation.js +0 -243
- package/src/git/hu-snapshot.js +0 -10
- package/src/git/symlink-targets.js +0 -57
- package/src/golden/asserter.js +0 -92
- package/src/golden/loader.js +0 -52
- package/src/golden/runner.js +0 -92
- package/src/golden/schema.js +0 -41
- package/src/guards/duplicate-members.js +0 -86
- package/src/guards/intent-guard.js +0 -161
- package/src/guards/perf-guard.js +0 -126
- package/src/guards/policy-guard.js +0 -90
- package/src/guards/policy-resolver.js +0 -85
- package/src/guards/secret-redactor.js +0 -71
- package/src/hu/acceptance-runner.js +0 -138
- package/src/hu/lazy-planner.js +0 -109
- package/src/hu/parallel-executor.js +0 -94
- package/src/hu/routing-metrics.js +0 -17
- package/src/hu/split-validator.js +0 -143
- package/src/hu/splitting-detector.js +0 -158
- package/src/hu/splitting-generator.js +0 -175
- package/src/hu/store.js +0 -394
- package/src/infrastructure/mocks.js +0 -182
- package/src/mcp/handlers/run-handler.js +0 -318
- package/src/mcp/sovereignty-guard.js +0 -113
- package/src/mcp/tool-arg-normalizers.js +0 -16
- package/src/mutate/reviewer-signal.js +0 -76
- package/src/orchestrator/agent-fallback.js +0 -81
- package/src/orchestrator/brain-coordinator.js +0 -186
- package/src/orchestrator/ci-integration.js +0 -139
- package/src/orchestrator/config-init.js +0 -426
- package/src/orchestrator/direct-actions.js +0 -217
- package/src/orchestrator/drivers/error-recovery.js +0 -287
- package/src/orchestrator/drivers/init-context.js +0 -238
- package/src/orchestrator/drivers/iteration-loop.js +0 -382
- package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +0 -69
- package/src/orchestrator/drivers/iteration-phases/guards.js +0 -91
- package/src/orchestrator/drivers/iteration-phases/handle-approved.js +0 -29
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +0 -125
- package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +0 -62
- package/src/orchestrator/drivers/post-loop.js +0 -404
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +0 -137
- package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +0 -47
- package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +0 -84
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +0 -393
- package/src/orchestrator/drivers/pre-loop.js +0 -524
- package/src/orchestrator/drivers/run-hu-batch.js +0 -468
- package/src/orchestrator/feedback-enrichment.js +0 -140
- package/src/orchestrator/feedback-queue.js +0 -151
- package/src/orchestrator/flow-control.js +0 -170
- package/src/orchestrator/flow-runner.js +0 -365
- package/src/orchestrator/fs-leak-detector.js +0 -228
- package/src/orchestrator/hu-scheduler.js +0 -114
- package/src/orchestrator/hu-sub-pipeline.js +0 -642
- package/src/orchestrator/iteration-gate.js +0 -73
- package/src/orchestrator/iteration-stages.js +0 -9
- package/src/orchestrator/parallel-limiter.js +0 -74
- package/src/orchestrator/pipeline-context.js +0 -72
- package/src/orchestrator/post-loop-stages.js +0 -468
- package/src/orchestrator/pre-loop-stages.js +0 -11
- package/src/orchestrator/preflight-checks.js +0 -487
- package/src/orchestrator/repair/repair-detector.js +0 -117
- package/src/orchestrator/repair/repair-runner.js +0 -142
- package/src/orchestrator/reviewer-fallback.js +0 -45
- package/src/orchestrator/role-output-compressor.js +0 -189
- package/src/orchestrator/session-journal.js +0 -308
- package/src/orchestrator/solomon-escalation.js +0 -331
- package/src/orchestrator/solomon-rules.js +0 -212
- package/src/orchestrator/stages/acceptance-coverage-stage.js +0 -47
- package/src/orchestrator/stages/architect-stage.js +0 -172
- package/src/orchestrator/stages/coder-stage.js +0 -506
- package/src/orchestrator/stages/domain-curator-stage.js +0 -82
- package/src/orchestrator/stages/format-stage.js +0 -61
- package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +0 -127
- package/src/orchestrator/stages/hu-reviewer-stage.js +0 -419
- package/src/orchestrator/stages/perf-stage.js +0 -140
- package/src/orchestrator/stages/planner-stage.js +0 -85
- package/src/orchestrator/stages/rag-context-stage.js +0 -80
- package/src/orchestrator/stages/rag-preload-decisor.js +0 -58
- package/src/orchestrator/stages/research-stage.js +0 -69
- package/src/orchestrator/stages/reviewer-stage.js +0 -431
- package/src/orchestrator/stages/sonar-stage.js +0 -439
- package/src/orchestrator/stages/stage-classes.js +0 -84
- package/src/orchestrator/stages/stage-executor.js +0 -155
- package/src/orchestrator/stages/suite-green-stage.js +0 -62
- package/src/orchestrator/stages/tdd-discipline-stage.js +0 -56
- package/src/orchestrator/stages/tool-judge-stage.js +0 -53
- package/src/orchestrator/stages/triage-stage.js +0 -172
- package/src/orchestrator/standby.js +0 -71
- package/src/orchestrator/verification-gate.js +0 -183
- package/src/orchestrator.js +0 -22
- package/src/plan/adr-generator.js +0 -170
- package/src/plan/adr-loader.js +0 -60
- package/src/plan/plan-executor.js +0 -100
- package/src/plan/plan-validation.js +0 -4
- package/src/prompts/acceptance-coverage.js +0 -32
- package/src/prompts/arbiter.js +0 -38
- package/src/prompts/architect.js +0 -103
- package/src/prompts/hu-reviewer.js +0 -244
- package/src/prompts/tool-correctness-judge.js +0 -31
- package/src/prompts/triage.js +0 -57
- package/src/rag/embedder.js +0 -2
- package/src/rag/embedders/_cloud-base.js +0 -2
- package/src/rag/embedders/cohere.js +0 -2
- package/src/rag/embedders/mistral.js +0 -2
- package/src/rag/embedders/onnx.js +0 -2
- package/src/rag/embedders/openai.js +0 -2
- package/src/rag/embedders/voyage.js +0 -2
- package/src/rag/rerank.js +0 -4
- package/src/rag/where-parser.js +0 -4
- package/src/repeat-detector.js +0 -88
- package/src/review/diff-generator.js +0 -112
- package/src/review/git-stash-helper.js +0 -104
- package/src/review/schema.js +0 -31
- package/src/review/scope-filter.js +0 -152
- package/src/review/snapshot-diff.js +0 -122
- package/src/review/tdd-discipline.js +0 -90
- package/src/review/tdd-policy.js +0 -69
- package/src/roles/acceptance-coverage-role.js +0 -78
- package/src/roles/architect-role.js +0 -77
- package/src/roles/coder-role.js +0 -113
- package/src/roles/commiter-role.js +0 -113
- package/src/roles/domain-curator-role.js +0 -134
- package/src/roles/hu-reviewer-role.js +0 -64
- package/src/roles/impeccable-role.js +0 -85
- package/src/roles/index.js +0 -18
- package/src/roles/karajan-brain-role.js +0 -134
- package/src/roles/perf-role.js +0 -122
- package/src/roles/planner-role.js +0 -151
- package/src/roles/refactorer-role.js +0 -30
- package/src/roles/repairer-role.js +0 -181
- package/src/roles/researcher-role.js +0 -70
- package/src/roles/reviewer-role.js +0 -115
- package/src/roles/security-role.js +0 -73
- package/src/roles/solomon-role.js +0 -271
- package/src/roles/sonar-role.js +0 -72
- package/src/roles/tester-role.js +0 -235
- package/src/roles/tool-correctness-judge-role.js +0 -78
- package/src/roles/triage-role.js +0 -99
- package/src/session/journal/decisions-writer.js +0 -200
- package/src/session/journal/iteration-logger.js +0 -194
- package/src/session/journal/summary-writer.js +0 -320
- package/src/session/journal/tree-writer.js +0 -164
- package/src/session/journal/write-all.js +0 -89
- package/src/session-cleanup.js +0 -116
- package/src/skills/semantic-detector.js +0 -167
- package/src/skills/skill-detector.js +0 -385
- package/src/sonar/cloud-scanner.js +0 -78
- package/src/tracker-bootstrap.js +0 -44
- package/src/types/index.js +0 -38
- package/src/types/policy.js +0 -43
- package/src/types/solomon.js +0 -87
- package/src/utils/async-lock.js +0 -31
- package/src/utils/budget.js +0 -229
- package/src/utils/cwd-confirm.js +0 -39
- package/src/utils/display/event-handlers.js +0 -403
- package/src/utils/display/formatters.js +0 -132
- package/src/utils/display/header.js +0 -47
- package/src/utils/display/session-summary.js +0 -118
- package/src/utils/display/solomon.js +0 -42
- package/src/utils/pricing.js +0 -29
- package/src/utils/quota-simulator.js +0 -56
- package/src/utils/role-assigner.js +0 -137
- package/src/utils/rtk-wrapper.js +0 -87
- package/src/utils/run-registry.js +0 -9
- package/src/utils/status-dashboard.js +0 -254
- package/src/utils/tool-call-extractor.js +0 -56
- package/src/webperf/devtools-detect.js +0 -112
- package/src/webperf/webperf-role-integration.js +0 -66
- package/templates/skills/kj-run.md +0 -73
package/src/commands/start.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Wires the read-only sweep (0568) → deterministic assessment (0569) → haiku
|
|
3
3
|
// decider (0569), reports the recommended next step, and — interactively only —
|
|
4
4
|
// confirms and dispatches the chosen intent to its saved command
|
|
5
|
-
// (harden/rag
|
|
5
|
+
// (harden/rag). --json / --yes / no-TTY stay read-only: emit the
|
|
6
6
|
// assessment + intent and apply nothing. Collaborators are injected for testing.
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
@@ -17,16 +17,15 @@ const NEXT_STEP = {
|
|
|
17
17
|
ASSESS_ONLY: "Everything looks healthy — nothing to do.",
|
|
18
18
|
RECOMMEND_HARDEN: "Suggested: `kj harden --interactive`",
|
|
19
19
|
RECOMMEND_INDEX: "Suggested: `kj rag index`",
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// KJC-TSK-1322: kj run and kj plan generate are retired in 5.0; your agent does the work.
|
|
21
|
+
START_TASK: 'Suggested: ask your agent for it, and put it on the board first with `kj hu add "<task>"`',
|
|
22
|
+
PROPOSE_PLAN: 'Suggested: have your agent plan it with `kj brief planner`, then `kj hu add` each piece',
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
// Each writing intent maps to the saved command + args `kj start` dispatches.
|
|
25
26
|
const DISPATCH = {
|
|
26
27
|
RECOMMEND_HARDEN: () => ["harden", "--interactive"],
|
|
27
28
|
RECOMMEND_INDEX: () => ["rag", "index"],
|
|
28
|
-
START_TASK: (task) => ["run", task],
|
|
29
|
-
PROPOSE_PLAN: (task) => ["plan", task],
|
|
30
29
|
};
|
|
31
30
|
|
|
32
31
|
const ASK_FALLBACK = "What would you like to do with this project?";
|
package/src/commands/steward.js
CHANGED
|
@@ -19,6 +19,7 @@ import { loadPreviousAudit } from "../audit/basal-cost.js";
|
|
|
19
19
|
import { collectOsvFindings } from "../audit/osv-findings.js";
|
|
20
20
|
import { recordGateDecision } from "../policy/decisions.js";
|
|
21
21
|
import { syncProposedWork } from "../steward/proposed-work.js";
|
|
22
|
+
import { sweepPhantoms } from "../steward/phantom-sweep.js";
|
|
22
23
|
|
|
23
24
|
const stewardDir = (projectDir) => path.join(projectDir, ".karajan", "steward");
|
|
24
25
|
|
|
@@ -30,39 +31,63 @@ const ghRuns = (projectDir, baseBranch) => () => {
|
|
|
30
31
|
|
|
31
32
|
const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; } };
|
|
32
33
|
|
|
34
|
+
const MARK = { [VERDICTS.OK]: "✓", [VERDICTS.BROKEN]: "✗", [VERDICTS.UNKNOWN]: "?", [VERDICTS.NOT_OBSERVABLE]: "∅" };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* STW-E (KJC-TSK-0793): --if-stale <days> — resuming work with a fresh
|
|
38
|
+
* report does not re-sweep; the report in the repo stays the ONE source.
|
|
39
|
+
* @returns {object|null} the fresh previous report, or null when a sweep is due
|
|
40
|
+
*/
|
|
41
|
+
function freshReport(projectDir, flags, probes) {
|
|
42
|
+
if (flags.ifStale === undefined) return null;
|
|
43
|
+
const prev = readJson(path.join(stewardDir(projectDir), "report.json"));
|
|
44
|
+
const ageMs = prev?.sweptAt ? (probes.nowMs ?? Date.now()) - Date.parse(prev.sweptAt) : Infinity;
|
|
45
|
+
return ageMs < Number(flags.ifStale) * 86_400_000 ? prev : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Live osv probe (best-effort): unavailable is null → the invariant answers
|
|
50
|
+
* unknown with its remedy, never a clean bill.
|
|
51
|
+
*/
|
|
52
|
+
async function liveVulns({ projectDir, logger, probes }) {
|
|
53
|
+
if (probes.vulns != null) return probes.vulns;
|
|
54
|
+
try {
|
|
55
|
+
const osv = await (probes.osvFn ?? collectOsvFindings)(projectDir, logger);
|
|
56
|
+
if (osv?.available) return (osv.vulnerabilities || []).map((v) => ({ id: v.id, severity: v.severity, publishedAt: v.publishedAt ?? null }));
|
|
57
|
+
} catch { /* stays null — unknown */ }
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The dead code the last audit counted: knip's exports + files, or the older deadExports list. */
|
|
62
|
+
async function deadCodeNow(projectDir) {
|
|
63
|
+
const snapshot = await loadPreviousAudit(projectDir).catch(() => null);
|
|
64
|
+
if (typeof snapshot?.knipDeadExports?.exports === "number") {
|
|
65
|
+
return { deadExports: snapshot.knipDeadExports.exports + (snapshot.knipDeadExports.files || 0) };
|
|
66
|
+
}
|
|
67
|
+
return Array.isArray(snapshot?.deadExports) ? { deadExports: snapshot.deadExports.length } : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The phantom detectors over the project's test↔source pairs; a failure is said, never clean. */
|
|
71
|
+
function phantomSweep(projectDir, probes) {
|
|
72
|
+
if (probes.phantoms) return probes.phantoms;
|
|
73
|
+
try { return sweepPhantoms({ projectDir }); } catch (err) { return { reason: `the phantom sweep failed (${err.message})` }; }
|
|
74
|
+
}
|
|
75
|
+
|
|
33
76
|
export async function stewardSweepCommand({ flags = {}, config = {}, logger = console, probes = {} } = {}) {
|
|
34
77
|
const projectDir = config.projectDir || process.cwd();
|
|
35
78
|
const baseBranch = config.base_branch || "main";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const ageMs = prev?.sweptAt ? (probes.nowMs ?? Date.now()) - Date.parse(prev.sweptAt) : Infinity;
|
|
41
|
-
if (ageMs < Number(flags.ifStale) * 86_400_000) {
|
|
42
|
-
logger.info?.(`steward: report is fresh (swept ${prev.sweptAt}) — not re-sweeping`);
|
|
43
|
-
return 0;
|
|
44
|
-
}
|
|
79
|
+
const fresh = freshReport(projectDir, flags, probes);
|
|
80
|
+
if (fresh) {
|
|
81
|
+
logger.info?.(`steward: report is fresh (swept ${fresh.sweptAt}) — not re-sweeping`);
|
|
82
|
+
return 0;
|
|
45
83
|
}
|
|
46
84
|
const freshness = resolveFreshness(config);
|
|
47
85
|
const nowMs = probes.nowMs ?? Date.now();
|
|
48
86
|
const runsFn = probes.runsFn ?? ghRuns(projectDir, baseBranch);
|
|
49
|
-
|
|
50
|
-
// Live osv probe (best-effort): unavailable is null → the invariant answers
|
|
51
|
-
// unknown with its remedy, never a clean bill.
|
|
52
|
-
let vulns = probes.vulns ?? null;
|
|
53
|
-
if (vulns === null) {
|
|
54
|
-
try {
|
|
55
|
-
const osv = await (probes.osvFn ?? collectOsvFindings)(projectDir, logger);
|
|
56
|
-
if (osv?.available) vulns = (osv.vulnerabilities || []).map((v) => ({ id: v.id, severity: v.severity, publishedAt: v.publishedAt ?? null }));
|
|
57
|
-
} catch { /* stays null — unknown */ }
|
|
58
|
-
}
|
|
59
|
-
|
|
87
|
+
const vulns = await liveVulns({ projectDir, logger, probes });
|
|
60
88
|
// Dead-code baseline: the PREVIOUS report in the repo — shared memory.
|
|
61
89
|
const prevReport = readJson(path.join(stewardDir(projectDir), "report.json"));
|
|
62
|
-
const
|
|
63
|
-
const deadNow = typeof snapshot?.knipDeadExports?.exports === "number"
|
|
64
|
-
? { deadExports: snapshot.knipDeadExports.exports + (snapshot.knipDeadExports.files || 0) }
|
|
65
|
-
: Array.isArray(snapshot?.deadExports) ? { deadExports: snapshot.deadExports.length } : null;
|
|
90
|
+
const deadNow = await deadCodeNow(projectDir);
|
|
66
91
|
|
|
67
92
|
const invariants = [
|
|
68
93
|
{ id: "main-ci", renew: "push to the base branch (or fix the red run)", evaluate: () => evaluateMainCi({ projectDir, baseBranch, freshness: freshness.values, runsFn, nowMs }) },
|
|
@@ -70,7 +95,7 @@ export async function stewardSweepCommand({ flags = {}, config = {}, logger = co
|
|
|
70
95
|
{ id: "vulnerable-deps", renew: "kj audit (osv-scanner)", evaluate: () => evaluateVulnAging({ vulns, freshness: freshness.values, nowMs }) },
|
|
71
96
|
{ id: "dead-code-trend", renew: "kj audit (knip inventory)", evaluate: () => evaluateDeadCodeTrend({ current: deadNow, previous: prevReport?.deadCodeBaseline ?? null }) },
|
|
72
97
|
{ id: "coverage-config", renew: "configure a coverage threshold", evaluate: () => evaluateCoverageConfig({ projectDir }) },
|
|
73
|
-
{ id: "phantom-coverage", renew: "
|
|
98
|
+
{ id: "phantom-coverage", renew: "kj steward sweep (runs the phantom detectors)", evaluate: () => evaluatePhantomCoverage(phantomSweep(projectDir, probes)) },
|
|
74
99
|
];
|
|
75
100
|
const results = runInvariants(invariants, {}).map((r, i) => ({ ...r, renew: invariants[i].renew }));
|
|
76
101
|
const broken = results.filter((r) => r.verdict === VERDICTS.BROKEN);
|
|
@@ -82,57 +107,81 @@ export async function stewardSweepCommand({ flags = {}, config = {}, logger = co
|
|
|
82
107
|
invariants: results,
|
|
83
108
|
deadCodeBaseline: deadNow ? { ...deadNow, timestamp: new Date(nowMs).toISOString() } : (prevReport?.deadCodeBaseline ?? null),
|
|
84
109
|
};
|
|
85
|
-
|
|
110
|
+
writeReport(projectDir, report, freshness);
|
|
111
|
+
sealSweep({ projectDir, results, broken, logger });
|
|
112
|
+
await cardBrokenInvariants({ projectDir, config, results, sweptAt: report.sweptAt, logger });
|
|
113
|
+
warnIfReportIgnored(projectDir, logger);
|
|
114
|
+
printSweep({ flags, report, results, broken, logger });
|
|
115
|
+
process.exitCode = broken.length ? 1 : 0;
|
|
116
|
+
return broken.length ? 1 : 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function writeReport(projectDir, report, freshness) {
|
|
120
|
+
const freshnessValues = Object.entries(freshness.values).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
86
121
|
const md = [
|
|
87
122
|
"# Steward report",
|
|
88
123
|
"", `Last swept: ${report.sweptAt}`, "",
|
|
89
|
-
`Freshness: ${freshness.declared ? "declared by the project" : "calibrated defaults (the project declared none)"} — ${
|
|
90
|
-
...
|
|
91
|
-
`## ${
|
|
124
|
+
`Freshness: ${freshness.declared ? "declared by the project" : "calibrated defaults (the project declared none)"} — ${freshnessValues}`, "",
|
|
125
|
+
...report.invariants.flatMap((r) => [
|
|
126
|
+
`## ${MARK[r.verdict] || "?"} ${r.id} — ${r.verdict}`,
|
|
92
127
|
`- evidence: ${r.evidence || "(none)"}`,
|
|
93
128
|
...(r.remedy ? [`- remedy: ${r.remedy}`] : []),
|
|
94
129
|
`- renews with: ${r.renew}`, "",
|
|
95
|
-
])
|
|
130
|
+
]),
|
|
96
131
|
].join("\n");
|
|
97
|
-
|
|
98
132
|
fs.mkdirSync(stewardDir(projectDir), { recursive: true });
|
|
99
133
|
fs.writeFileSync(path.join(stewardDir(projectDir), "report.md"), md);
|
|
100
134
|
fs.writeFileSync(path.join(stewardDir(projectDir), "report.json"), `${JSON.stringify(report, null, 2)}\n`);
|
|
135
|
+
}
|
|
101
136
|
|
|
102
|
-
|
|
103
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Every sweep is SEALED in the decision chain — the same criterion policy
|
|
139
|
+
* decisions already meet: recorded and verifiable, not just notified.
|
|
140
|
+
*/
|
|
141
|
+
function sealSweep({ projectDir, results, broken, logger }) {
|
|
104
142
|
try {
|
|
105
143
|
const counts = {};
|
|
106
144
|
for (const r of results) counts[r.verdict] = (counts[r.verdict] || 0) + 1;
|
|
107
145
|
recordGateDecision(projectDir, { kind: "steward-sweep", verdicts: { ok: counts[VERDICTS.OK] || 0, broken: broken.length, unknown: counts[VERDICTS.UNKNOWN] || 0, "not-observable": counts[VERDICTS.NOT_OBSERVABLE] || 0 }, broken_ids: broken.map((b) => b.id) });
|
|
108
146
|
} catch (err) { logger.warn?.(`⚠ steward: the sweep could not be sealed in the decision chain (${err.message})`); }
|
|
147
|
+
}
|
|
109
148
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
149
|
+
/**
|
|
150
|
+
* STW-D (KJC-TSK-0792): every break is PROPOSED work on the board the brain
|
|
151
|
+
* already consumes. AFTER the report and the seal: a board failure never
|
|
152
|
+
* costs either, and it is said loudly — never swallowed.
|
|
153
|
+
*/
|
|
154
|
+
async function cardBrokenInvariants({ projectDir, config, results, sweptAt, logger }) {
|
|
113
155
|
try {
|
|
114
|
-
const sync = await syncProposedWork({ projectDir, config, results, sweptAt
|
|
115
|
-
if (!sync.synced)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (sync.replicateIn) logger.info?.(`→ replicate them in ${sync.replicateIn} through your agent's board tools (kj hu list shows them)`);
|
|
156
|
+
const sync = await syncProposedWork({ projectDir, config, results, sweptAt });
|
|
157
|
+
if (!sync.synced) {
|
|
158
|
+
logger.warn?.(`⚠ steward: broken invariants not carded — ${sync.reason}`);
|
|
159
|
+
return;
|
|
119
160
|
}
|
|
161
|
+
if (!(sync.created || sync.updated || sync.resolved)) return;
|
|
162
|
+
logger.info?.(`steward board: ${sync.created} card(s) proposed, ${sync.updated} updated, ${sync.resolved} resolved`);
|
|
163
|
+
if (sync.replicateIn) logger.info?.(`→ replicate them in ${sync.replicateIn} through your agent's board tools (kj hu list shows them)`);
|
|
120
164
|
} catch (err) { logger.warn?.(`⚠ steward: carding the broken invariants failed (${err.message}) — the report and the seal stand`); }
|
|
165
|
+
}
|
|
121
166
|
|
|
122
|
-
|
|
167
|
+
/** A report nobody can see is not shared state — say it, do not fix their tree. */
|
|
168
|
+
function warnIfReportIgnored(projectDir, logger) {
|
|
123
169
|
try {
|
|
124
170
|
execFileSync("git", ["check-ignore", "-q", path.join(".karajan", "steward", "report.md")], { cwd: projectDir });
|
|
125
171
|
logger.warn?.("⚠ steward: the report path is gitignored — untrack .karajan/steward from .gitignore so the state is shared");
|
|
126
172
|
} catch { /* not ignored — good */ }
|
|
173
|
+
}
|
|
127
174
|
|
|
175
|
+
function printSweep({ flags, report, results, broken, logger }) {
|
|
128
176
|
if (flags.json) {
|
|
129
177
|
process.stdout.write(`${JSON.stringify(report)}\n`);
|
|
130
|
-
|
|
131
|
-
for (const r of results) logger.info?.(`${mark[r.verdict]} ${r.id}: ${r.verdict}${r.verdict === VERDICTS.OK ? "" : ` — ${r.remedy || r.evidence}`}`);
|
|
132
|
-
logger.info?.(broken.length ? `steward: ${broken.length} invariant(s) BROKEN — the report names them (.karajan/steward/report.md)` : "steward: nothing broken — the full state is in .karajan/steward/report.md");
|
|
178
|
+
return;
|
|
133
179
|
}
|
|
134
|
-
|
|
135
|
-
|
|
180
|
+
for (const r of results) {
|
|
181
|
+
const why = r.verdict === VERDICTS.OK ? "" : ` — ${r.remedy || r.evidence}`;
|
|
182
|
+
logger.info?.(`${MARK[r.verdict]} ${r.id}: ${r.verdict}${why}`);
|
|
183
|
+
}
|
|
184
|
+
logger.info?.(broken.length ? `steward: ${broken.length} invariant(s) BROKEN — the report names them (.karajan/steward/report.md)` : "steward: nothing broken — the full state is in .karajan/steward/report.md");
|
|
136
185
|
}
|
|
137
186
|
|
|
138
187
|
/**
|
|
@@ -20,47 +20,53 @@ function tournamentDirFor(config, rawId) {
|
|
|
20
20
|
return path.join(config?.projectDir || process.cwd(), ".kj", "tournaments", id);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// TOR-C: `--judge <id>` ranks the finalists.
|
|
24
|
+
async function judgeAction({ config, logger, flags, judgeTournamentFn }) {
|
|
25
|
+
const judgement = await judgeTournamentFn({ tournamentDir: tournamentDirFor(config, flags.judge), config, logger });
|
|
26
|
+
if (flags.json) logger?.info?.(JSON.stringify(judgement));
|
|
27
|
+
else logger?.info?.(` siguiente: kj tournament --crown ${flags.judge}${judgement.escalated ? " (ganador vía solomon)" : ""}`);
|
|
28
|
+
return judgement;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// TOR-C: `--crown <id>` commits the winner through the real gate in its lane.
|
|
32
|
+
async function crownAction({ config, logger, flags, crownWinnerFn }) {
|
|
33
|
+
const crown = await crownWinnerFn({
|
|
34
|
+
tournamentDir: tournamentDirFor(config, flags.crown), config, logger, cardRef: flags.card || null,
|
|
35
|
+
});
|
|
36
|
+
if (flags.json) logger?.info?.(JSON.stringify(crown));
|
|
37
|
+
return crown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function scoreboardRow(r) {
|
|
41
|
+
if (!r.finalist) return ` ${r.coder.padEnd(10)} eliminado (${r.eliminated_by})`;
|
|
42
|
+
const mut = typeof r.mutation?.score === "number" ? `${r.mutation.score}%` : "n/a";
|
|
43
|
+
return ` ${r.coder.padEnd(10)} FINALISTA loc:${String(r.loc.net).padStart(5)} tests:${r.loc.testsTouched} mutation:${mut}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// TOR-B: `--score <id>` scores an EXISTING tournament from its artifacts.
|
|
47
|
+
async function scoreAction({ config, logger, flags, scoreTournamentFn }) {
|
|
48
|
+
const board = await scoreTournamentFn({ tournamentDir: tournamentDirFor(config, flags.score) });
|
|
49
|
+
if (flags.json) logger?.info?.(JSON.stringify(board));
|
|
50
|
+
else printScoreboard(board, logger);
|
|
51
|
+
return board;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function printScoreboard(board, logger) {
|
|
55
|
+
logger?.info?.(`scoreboard ${board.id} — regla: ${board.rule}`);
|
|
56
|
+
for (const r of board.rows) logger?.info?.(scoreboardRow(r));
|
|
57
|
+
logger?.info?.(` ranking: ${board.ranking.join(" > ") || "(sin finalistas)"}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The first of --judge, --crown, --score that is set wins, in that order.
|
|
61
|
+
const ID_ACTIONS = [["judge", judgeAction], ["crown", crownAction], ["score", scoreAction]];
|
|
62
|
+
|
|
23
63
|
export async function tournamentCommand({
|
|
24
64
|
task, config, logger = console, flags = {},
|
|
25
65
|
runTournamentFn = runTournament, scoreTournamentFn = scoreTournament,
|
|
26
66
|
judgeTournamentFn = judgeTournament, crownWinnerFn = crownWinner,
|
|
27
67
|
}) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (flags.judge) {
|
|
31
|
-
const judgement = await judgeTournamentFn({ tournamentDir: tournamentDirFor(config, flags.judge), config, logger });
|
|
32
|
-
if (flags.json) logger?.info?.(JSON.stringify(judgement));
|
|
33
|
-
else logger?.info?.(` siguiente: kj tournament --crown ${flags.judge}${judgement.escalated ? " (ganador vía solomon)" : ""}`);
|
|
34
|
-
return judgement;
|
|
35
|
-
}
|
|
36
|
-
if (flags.crown) {
|
|
37
|
-
const crown = await crownWinnerFn({
|
|
38
|
-
tournamentDir: tournamentDirFor(config, flags.crown), config, logger, cardRef: flags.card || null,
|
|
39
|
-
});
|
|
40
|
-
if (flags.json) logger?.info?.(JSON.stringify(crown));
|
|
41
|
-
return crown;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// TOR-B: `--score <id>` scores an EXISTING tournament from its artifacts.
|
|
45
|
-
if (flags.score) {
|
|
46
|
-
const tournamentDir = tournamentDirFor(config, flags.score);
|
|
47
|
-
const board = await scoreTournamentFn({ tournamentDir });
|
|
48
|
-
if (flags.json) {
|
|
49
|
-
logger?.info?.(JSON.stringify(board));
|
|
50
|
-
return board;
|
|
51
|
-
}
|
|
52
|
-
logger?.info?.(`scoreboard ${board.id} — regla: ${board.rule}`);
|
|
53
|
-
for (const r of board.rows) {
|
|
54
|
-
if (!r.finalist) {
|
|
55
|
-
logger?.info?.(` ${r.coder.padEnd(10)} eliminado (${r.eliminated_by})`);
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
const mut = typeof r.mutation?.score === "number" ? `${r.mutation.score}%` : "n/a";
|
|
59
|
-
logger?.info?.(` ${r.coder.padEnd(10)} FINALISTA loc:${String(r.loc.net).padStart(5)} tests:${r.loc.testsTouched} mutation:${mut}`);
|
|
60
|
-
}
|
|
61
|
-
logger?.info?.(` ranking: ${board.ranking.join(" > ") || "(sin finalistas)"}`);
|
|
62
|
-
return board;
|
|
63
|
-
}
|
|
68
|
+
const idAction = ID_ACTIONS.find(([flag]) => flags[flag]);
|
|
69
|
+
if (idAction) return idAction[1]({ config, logger, flags, scoreTournamentFn, judgeTournamentFn, crownWinnerFn });
|
|
64
70
|
|
|
65
71
|
if (!task || !String(task).trim()) {
|
|
66
72
|
throw new Error("tournament: falta la tarea — kj tournament \"<tarea>\" --coders claude,codex");
|
|
@@ -74,12 +80,15 @@ export async function tournamentCommand({
|
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
const res = await runTournamentFn({ task, coders, config, logger });
|
|
83
|
+
printRunSummary(res, logger);
|
|
84
|
+
return res;
|
|
85
|
+
}
|
|
77
86
|
|
|
87
|
+
function printRunSummary(res, logger) {
|
|
78
88
|
const completed = res.lanes.filter((l) => l.status === "completed");
|
|
79
89
|
const green = completed.filter((l) => l.suite?.ok);
|
|
80
90
|
logger?.info?.("");
|
|
81
91
|
logger?.info?.(`torneo ${res.id}: ${completed.length}/${res.lanes.length} carriles completados, ${green.length} con suite verde`);
|
|
82
92
|
logger?.info?.(` evidencia: ${res.dir}`);
|
|
83
93
|
logger?.info?.(" siguiente: el scoreboard determinista (TOR-B) puntuará los carriles; de momento compara los diff.patch a mano.");
|
|
84
|
-
return res;
|
|
85
94
|
}
|
package/src/commands/worktree.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* host agent. Field case: the playbook said "one worktree per task" and an
|
|
4
4
|
* agent NARRATED the isolation while editing the project root. Ordering a
|
|
5
5
|
* command beats ordering a practice: start = worktree + feat/ branch +
|
|
6
|
-
* dependency bootstrap + the exact cd printed; done = remove after merge
|
|
6
|
+
* dependency bootstrap + the exact cd printed; done = remove after merge
|
|
7
|
+
* and delete the branch once it is merged into origin/<base>.
|
|
7
8
|
* Same lane dir (.kj/worktrees) and bootstrap the headless pipeline uses.
|
|
8
9
|
*/
|
|
9
10
|
import fs from "node:fs";
|
|
@@ -11,8 +12,33 @@ import path from "node:path";
|
|
|
11
12
|
import { execFile } from "node:child_process";
|
|
12
13
|
import { promisify } from "node:util";
|
|
13
14
|
import { bootstrapWorktree } from "../hu/worktree-bootstrap.js";
|
|
14
|
-
import {
|
|
15
|
+
import { laneWork, setupLane } from "../hu/lane-setup.js";
|
|
16
|
+
import { classifyLanes, commitsNotIn, liveProcesses, looseLaneBranches, resolveBase } from "../hu/lane-prune.js";
|
|
15
17
|
import { recordGateDecision } from "../policy/decisions.js";
|
|
18
|
+
import { mainCheckout } from "../utils/main-checkout.js";
|
|
19
|
+
import { cardsOfLane, parseLanes, prsByBranch } from "../hu/lanes.js";
|
|
20
|
+
import { listPlans, loadPlan } from "../plan/plan-store.js";
|
|
21
|
+
|
|
22
|
+
const REF_SEPARATOR = " · "; // external_ref joins a tracker card and an issue (src/commands/hu.js)
|
|
23
|
+
|
|
24
|
+
// KJC-TSK-1316: ONE gh call for every lane, open and merged PRs alike.
|
|
25
|
+
const listPrs = async (cwd) => JSON.parse((await execFileAsync("gh", ["pr", "list", "--state", "all", "--limit", "200", "--json", "number,url,state,headRefName"], { cwd })).stdout);
|
|
26
|
+
|
|
27
|
+
/** Each branch's PR; when gh fails the lanes go without PRs, and kj says so. */
|
|
28
|
+
async function lanePrs(list, cwd) {
|
|
29
|
+
try {
|
|
30
|
+
return prsByBranch(await list(cwd));
|
|
31
|
+
} catch (err) {
|
|
32
|
+
console.warn(`! kj worktree list: cannot read the PRs (${String(err.stderr || err.message).trim().split("\n")[0]}); lanes listed without them`);
|
|
33
|
+
return new Map();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Every HU of the project's plans with its references, as `{id, refs}`. */
|
|
38
|
+
async function planCards(projectDir) {
|
|
39
|
+
const plans = await Promise.all((await listPlans(projectDir)).map((m) => loadPlan(projectDir, m.planId)));
|
|
40
|
+
return plans.flatMap((p) => (p?.hus ?? []).map((h) => ({ id: h.id, refs: String(h.external_ref ?? "").split(REF_SEPARATOR).filter(Boolean) })));
|
|
41
|
+
}
|
|
16
42
|
|
|
17
43
|
const execFileAsync = promisify(execFile);
|
|
18
44
|
const WORKTREE_DIR = ".kj/worktrees"; // shared with the headless lanes
|
|
@@ -22,7 +48,10 @@ const SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
|
22
48
|
export async function worktreeCommand({ config = null, action, args = [], flags = {}, deps = {} }) {
|
|
23
49
|
const projectDir = config?.projectDir || process.cwd();
|
|
24
50
|
const git = (gitArgs, cwd = projectDir) => execFileAsync("git", gitArgs, { cwd });
|
|
25
|
-
|
|
51
|
+
// #2566: lanes hang from the main checkout, never from the lane the command runs in.
|
|
52
|
+
const lanesDir = path.join(mainCheckout(projectDir), WORKTREE_DIR);
|
|
53
|
+
const lanePath = (slug) => path.join(lanesDir, slug);
|
|
54
|
+
const baseName = config?.base_branch || "main";
|
|
26
55
|
|
|
27
56
|
if (action === "start") {
|
|
28
57
|
const slug = String(args[0] || "").trim();
|
|
@@ -34,28 +63,48 @@ export async function worktreeCommand({ config = null, action, args = [], flags
|
|
|
34
63
|
throw new Error(`lane "${slug}" already exists — cd ${wtPath}, or finish it with: kj worktree done ${slug}`);
|
|
35
64
|
}
|
|
36
65
|
const branch = `feat/${slug}`;
|
|
66
|
+
// HU-2761: a lane is cut from origin/<base>, never from the HEAD the command runs in (#2566).
|
|
67
|
+
const from = await resolveBase({ git, base: baseName });
|
|
68
|
+
try { await git(["rev-parse", "--verify", "--quiet", `${from}^{commit}`]); } catch {
|
|
69
|
+
throw new Error(`base "${baseName}" not found — set base_branch in kj config`);
|
|
70
|
+
}
|
|
71
|
+
if (from === baseName) {
|
|
72
|
+
console.warn(`! no origin/${baseName} ref — lane starts from local ${baseName}, which may lag (git fetch origin ${baseName} to refresh)`);
|
|
73
|
+
}
|
|
74
|
+
const leftover = await git(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]).then(() => true, () => false);
|
|
75
|
+
if (leftover) {
|
|
76
|
+
throw new Error(`branch ${branch} already exists (kept by an unmerged done?) — delete it with git branch -D ${branch} or pick another slug`);
|
|
77
|
+
}
|
|
37
78
|
// KJC-TSK-1093: --no-track, so no upstream lands in .git/config (read-only in the host sandbox).
|
|
38
|
-
await git(["worktree", "add", "--no-track", "-b", branch, wtPath]);
|
|
79
|
+
await git(["worktree", "add", "--no-track", "-b", branch, wtPath, from]);
|
|
39
80
|
const boot = await bootstrapWorktree({ worktreePath: wtPath, setupCommand: config?.session?.worktree_setup || null });
|
|
40
|
-
|
|
81
|
+
// KJC-TSK-1371 (#2852): identity and Sentinel in the lane; a failure names the step and the way out.
|
|
82
|
+
const retire = `kj worktree done ${slug} --force`;
|
|
83
|
+
const setup = await (deps.setupLane ?? setupLane)({ mainDir: mainCheckout(projectDir), lanePath: wtPath }).catch((err) => {
|
|
84
|
+
throw new Error(`${err.message} — the lane is half set; retire it with: ${retire}`, { cause: err });
|
|
85
|
+
});
|
|
86
|
+
const { stdout: touched } = await git(["status", "--porcelain", "--untracked-files=no"], wtPath);
|
|
87
|
+
if (touched.trim()) throw new Error(`lane setup changed tracked files (${touched.trim().split("\n").join(", ")}) — retire it with: ${retire}`);
|
|
88
|
+
console.log(`✓ lane ready: ${branch} (from ${from})`);
|
|
41
89
|
console.log(` cd ${wtPath}`);
|
|
42
90
|
console.log(" The gates travel with the repo — kj review --staged works there. When merged: kj worktree done " + slug);
|
|
43
|
-
return { path: wtPath, branch, bootstrap: boot };
|
|
91
|
+
return { path: wtPath, branch, from, bootstrap: boot, setup };
|
|
44
92
|
}
|
|
45
93
|
|
|
46
94
|
if (action === "list") {
|
|
47
|
-
const laneRoot = path.join(projectDir, WORKTREE_DIR) + path.sep;
|
|
48
95
|
const { stdout } = await git(["worktree", "list", "--porcelain"]);
|
|
49
|
-
const rows =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
}
|
|
96
|
+
const rows = parseLanes(stdout, { root: lanesDir });
|
|
97
|
+
if (flags.json) {
|
|
98
|
+
// KJC-TSK-1313: what the HU Board paints on each card.
|
|
99
|
+
const base = await resolveBase({ git, base: baseName });
|
|
100
|
+
const cards = await planCards(projectDir);
|
|
101
|
+
const prs = flags.prs ? await lanePrs(deps.listPrs ?? listPrs, projectDir) : null;
|
|
102
|
+
const out = await Promise.all(rows.map(async (r) => ({
|
|
103
|
+
...r, ahead: await commitsNotIn({ git, base, ref: r.branch }).catch(() => null), cards: cardsOfLane(r, cards),
|
|
104
|
+
...(prs && { pr: prs.get(r.branch) ?? null }),
|
|
105
|
+
})));
|
|
106
|
+
console.log(JSON.stringify(out));
|
|
107
|
+
return out;
|
|
59
108
|
}
|
|
60
109
|
if (rows.length === 0) console.log("no lanes — create one with: kj worktree start <slug>");
|
|
61
110
|
for (const r of rows) console.log(`${r.slug.padEnd(24)} ${r.branch.padEnd(30)} ${r.path}`);
|
|
@@ -66,19 +115,38 @@ export async function worktreeCommand({ config = null, action, args = [], flags
|
|
|
66
115
|
const slug = String(args[0] || "").trim();
|
|
67
116
|
const wtPath = lanePath(slug);
|
|
68
117
|
if (!fs.existsSync(wtPath)) throw new Error(`lane "${slug}" not found — see kj worktree list`);
|
|
69
|
-
|
|
70
|
-
|
|
118
|
+
// KJC-TSK-1371: the identity and Sentinel files lane setup wrote are not the lane's work.
|
|
119
|
+
const { stdout: status } = await git(["status", "--porcelain", "--untracked-files=all"], wtPath);
|
|
120
|
+
const dirty = laneWork(status, wtPath).join("\n");
|
|
121
|
+
if (dirty && !flags.force) {
|
|
71
122
|
throw new Error(`lane "${slug}" has uncommitted changes — commit them (through the gate) or discard with --force`);
|
|
72
123
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
124
|
+
// The lane's real branch (headless kj-hu-* lanes share the folder); detached HEAD → null.
|
|
125
|
+
// symbolic-ref -q exits 1 on a detached HEAD; any other failure is a real error.
|
|
126
|
+
const branch = await git(["symbolic-ref", "--short", "-q", "HEAD"], wtPath).then((r) => r.stdout.trim() || null, (err) => {
|
|
127
|
+
if (err.code === 1) return null;
|
|
128
|
+
throw err;
|
|
129
|
+
});
|
|
130
|
+
const base = await resolveBase({ git, base: baseName });
|
|
131
|
+
// A missing base must not block removing a clean lane: the branch is kept, with its remedy.
|
|
132
|
+
const ahead = branch ? await commitsNotIn({ git, base, ref: branch }).catch(() => null) : 0;
|
|
133
|
+
// Only setup files left (checked above): git still calls them untracked, so the remove is forced.
|
|
134
|
+
await git(["worktree", "remove", ...(flags.force || status.trim() ? ["--force"] : []), wtPath]);
|
|
135
|
+
// -D after our own check: -d measures against HEAD/upstream, not origin/<base>. Never the base itself.
|
|
136
|
+
const branchDeleted = Boolean(branch) && branch !== baseName && (ahead === 0 || Boolean(flags.force));
|
|
137
|
+
if (branchDeleted) await git(["branch", "-D", branch]);
|
|
138
|
+
const note = !branch ? "detached HEAD, no branch"
|
|
139
|
+
: branchDeleted ? (ahead === 0 ? `branch ${branch} deleted (merged into ${base})` : `branch ${branch} deleted with --force (${ahead ?? "unknown"} commit(s) not in ${base} were discarded)`)
|
|
140
|
+
: ahead === null ? `branch ${branch} kept: base "${base}" not found, cannot tell if it is merged (git branch -D ${branch} to delete it)`
|
|
141
|
+
: `branch ${branch} kept: ${ahead} commit(s) not in ${base} (squash-merged or not fetched? git fetch, then git branch -D ${branch})`;
|
|
142
|
+
console.log(`✓ lane "${slug}" removed; ${note}`);
|
|
143
|
+
return { removed: true, slug, branch, branchDeleted, merged: branch ? ahead === 0 : null };
|
|
76
144
|
}
|
|
77
145
|
|
|
78
146
|
if (action === "prune") {
|
|
79
147
|
const { processes = liveProcesses() } = deps;
|
|
80
|
-
const base = await resolveBase({ git, base:
|
|
81
|
-
const lanes = await classifyLanes({ git, laneRoot:
|
|
148
|
+
const base = await resolveBase({ git, base: baseName });
|
|
149
|
+
const lanes = await classifyLanes({ git, laneRoot: lanesDir, base, processes, now: deps.now });
|
|
82
150
|
const abandoned = lanes.filter((l) => l.reasons.length === 0);
|
|
83
151
|
const kept = lanes.filter((l) => l.reasons.length > 0);
|
|
84
152
|
for (const l of kept) console.log(`kept ${l.slug.padEnd(24)} ${l.reasons.join("; ")}`);
|
|
@@ -96,7 +164,8 @@ export async function worktreeCommand({ config = null, action, args = [], flags
|
|
|
96
164
|
console.log(`✓ deleted branch ${b}`);
|
|
97
165
|
}
|
|
98
166
|
for (const l of abandoned) {
|
|
99
|
-
|
|
167
|
+
// Judged abandoned above: what is left are setup or regenerable files, which git would refuse.
|
|
168
|
+
await git(["worktree", "remove", "--force", l.path]);
|
|
100
169
|
if (l.branch) await git(["branch", "-D", l.branch]);
|
|
101
170
|
console.log(`✓ retired ${l.slug} (${l.branch ?? "detached"})`);
|
|
102
171
|
}
|
package/src/config/loader.js
CHANGED
|
@@ -27,6 +27,7 @@ import { getKarajanHome } from "../utils/paths.js";
|
|
|
27
27
|
import { safeParseConfig, formatConfigIssues } from "./schema.js";
|
|
28
28
|
import { DEFAULTS } from "./defaults.js";
|
|
29
29
|
import { resolveTestHarness } from "./test-harness.js";
|
|
30
|
+
import { SECRET_PATHS } from "./secrets.js";
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* Parse a YAML config file with an actionable error message.
|
|
@@ -74,13 +75,18 @@ export function getProjectConfigPath(projectDir = process.cwd()) {
|
|
|
74
75
|
return path.join(projectDir, ".karajan", "kj.config.yml");
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
/** ADR 0032: a member's own preferences, ignored by git, on top of the team's project config. */
|
|
79
|
+
export function getLocalConfigPath(projectDir = process.cwd()) {
|
|
80
|
+
return path.join(projectDir, ".karajan", "kj.config.local.yml");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function readYamlIfPresent(filePath) {
|
|
84
|
+
if (!(await exists(filePath))) return null;
|
|
85
|
+
return parseYamlOrThrow(await fs.readFile(filePath, "utf8"), filePath);
|
|
86
|
+
}
|
|
87
|
+
|
|
77
88
|
export async function loadProjectConfig(projectDir = process.cwd()) {
|
|
78
|
-
|
|
79
|
-
if (!(await exists(projectConfigPath))) {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
const raw = await fs.readFile(projectConfigPath, "utf8");
|
|
83
|
-
return parseYamlOrThrow(raw, projectConfigPath);
|
|
89
|
+
return readYamlIfPresent(getProjectConfigPath(projectDir));
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
async function loadProjectPricingOverrides(projectDir = process.cwd()) {
|
|
@@ -113,6 +119,7 @@ export async function loadConfig(projectDir) {
|
|
|
113
119
|
|
|
114
120
|
// Load project config (.karajan/kj.config.yml)
|
|
115
121
|
const projectConfig = await loadProjectConfig(projectDir);
|
|
122
|
+
const localConfig = await readYamlIfPresent(getLocalConfigPath(projectDir));
|
|
116
123
|
|
|
117
124
|
// KJC-TSK-0395: enforce the local-overrides-global hierarchy. A project
|
|
118
125
|
// config with no global counterpart is almost always user error (copied
|
|
@@ -120,19 +127,24 @@ export async function loadConfig(projectDir) {
|
|
|
120
127
|
// would silently behave like a global config without the defaults
|
|
121
128
|
// people expect. Refuse with an actionable message.
|
|
122
129
|
// KJC-BUG-0428 (issue #2694): a CI runner never has a global config; there the
|
|
123
|
-
// team's committed project config sits on the defaults instead.
|
|
124
|
-
|
|
130
|
+
// team's committed project config sits on the defaults instead. A member's local
|
|
131
|
+
// config is never committed, so without a global it is still refused, in CI too.
|
|
132
|
+
if (!globalExists && (localConfig || (projectConfig && !process.env.CI))) {
|
|
133
|
+
const found = projectConfig ? getProjectConfigPath(projectDir) : getLocalConfigPath(projectDir);
|
|
125
134
|
throw new Error(
|
|
126
|
-
`Local config found at ${
|
|
135
|
+
`Local config found at ${found} but no global config exists.\n` +
|
|
127
136
|
"Local configs are merged ON TOP of the global one — run `kj init --global` first to create the base."
|
|
128
137
|
);
|
|
129
138
|
}
|
|
130
139
|
|
|
131
|
-
// Merge: DEFAULTS < global < project
|
|
140
|
+
// Merge: DEFAULTS < global < project (the team's) < local (the member's, ADR 0032)
|
|
132
141
|
let merged = mergeDeep(DEFAULTS, globalConfig);
|
|
133
142
|
if (projectConfig) {
|
|
134
143
|
merged = mergeDeep(merged, projectConfig);
|
|
135
144
|
}
|
|
145
|
+
if (localConfig) {
|
|
146
|
+
merged = mergeDeep(merged, localConfig);
|
|
147
|
+
}
|
|
136
148
|
|
|
137
149
|
if (projectPricing) {
|
|
138
150
|
merged.budget = mergeDeep(merged.budget || {}, { pricing: projectPricing });
|
|
@@ -199,19 +211,24 @@ function stripRuntimeOnlyKeys(config) {
|
|
|
199
211
|
// KJC-BUG-0253: a project's .karajan/kj.config.yml is versioned with the repo,
|
|
200
212
|
// so the SonarQube credentials never go there; they live in the global config
|
|
201
213
|
// (~/.karajan), where the token bootstrap already saves them.
|
|
202
|
-
const SECRET_KEYS = ["token", "admin_password"];
|
|
203
214
|
const isProjectConfig = (configPath) =>
|
|
204
215
|
path.basename(configPath) === "kj.config.yml"
|
|
205
216
|
&& path.basename(path.dirname(configPath)) === ".karajan"
|
|
206
217
|
&& path.resolve(configPath) !== path.resolve(getConfigPath());
|
|
207
218
|
|
|
219
|
+
// KJC-TSK-1234 (ADR 0032): the same secret paths the pre-commit check refuses (SonarCloud's token
|
|
220
|
+
// included), one list for both, so writing never keeps what committing would reject.
|
|
208
221
|
function stripSecrets(config) {
|
|
209
|
-
|
|
210
|
-
const stripped =
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
222
|
+
let out = config;
|
|
223
|
+
const stripped = [];
|
|
224
|
+
for (const [section, key] of SECRET_PATHS) {
|
|
225
|
+
const block = out?.[section];
|
|
226
|
+
if (!block || typeof block !== "object" || block[key] == null) continue;
|
|
227
|
+
const { [key]: _secret, ...kept } = block;
|
|
228
|
+
out = { ...out, [section]: kept };
|
|
229
|
+
stripped.push(`${section}.${key}`);
|
|
230
|
+
}
|
|
231
|
+
return { out, stripped };
|
|
215
232
|
}
|
|
216
233
|
|
|
217
234
|
/** @returns {Promise<{strippedSecrets: string[]}>} the secret keys kept out of a project config */
|