karajan-code 3.7.2 → 3.9.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 +46 -1
- package/package.json +1 -1
- package/scripts/install-binary.ps1 +82 -0
- package/scripts/install-binary.sh +103 -0
- package/src/cli/advanced-commands.js +61 -0
- package/src/cli/register-meta.js +49 -0
- package/src/cli.js +17 -4
- package/src/commands/advanced.js +31 -0
- package/src/commands/harden.js +2 -1
- package/src/commands/mutate.js +94 -0
- package/src/commands/start.js +117 -0
- package/src/config/defaults.js +2 -1
- package/src/config/schema.js +1 -0
- package/src/guards/secret-redactor.js +71 -0
- package/src/harden/workflow-engine.js +4 -2
- package/src/harden/workflow-templates.js +52 -0
- package/src/mutate/diff-scope.js +101 -0
- package/src/mutate/reviewer-signal.js +76 -0
- package/src/mutate/runner.js +72 -0
- package/src/mutate/tool-registry.js +90 -0
- package/src/orchestrator/stages/reviewer-stage.js +9 -3
- package/src/prompts/start-decision.js +52 -0
- package/src/roles/reviewer-role.js +7 -0
- package/src/start/assessment.js +74 -0
- package/src/start/maturity.js +73 -0
- package/src/start/start-decider-role.js +79 -0
- package/src/start/sweep.js +112 -0
- package/src/utils/update-check.js +38 -1
package/README.md
CHANGED
|
@@ -112,6 +112,16 @@ brew install manufosela/tap/karajan-code
|
|
|
112
112
|
|
|
113
113
|
> **Installing with pnpm?** pnpm blocks dependency build scripts by default, so `better-sqlite3`'s native addon won't compile on install and DB-backed commands (`board`, `rag`, cost tracking) will fail. After installing, run `pnpm approve-builds better-sqlite3` — or just use npm. `kj doctor` flags this if it happens.
|
|
114
114
|
|
|
115
|
+
> **On a bare Linux box, install `curl` first.** The binary and script installers
|
|
116
|
+
> below download with `curl` (or `wget`). macOS already ships `curl` and Windows
|
|
117
|
+
> PowerShell has `irm` built in, but a minimal server or container image often has
|
|
118
|
+
> neither — install it with your package manager, then run the one-liner:
|
|
119
|
+
> - **Debian/Ubuntu**: `sudo apt update && sudo apt install -y curl`
|
|
120
|
+
> - **Fedora/RHEL/CentOS**: `sudo dnf install -y curl`
|
|
121
|
+
> - **Arch**: `sudo pacman -S --noconfirm curl`
|
|
122
|
+
> - **Alpine**: `sudo apk add curl`
|
|
123
|
+
> - **openSUSE**: `sudo zypper install -y curl`
|
|
124
|
+
|
|
115
125
|
**Standalone binary** (no Node.js needed):
|
|
116
126
|
```bash
|
|
117
127
|
# macOS (Apple Silicon)
|
|
@@ -124,7 +134,28 @@ curl -L https://github.com/manufosela/karajan-code/releases/latest/download/kj-l
|
|
|
124
134
|
curl -L https://github.com/manufosela/karajan-code/releases/latest/download/kj-win-x64.exe -o kj.exe
|
|
125
135
|
```
|
|
126
136
|
|
|
127
|
-
**One-liner** (detects OS, installs
|
|
137
|
+
**One-liner, binary** (no Node — detects OS/arch, verifies the checksum, installs to `~/.local/bin`):
|
|
138
|
+
```bash
|
|
139
|
+
curl -fsSL https://raw.githubusercontent.com/manufosela/karajan-code/main/scripts/install-binary.sh | sh
|
|
140
|
+
```
|
|
141
|
+
Pin a version or install dir with env vars: `KJ_VERSION=v3.7.2 KJ_INSTALL_DIR=/usr/local/bin`.
|
|
142
|
+
|
|
143
|
+
**One-liner, binary — Windows** (no Node — verifies the checksum, installs to `%LOCALAPPDATA%\Karajan` and adds it to your user PATH):
|
|
144
|
+
```powershell
|
|
145
|
+
irm https://raw.githubusercontent.com/manufosela/karajan-code/main/scripts/install-binary.ps1 | iex
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
> **If your OS blocks the binary.** The macOS build is ad-hoc signed and the
|
|
149
|
+
> Windows build is unsigned — neither carries a paid certificate, so a manually
|
|
150
|
+
> downloaded binary may be blocked on first run. The install scripts above clear
|
|
151
|
+
> the flag for you. If you download the binary by hand instead:
|
|
152
|
+
> - **macOS** — Gatekeeper says the binary "cannot be opened": `xattr -d com.apple.quarantine ./kj`, or Control-click the file → **Open**.
|
|
153
|
+
> - **Windows** — SmartScreen shows "Windows protected your PC": click **More info → Run anyway**, or run `Unblock-File .\kj.exe`.
|
|
154
|
+
>
|
|
155
|
+
> Paid notarization (Apple) and Authenticode signing (Windows) are optional and
|
|
156
|
+
> not enabled by default — this is a deliberate cost decision, not an oversight.
|
|
157
|
+
|
|
158
|
+
**One-liner, npm** (detects OS, installs via npm — needs Node ≥ 18):
|
|
128
159
|
```bash
|
|
129
160
|
curl -fsSL https://raw.githubusercontent.com/manufosela/karajan-code/main/scripts/install-kj.sh | sh
|
|
130
161
|
```
|
|
@@ -235,8 +266,22 @@ kj board start # Start web dashboard (po
|
|
|
235
266
|
kj board open # Start + open in browser
|
|
236
267
|
kj board status # Check if running
|
|
237
268
|
kj board stop # Stop the board
|
|
269
|
+
|
|
270
|
+
# Quality guardrails
|
|
271
|
+
kj harden # Install hooks, config, CI, guidelines
|
|
272
|
+
kj check # Verify the harness (drift gate)
|
|
273
|
+
kj mutate # Mutation-test the diff (do tests catch mutants?)
|
|
238
274
|
```
|
|
239
275
|
|
|
276
|
+
**Mutation testing** (`kj mutate`) closes the gap coverage leaves: it flips the
|
|
277
|
+
logic of the code you just changed and reruns your suite, so a line that ran but
|
|
278
|
+
isn't really tested shows up as a **survivor**. It is diff-scoped by default
|
|
279
|
+
(vs `HEAD~1`), drives the right runner per stack (Stryker/mutmut/Infection/…
|
|
280
|
+
with no silent fallback), and folds into a workflow three ways — on demand,
|
|
281
|
+
as an opt-in reviewer signal (`KJ_REVIEW_MUTATION=1`), or as a non-blocking
|
|
282
|
+
nightly CI job (`kj harden --mutation`). See
|
|
283
|
+
[GETTING-STARTED](docs/GETTING-STARTED.md#mutation-testing--kj-mutate).
|
|
284
|
+
|
|
240
285
|
### 2. MCP: inside your AI agent
|
|
241
286
|
|
|
242
287
|
This is the primary use case. Karajan runs as an MCP server inside Claude Code, Codex, or Gemini. You ask your AI agent to do something, and it delegates the heavy lifting to Karajan's pipeline.
|
package/package.json
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Karajan Code — standalone binary installer for Windows (no Node required).
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Downloads the prebuilt kj.exe for Windows x64 from the GitHub release,
|
|
7
|
+
verifies its SHA256 checksum, installs it to %LOCALAPPDATA%\Karajan, and
|
|
8
|
+
adds that folder to the user PATH (idempotently). Re-running updates the
|
|
9
|
+
binary in place without duplicating PATH entries.
|
|
10
|
+
|
|
11
|
+
Run it with:
|
|
12
|
+
irm https://karajancode.com/install.ps1 | iex
|
|
13
|
+
|
|
14
|
+
Override with env vars: $env:KJ_VERSION (e.g. v3.7.2), $env:KJ_INSTALL_DIR.
|
|
15
|
+
|
|
16
|
+
KJC-TSK-0594.
|
|
17
|
+
#>
|
|
18
|
+
$ErrorActionPreference = "Stop"
|
|
19
|
+
|
|
20
|
+
$repo = "manufosela/karajan-code"
|
|
21
|
+
$version = if ($env:KJ_VERSION) { $env:KJ_VERSION } else { "latest" }
|
|
22
|
+
$installDir = if ($env:KJ_INSTALL_DIR) { $env:KJ_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA "Karajan" }
|
|
23
|
+
$asset = "kj-win-x64.exe"
|
|
24
|
+
|
|
25
|
+
if ($version -eq "latest") {
|
|
26
|
+
$base = "https://github.com/$repo/releases/latest/download"
|
|
27
|
+
} else {
|
|
28
|
+
$base = "https://github.com/$repo/releases/download/$version"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Download to a temp folder; only install once the checksum matches, so a
|
|
32
|
+
# failure never leaves a half-installed binary behind.
|
|
33
|
+
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("kj-install-" + [System.Guid]::NewGuid().ToString("N"))
|
|
34
|
+
New-Item -ItemType Directory -Path $tmp -Force | Out-Null
|
|
35
|
+
try {
|
|
36
|
+
$binTmp = Join-Path $tmp "kj.exe"
|
|
37
|
+
$shaTmp = Join-Path $tmp "kj.exe.sha256"
|
|
38
|
+
|
|
39
|
+
Write-Host "kj-install: downloading $asset ($version)..."
|
|
40
|
+
try {
|
|
41
|
+
Invoke-WebRequest -Uri "$base/$asset" -OutFile $binTmp -UseBasicParsing
|
|
42
|
+
Invoke-WebRequest -Uri "$base/$asset.sha256" -OutFile $shaTmp -UseBasicParsing
|
|
43
|
+
} catch {
|
|
44
|
+
throw "could not download $base/$asset — does that version/asset exist, and is the network reachable? ($_)"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# certutil writes a multi-line file; keep only the hex of the hash line.
|
|
48
|
+
$shaLine = (Get-Content $shaTmp | Where-Object { $_ -notmatch "SHA256|CertUtil" } | Select-Object -First 1)
|
|
49
|
+
$expected = ($shaLine -replace "[^0-9A-Fa-f]", "").ToLower()
|
|
50
|
+
$actual = (Get-FileHash -Algorithm SHA256 -Path $binTmp).Hash.ToLower()
|
|
51
|
+
if ($expected.Length -ne 64 -or $expected -ne $actual) {
|
|
52
|
+
throw "checksum mismatch (expected '$expected', got '$actual'). Aborting, nothing installed."
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Install: move into place, replacing any previous install (idempotent).
|
|
56
|
+
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
|
|
57
|
+
$dest = Join-Path $installDir "kj.exe"
|
|
58
|
+
Move-Item -Path $binTmp -Destination $dest -Force
|
|
59
|
+
|
|
60
|
+
# The download carries a "mark of the web" (Zone.Identifier) that makes
|
|
61
|
+
# SmartScreen warn on first run. The binary is not code-signed with a paid
|
|
62
|
+
# certificate, so clear the mark on the copy we just verified ourselves.
|
|
63
|
+
Unblock-File -Path $dest -ErrorAction SilentlyContinue
|
|
64
|
+
|
|
65
|
+
$installed = (& $dest --version) 2>$null
|
|
66
|
+
Write-Host "kj-install: installed kj $installed to $dest"
|
|
67
|
+
|
|
68
|
+
# Add to the user PATH only if it is not already there (no duplicates).
|
|
69
|
+
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
70
|
+
$parts = @()
|
|
71
|
+
if ($userPath) { $parts = $userPath.Split(";") | Where-Object { $_ -ne "" } }
|
|
72
|
+
$already = $parts | Where-Object { $_.TrimEnd("\") -ieq $installDir.TrimEnd("\") }
|
|
73
|
+
if (-not $already) {
|
|
74
|
+
$newPath = if ($userPath) { "$userPath;$installDir" } else { $installDir }
|
|
75
|
+
[Environment]::SetEnvironmentVariable("Path", $newPath, "User")
|
|
76
|
+
Write-Host "kj-install: added '$installDir' to your user PATH. Open a new terminal to use 'kj'."
|
|
77
|
+
} else {
|
|
78
|
+
Write-Host "kj-install: '$installDir' is already on your user PATH — run 'kj --help' to get started."
|
|
79
|
+
}
|
|
80
|
+
} finally {
|
|
81
|
+
Remove-Item -Path $tmp -Recurse -Force -ErrorAction SilentlyContinue
|
|
82
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Karajan Code — standalone binary installer (no Node required).
|
|
3
|
+
#
|
|
4
|
+
# curl -fsSL https://karajancode.com/install.sh | sh
|
|
5
|
+
#
|
|
6
|
+
# Downloads the prebuilt `kj` binary for your OS/arch from the GitHub
|
|
7
|
+
# release, verifies its SHA256 checksum, and installs it to ~/.local/bin.
|
|
8
|
+
# Override with env vars: KJ_VERSION (e.g. v3.7.2), KJ_INSTALL_DIR.
|
|
9
|
+
#
|
|
10
|
+
# POSIX sh only — no bashisms, no Node. KJC-TSK-0593.
|
|
11
|
+
set -eu
|
|
12
|
+
|
|
13
|
+
REPO="manufosela/karajan-code"
|
|
14
|
+
VERSION="${KJ_VERSION:-latest}"
|
|
15
|
+
INSTALL_DIR="${KJ_INSTALL_DIR:-$HOME/.local/bin}"
|
|
16
|
+
|
|
17
|
+
die() {
|
|
18
|
+
echo "kj-install: $1" >&2
|
|
19
|
+
exit 1
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# --- Detect OS/arch, map to the release asset names we actually build. ---
|
|
23
|
+
os="$(uname -s)"
|
|
24
|
+
arch="$(uname -m)"
|
|
25
|
+
case "$os" in
|
|
26
|
+
Linux) os="linux" ;;
|
|
27
|
+
Darwin) os="darwin" ;;
|
|
28
|
+
*) die "unsupported OS '$os'. Supported: Linux, macOS. Use npm instead: npm install -g karajan-code" ;;
|
|
29
|
+
esac
|
|
30
|
+
case "$arch" in
|
|
31
|
+
x86_64 | amd64) arch="x64" ;;
|
|
32
|
+
arm64 | aarch64) arch="arm64" ;;
|
|
33
|
+
*) die "unsupported architecture '$arch'" ;;
|
|
34
|
+
esac
|
|
35
|
+
|
|
36
|
+
target="${os}-${arch}"
|
|
37
|
+
case "$target" in
|
|
38
|
+
linux-x64 | darwin-arm64) ;;
|
|
39
|
+
*) die "no prebuilt binary for '$target'. Available: linux-x64, darwin-arm64. Use npm instead: npm install -g karajan-code" ;;
|
|
40
|
+
esac
|
|
41
|
+
|
|
42
|
+
# --- Resolve the download URL for the requested version (or latest). ---
|
|
43
|
+
asset="kj-${target}"
|
|
44
|
+
if [ "$VERSION" = "latest" ]; then
|
|
45
|
+
base="https://github.com/${REPO}/releases/latest/download"
|
|
46
|
+
else
|
|
47
|
+
base="https://github.com/${REPO}/releases/download/${VERSION}"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# --- Pick a downloader. ---
|
|
51
|
+
if command -v curl >/dev/null 2>&1; then
|
|
52
|
+
fetch() { curl -fsSL "$1" -o "$2"; }
|
|
53
|
+
elif command -v wget >/dev/null 2>&1; then
|
|
54
|
+
fetch() { wget -qO "$2" "$1"; }
|
|
55
|
+
else
|
|
56
|
+
die "need curl or wget to download the binary"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# --- Pick a checksum tool. ---
|
|
60
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
61
|
+
sha256() { sha256sum "$1" | cut -d' ' -f1; }
|
|
62
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
63
|
+
sha256() { shasum -a 256 "$1" | cut -d' ' -f1; }
|
|
64
|
+
else
|
|
65
|
+
die "need sha256sum or shasum to verify the download"
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# --- Download to a temp dir; only install after the checksum matches. ---
|
|
69
|
+
tmp="$(mktemp -d "${TMPDIR:-/tmp}/kj-install.XXXXXX")"
|
|
70
|
+
trap 'rm -rf "$tmp"' EXIT INT TERM
|
|
71
|
+
|
|
72
|
+
echo "kj-install: downloading ${asset} (${VERSION})..."
|
|
73
|
+
fetch "${base}/${asset}" "${tmp}/kj" || die "could not download ${base}/${asset} — does that version/asset exist?"
|
|
74
|
+
fetch "${base}/${asset}.sha256" "${tmp}/kj.sha256" || die "could not download the checksum for ${asset}"
|
|
75
|
+
|
|
76
|
+
expected="$(cut -d' ' -f1 <"${tmp}/kj.sha256")"
|
|
77
|
+
actual="$(sha256 "${tmp}/kj")"
|
|
78
|
+
[ "$expected" = "$actual" ] || die "checksum mismatch (expected ${expected}, got ${actual}). Aborting, nothing installed."
|
|
79
|
+
|
|
80
|
+
# --- Install atomically: chmod on the temp file, then move into place. ---
|
|
81
|
+
chmod +x "${tmp}/kj"
|
|
82
|
+
mkdir -p "$INSTALL_DIR"
|
|
83
|
+
mv -f "${tmp}/kj" "${INSTALL_DIR}/kj"
|
|
84
|
+
|
|
85
|
+
# On macOS the binary is ad-hoc signed, not notarized with a paid Apple
|
|
86
|
+
# certificate, so Gatekeeper can quarantine it. Clear the flag on the copy
|
|
87
|
+
# we just checksummed ourselves (no-op on Linux / if the flag is absent).
|
|
88
|
+
if [ "$os" = "darwin" ] && command -v xattr >/dev/null 2>&1; then
|
|
89
|
+
xattr -d com.apple.quarantine "${INSTALL_DIR}/kj" 2>/dev/null || true
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
installed="$("${INSTALL_DIR}/kj" --version 2>/dev/null || echo '?')"
|
|
93
|
+
echo "kj-install: installed kj ${installed} to ${INSTALL_DIR}/kj"
|
|
94
|
+
|
|
95
|
+
# --- Tell the user how to reach it if the dir is not on PATH. ---
|
|
96
|
+
case ":${PATH}:" in
|
|
97
|
+
*":${INSTALL_DIR}:"*) echo "kj-install: '${INSTALL_DIR}' is on your PATH — run 'kj --help' to get started." ;;
|
|
98
|
+
*)
|
|
99
|
+
echo "kj-install: '${INSTALL_DIR}' is not on your PATH. Add it with:"
|
|
100
|
+
echo " export PATH=\"${INSTALL_DIR}:\$PATH\""
|
|
101
|
+
echo " (add that line to ~/.bashrc, ~/.zshrc or ~/.profile to make it permanent)"
|
|
102
|
+
;;
|
|
103
|
+
esac
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// KJC-TSK-0582: the flat `kj --help` list had grown to 37 commands, which
|
|
2
|
+
// drowns the handful a newcomer actually needs. This module is the single
|
|
3
|
+
// source of truth for which commands are "core" (always shown in `kj --help`)
|
|
4
|
+
// vs "advanced/specialized" (grouped under `kj advanced`). Nothing is hidden
|
|
5
|
+
// or unregistered — every command stays top-level and invokable for
|
|
6
|
+
// back-compat; this only changes what the help listing surfaces by default.
|
|
7
|
+
|
|
8
|
+
/** The few commands a newcomer needs. Shown in `kj --help`. */
|
|
9
|
+
export const CORE_COMMANDS = [
|
|
10
|
+
"start",
|
|
11
|
+
"init",
|
|
12
|
+
"run",
|
|
13
|
+
"plan",
|
|
14
|
+
"status",
|
|
15
|
+
"doctor",
|
|
16
|
+
"harden",
|
|
17
|
+
"config",
|
|
18
|
+
"update",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/** Navigation/built-ins that are neither core-basics nor advanced. */
|
|
22
|
+
export const META_COMMANDS = ["advanced", "help"];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Advanced commands grouped by area, in display order. Every advanced
|
|
26
|
+
* command MUST live in exactly one group — the parity test fails otherwise,
|
|
27
|
+
* so a newly-registered command can never silently vanish from `kj advanced`.
|
|
28
|
+
*/
|
|
29
|
+
export const ADVANCED_GROUPS = [
|
|
30
|
+
{ title: "Pipeline (piezas sueltas)", commands: ["autorun", "code", "review", "scan"] },
|
|
31
|
+
{ title: "Análisis pre-run", commands: ["discover", "triage", "researcher", "architect", "onboard"] },
|
|
32
|
+
{ title: "Búsqueda / RAG", commands: ["rag", "qmd", "watch"] },
|
|
33
|
+
{ title: "Calidad / auditoría", commands: ["audit", "check", "mutate", "webperf", "sonar"] },
|
|
34
|
+
{ title: "Sesión / board", commands: ["resume", "report", "board", "undo", "standby"] },
|
|
35
|
+
{ title: "Infra / setup", commands: ["install-tools", "ollama", "skills", "roles", "agents"] },
|
|
36
|
+
{ title: "Mantenimiento", commands: ["clean", "sync", "telemetry"] },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** Flat set of every advanced command name (for fast lookup / filtering). */
|
|
40
|
+
export const ADVANCED_COMMANDS = ADVANCED_GROUPS.flatMap((g) => g.commands);
|
|
41
|
+
|
|
42
|
+
const ADVANCED_SET = new Set(ADVANCED_COMMANDS);
|
|
43
|
+
const CORE_SET = new Set(CORE_COMMANDS);
|
|
44
|
+
const META_SET = new Set(META_COMMANDS);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Classify a command name. Returns "core" | "advanced" | "meta" | "unknown".
|
|
48
|
+
* "unknown" means the command is registered but not yet placed in this file —
|
|
49
|
+
* the parity test treats that as a failure so the listing never drifts.
|
|
50
|
+
*/
|
|
51
|
+
export function classifyCommand(name) {
|
|
52
|
+
if (CORE_SET.has(name)) return "core";
|
|
53
|
+
if (ADVANCED_SET.has(name)) return "advanced";
|
|
54
|
+
if (META_SET.has(name)) return "meta";
|
|
55
|
+
return "unknown";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** True when the command should be hidden from the flat `kj --help` list. */
|
|
59
|
+
export function isAdvancedCommand(name) {
|
|
60
|
+
return ADVANCED_SET.has(name);
|
|
61
|
+
}
|
package/src/cli/register-meta.js
CHANGED
|
@@ -3,6 +3,7 @@ import { triageCommand } from "../commands/triage.js";
|
|
|
3
3
|
import { researcherCommand } from "../commands/researcher.js";
|
|
4
4
|
import { architectCommand } from "../commands/architect.js";
|
|
5
5
|
import { onboardCommand } from "../commands/onboard.js";
|
|
6
|
+
import { startCommand } from "../commands/start.js";
|
|
6
7
|
import { ragIndexCommand, ragQueryCommand, ragInstallHooksCommand, ragEvalCommand } from "../commands/rag.js";
|
|
7
8
|
import { qmdQueryCommand } from "../commands/qmd.js";
|
|
8
9
|
import { ragMcpCommand } from "../commands/rag-mcp.js";
|
|
@@ -15,8 +16,10 @@ import { undoCommand } from "../commands/undo.js";
|
|
|
15
16
|
import { syncCommand } from "../commands/sync.js";
|
|
16
17
|
import { cleanCommand } from "../commands/clean.js";
|
|
17
18
|
import { checkCommand } from "../commands/check.js";
|
|
19
|
+
import { mutateCommand } from "../commands/mutate.js";
|
|
18
20
|
import { hardenCommand } from "../commands/harden.js";
|
|
19
21
|
import { telemetryPreviewCommand, telemetryStatusCommand } from "../commands/telemetry.js";
|
|
22
|
+
import { formatAdvancedIndex } from "../commands/advanced.js";
|
|
20
23
|
import { withConfig } from "./_shared.js";
|
|
21
24
|
|
|
22
25
|
/**
|
|
@@ -105,6 +108,19 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
105
108
|
});
|
|
106
109
|
});
|
|
107
110
|
|
|
111
|
+
program
|
|
112
|
+
.command("start")
|
|
113
|
+
.description("Single entry point: assess the project and recommend the next step")
|
|
114
|
+
.argument("[task]", "What you want to do (optional natural-language goal)")
|
|
115
|
+
.option("--maturity <type>", "Declare project maturity: new|existing|legacy")
|
|
116
|
+
.option("--yes", "Non-interactive: emit assessment + recommendation, apply nothing")
|
|
117
|
+
.option("--json", "Output the assessment + decision as JSON")
|
|
118
|
+
.action(async (task, flags) => {
|
|
119
|
+
await withConfig(pkgVersion, "start", flags, async ({ config, logger }) => {
|
|
120
|
+
await startCommand({ task, config, logger, flags });
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
108
124
|
const rag = program.command("rag").description("Retrieval-augmented search over Karajan plans, onboarding briefs and project code");
|
|
109
125
|
rag.command("index")
|
|
110
126
|
.description("Index plans + onboarding (and optionally project sources) into the local vector store")
|
|
@@ -406,6 +422,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
406
422
|
.option("--no-config", "Skip lint/format/commit config files (hooks only)")
|
|
407
423
|
.option("--no-ci", "Skip CI quality workflows")
|
|
408
424
|
.option("--no-guidelines", "Skip AI-agent guideline files (AGENTS.md/CLAUDE.md)")
|
|
425
|
+
.option("--mutation", "Also seed an opt-in nightly/manual mutation CI job (never a PR gate)")
|
|
409
426
|
.option("--only <dirs...>", "Harden only these language roots (e.g. frontend backend)")
|
|
410
427
|
.option("--exclude <globs...>", "Skip language roots matching these globs (e.g. wrappers)")
|
|
411
428
|
.option("--report", "Read-only: show what kj would add/improve, change nothing")
|
|
@@ -419,6 +436,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
419
436
|
config: flags.config !== false,
|
|
420
437
|
ci: flags.ci !== false,
|
|
421
438
|
guidelines: flags.guidelines !== false,
|
|
439
|
+
mutation: Boolean(flags.mutation),
|
|
422
440
|
only: flags.only ?? [],
|
|
423
441
|
exclude: flags.exclude ?? [],
|
|
424
442
|
report: Boolean(flags.report),
|
|
@@ -441,4 +459,35 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
441
459
|
);
|
|
442
460
|
if (Number.isInteger(code)) process.exit(code);
|
|
443
461
|
});
|
|
462
|
+
|
|
463
|
+
program
|
|
464
|
+
.command("mutate")
|
|
465
|
+
.description("Diff-scoped mutation testing (¿cazan tus tests los mutantes de tu cambio?)")
|
|
466
|
+
.argument("[path]", "Target explícito a mutar (omite el diff)")
|
|
467
|
+
.option("--since <ref>", "Ref git contra la que sacar el diff", "HEAD~1")
|
|
468
|
+
.option("--max-survivors <n>", "Supervivientes tolerados antes de exit 1", "0")
|
|
469
|
+
.option("--json", "Emitir el resultado normalizado como JSON")
|
|
470
|
+
.action(async (pathArg, flags) => {
|
|
471
|
+
const code = await withConfig(pkgVersion, "mutate", flags, async ({ logger }) =>
|
|
472
|
+
mutateCommand({
|
|
473
|
+
path: pathArg,
|
|
474
|
+
since: flags.since,
|
|
475
|
+
maxSurvivors: Number(flags.maxSurvivors),
|
|
476
|
+
json: Boolean(flags.json),
|
|
477
|
+
logger,
|
|
478
|
+
})
|
|
479
|
+
);
|
|
480
|
+
if (Number.isInteger(code)) process.exit(code);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// KJC-TSK-0582: index of the advanced/specialized commands kept out of the
|
|
484
|
+
// flat `kj --help` list. Descriptions are read from commander itself so they
|
|
485
|
+
// never drift from each command's own --description.
|
|
486
|
+
program
|
|
487
|
+
.command("advanced")
|
|
488
|
+
.description("List advanced & specialized commands, grouped by area")
|
|
489
|
+
.action(() => {
|
|
490
|
+
const descriptions = Object.fromEntries(program.commands.map((c) => [c.name(), c.description()]));
|
|
491
|
+
console.log(formatAdvancedIndex(descriptions));
|
|
492
|
+
});
|
|
444
493
|
}
|
package/src/cli.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { Command } from "commander";
|
|
5
|
+
import { Command, Help } from "commander";
|
|
6
6
|
import { loadConfig } from "./config.js";
|
|
7
|
+
import { isAdvancedCommand } from "./cli/advanced-commands.js";
|
|
7
8
|
import { registerPipeline } from "./cli/register-pipeline.js";
|
|
8
9
|
import { registerPlan } from "./cli/register-plan.js";
|
|
9
10
|
import { registerRolesSkills } from "./cli/register-roles-skills.js";
|
|
@@ -41,8 +42,20 @@ program
|
|
|
41
42
|
.allowUnknownOption(true)
|
|
42
43
|
.allowExcessArguments(true);
|
|
43
44
|
|
|
44
|
-
// KJC-TSK-
|
|
45
|
-
//
|
|
45
|
+
// KJC-TSK-0582: the flat list had grown to 37 commands. Show only the core
|
|
46
|
+
// basics in `kj --help`; the advanced/specialized ones live under
|
|
47
|
+
// `kj advanced`. The filter applies to the ROOT help only — subcommand help
|
|
48
|
+
// keeps listing its own subcommands untouched.
|
|
49
|
+
program.configureHelp({
|
|
50
|
+
visibleCommands(cmd) {
|
|
51
|
+
const cmds = Help.prototype.visibleCommands.call(this, cmd);
|
|
52
|
+
if (cmd !== program) return cmds;
|
|
53
|
+
return cmds.filter((c) => !isAdvancedCommand(c.name()));
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// KJC-TSK-0571/0582: orient a newcomer to the few commands they need and
|
|
58
|
+
// point them at `kj advanced` for everything else.
|
|
46
59
|
program.addHelpText(
|
|
47
60
|
"after",
|
|
48
61
|
`
|
|
@@ -53,7 +66,7 @@ Getting started (the basics — start here):
|
|
|
53
66
|
kj doctor Check your environment is ready
|
|
54
67
|
kj harden Add quality git hooks + CI to any repo
|
|
55
68
|
|
|
56
|
-
|
|
69
|
+
kj advanced List every advanced/specialized command, grouped by area`
|
|
57
70
|
);
|
|
58
71
|
|
|
59
72
|
registerPipeline(program, { pkgVersion: PKG_VERSION });
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// KJC-TSK-0582: `kj advanced` prints every advanced/specialized command,
|
|
2
|
+
// grouped by area, with the one-line description commander already holds for
|
|
3
|
+
// it. Pure formatter (takes a name→description map) so it's trivially
|
|
4
|
+
// testable without spinning up the whole CLI.
|
|
5
|
+
import { ADVANCED_GROUPS } from "../cli/advanced-commands.js";
|
|
6
|
+
|
|
7
|
+
const NAME_PAD = 14;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build the `kj advanced` index text.
|
|
11
|
+
* @param {Record<string,string>|Map<string,string>} descriptions name → one-line description
|
|
12
|
+
*/
|
|
13
|
+
export function formatAdvancedIndex(descriptions) {
|
|
14
|
+
const lookup = descriptions instanceof Map ? descriptions : new Map(Object.entries(descriptions || {}));
|
|
15
|
+
const firstLine = (text) => String(text || "").split("\n")[0].trim();
|
|
16
|
+
|
|
17
|
+
const lines = [
|
|
18
|
+
"Comandos avanzados y especializados (agrupados por área).",
|
|
19
|
+
"Todos siguen siendo invocables directamente: kj <comando>.",
|
|
20
|
+
"",
|
|
21
|
+
];
|
|
22
|
+
for (const group of ADVANCED_GROUPS) {
|
|
23
|
+
lines.push(`${group.title}:`);
|
|
24
|
+
for (const name of group.commands) {
|
|
25
|
+
lines.push(` kj ${name.padEnd(NAME_PAD)} ${firstLine(lookup.get(name))}`.trimEnd());
|
|
26
|
+
}
|
|
27
|
+
lines.push("");
|
|
28
|
+
}
|
|
29
|
+
lines.push("Usa 'kj <comando> --help' para los detalles de cualquiera de ellos.");
|
|
30
|
+
return lines.join("\n");
|
|
31
|
+
}
|
package/src/commands/harden.js
CHANGED
|
@@ -76,6 +76,7 @@ export async function hardenCommand({
|
|
|
76
76
|
config = true,
|
|
77
77
|
ci = true,
|
|
78
78
|
guidelines = true,
|
|
79
|
+
mutation = false,
|
|
79
80
|
dryRun = false,
|
|
80
81
|
json = false,
|
|
81
82
|
report = false,
|
|
@@ -134,7 +135,7 @@ export async function hardenCommand({
|
|
|
134
135
|
const cfg = withConfig ? installConfigsForRoots({ projectDir, roots, dryRun }) : null;
|
|
135
136
|
const withCi = ci && profile !== "minimal";
|
|
136
137
|
const wf = withCi
|
|
137
|
-
? installWorkflows({ projectDir, language: roots[0]?.language ?? null, profile, dryRun })
|
|
138
|
+
? installWorkflows({ projectDir, language: roots[0]?.language ?? null, profile, mutation, dryRun })
|
|
138
139
|
: null;
|
|
139
140
|
const withGuidelines = guidelines && profile !== "minimal";
|
|
140
141
|
const gl = withGuidelines ? installGuidelines({ projectDir, dryRun }) : null;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `kj mutate` (KJC-TSK-0587) — diff-scoped mutation testing. Detects language
|
|
3
|
+
* (M-A registry) → changed lines (M-B diff-scope) → runs the tool (M-C runner)
|
|
4
|
+
* only over what you just touched. `--json` + exit code make it a gate. No
|
|
5
|
+
* silent fallback: unsupported language / unreadable report exit non-zero.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { detectProjectStack } from "../utils/stack-detect.js";
|
|
11
|
+
import { getMutationTool } from "../mutate/tool-registry.js";
|
|
12
|
+
import { getDiffScope } from "../mutate/diff-scope.js";
|
|
13
|
+
import { runMutation } from "../mutate/runner.js";
|
|
14
|
+
|
|
15
|
+
// Per-tool launch detail: subcommand + JSON report path. Tools without a JSON
|
|
16
|
+
// report leave `report` empty → the run surfaces the tool's own output.
|
|
17
|
+
const INVOCATION = {
|
|
18
|
+
stryker: { base: ["run"], report: "reports/mutation/mutation.json" },
|
|
19
|
+
mutmut: { base: ["run"], report: "mutmut-report.json" },
|
|
20
|
+
infection: { base: [], report: "infection.json" },
|
|
21
|
+
"go-mutesting": { base: [], report: "" },
|
|
22
|
+
pitest: { base: [], report: "" },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const DEFAULT_SINCE = "HEAD~1";
|
|
26
|
+
|
|
27
|
+
function makeReadReport(reportFile) {
|
|
28
|
+
if (!reportFile) return undefined;
|
|
29
|
+
return async () => JSON.parse(await fs.readFile(reportFile, "utf8"));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `deps` ({detectStack, diffScope, run}) is injectable so tests stay pure.
|
|
34
|
+
* `since` diffs against a git ref (default HEAD~1); `path` bypasses the diff.
|
|
35
|
+
* @returns {Promise<number>} process exit code
|
|
36
|
+
*/
|
|
37
|
+
export async function mutateCommand({
|
|
38
|
+
projectDir = process.cwd(),
|
|
39
|
+
since,
|
|
40
|
+
path: pathArg,
|
|
41
|
+
json = false,
|
|
42
|
+
maxSurvivors = 0,
|
|
43
|
+
logger = console,
|
|
44
|
+
deps = {},
|
|
45
|
+
} = {}) {
|
|
46
|
+
const detectStack = deps.detectStack ?? detectProjectStack;
|
|
47
|
+
const diffScope = deps.diffScope ?? getDiffScope;
|
|
48
|
+
const run = deps.run ?? runMutation;
|
|
49
|
+
const say = (msg) => logger.info?.(msg);
|
|
50
|
+
|
|
51
|
+
const { language } = await detectStack(projectDir);
|
|
52
|
+
const tool = getMutationTool(language);
|
|
53
|
+
if (!tool.supported) {
|
|
54
|
+
say(`kj mutate: lenguaje no soportado (${language ?? "desconocido"}) — ${tool.reason}`);
|
|
55
|
+
return 2;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let scope;
|
|
59
|
+
if (pathArg) {
|
|
60
|
+
const args = tool.scope.flag ? [tool.scope.flag, pathArg] : [pathArg];
|
|
61
|
+
scope = { supported: true, empty: false, args };
|
|
62
|
+
} else {
|
|
63
|
+
scope = await diffScope({ since: since ?? DEFAULT_SINCE, language, projectDir });
|
|
64
|
+
}
|
|
65
|
+
if (scope.empty) {
|
|
66
|
+
say("kj mutate: nada que mutar en el diff.");
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const invocation = INVOCATION[tool.id] ?? { base: [], report: "" };
|
|
71
|
+
const reportFile = invocation.report ? path.join(projectDir, invocation.report) : "";
|
|
72
|
+
const outcome = await run({
|
|
73
|
+
binary: tool.binary,
|
|
74
|
+
args: [...invocation.base, ...scope.args],
|
|
75
|
+
cwd: projectDir,
|
|
76
|
+
readReport: makeReadReport(reportFile),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!outcome.result) {
|
|
80
|
+
say(`kj mutate: la herramienta ${tool.id} no produjo un informe legible.`);
|
|
81
|
+
if (outcome.stderr) say(outcome.stderr.trim());
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { score, killed, total, survived } = outcome.result;
|
|
86
|
+
if (json) {
|
|
87
|
+
say(JSON.stringify(outcome.result));
|
|
88
|
+
} else {
|
|
89
|
+
say(`kj mutate (${tool.id}): score ${score ?? "n/a"}% — ${killed}/${total} mutantes cazados`);
|
|
90
|
+
for (const s of survived) say(` ✗ superviviente ${s.file}:${s.line} (${s.mutator ?? s.status})`);
|
|
91
|
+
if (survived.length === 0) say(" ✓ sin supervivientes");
|
|
92
|
+
}
|
|
93
|
+
return survived.length > maxSurvivors ? 1 : 0;
|
|
94
|
+
}
|