karajan-code 4.1.5 → 4.1.7
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/package.json +1 -1
- package/scripts/install-binary.ps1 +135 -50
- package/src/agents/copilot-agent.js +65 -0
- package/src/agents/index.js +2 -0
- package/src/audit/osv-findings.js +5 -0
- package/src/audit/semgrep-findings.js +8 -0
- package/src/commands/hu.js +23 -4
- package/src/commands/init.js +18 -10
- package/src/commands/plan/add-hu.js +2 -0
- package/src/commands/plan/generate.js +3 -0
- package/src/orchestrator/config-init.js +6 -1
- package/src/plan/plan-fixer.js +1 -0
- package/src/review/gate-gitignore.js +20 -1
- package/src/utils/agent-detect.js +2 -1
- package/src/utils/os-detect.js +5 -0
package/package.json
CHANGED
|
@@ -1,50 +1,158 @@
|
|
|
1
1
|
<#
|
|
2
2
|
.SYNOPSIS
|
|
3
|
-
Karajan Code
|
|
3
|
+
Karajan Code installer for Windows — guarantees a COMPLETE install (KJC-TSK-0667).
|
|
4
4
|
|
|
5
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
6
|
Run it with:
|
|
12
7
|
irm https://karajancode.com/install.ps1 | iex
|
|
13
8
|
|
|
14
|
-
|
|
9
|
+
Default route: npm — the full product (CLI + RAG + HU Board + MCP).
|
|
10
|
+
1. Node >= 22.12 present -> npm install -g karajan-code
|
|
11
|
+
2. No usable Node -> auto-provision the official Node LTS zip into
|
|
12
|
+
~\.karajan\node (checksum-verified, nothing system-wide touched),
|
|
13
|
+
install karajan-code with it, drop kj/karajan-mcp wrappers into the
|
|
14
|
+
install dir and add it to the user PATH.
|
|
15
|
+
Standalone route (CLI only, no native-module features — RAG/board/MCP
|
|
16
|
+
unavailable): opt-in with $env:KJ_STANDALONE = "1".
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
Env overrides (irm|iex cannot take parameters): KJ_VERSION, KJ_INSTALL_DIR,
|
|
19
|
+
KJ_STANDALONE. Windows PowerShell 5.1 compatible.
|
|
17
20
|
#>
|
|
18
21
|
$ErrorActionPreference = "Stop"
|
|
19
22
|
|
|
20
23
|
$repo = "manufosela/karajan-code"
|
|
21
24
|
$version = if ($env:KJ_VERSION) { $env:KJ_VERSION } else { "latest" }
|
|
22
25
|
$installDir = if ($env:KJ_INSTALL_DIR) { $env:KJ_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA "Karajan" }
|
|
23
|
-
$
|
|
26
|
+
$mode = if ($env:KJ_STANDALONE -eq "1") { "standalone" } else { "full" }
|
|
27
|
+
$nodeMajor = 22
|
|
28
|
+
$nodeMinMinor = 12
|
|
29
|
+
|
|
30
|
+
function Get-NpmPkg {
|
|
31
|
+
if ($version -eq "latest") { "karajan-code" } else { "karajan-code@" + $version.TrimStart("v") }
|
|
32
|
+
}
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
$
|
|
27
|
-
|
|
28
|
-
$
|
|
34
|
+
function Test-NodeOk {
|
|
35
|
+
$node = Get-Command node -ErrorAction SilentlyContinue
|
|
36
|
+
if (-not $node) { return $false }
|
|
37
|
+
$v = (& node --version) 2>$null
|
|
38
|
+
if (-not $v) { return $false }
|
|
39
|
+
$parts = $v.TrimStart("v").Split(".")
|
|
40
|
+
$major = [int]$parts[0]; $minor = [int]$parts[1]
|
|
41
|
+
return ($major -gt $nodeMajor) -or ($major -eq $nodeMajor -and $minor -ge $nodeMinMinor)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function Add-UserPath([string]$dir) {
|
|
45
|
+
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
46
|
+
$parts = @()
|
|
47
|
+
if ($userPath) { $parts = $userPath.Split(";") | Where-Object { $_ -ne "" } }
|
|
48
|
+
$already = $parts | Where-Object { $_.TrimEnd("\") -ieq $dir.TrimEnd("\") }
|
|
49
|
+
if (-not $already) {
|
|
50
|
+
$newPath = if ($userPath) { "$userPath;$dir" } else { $dir }
|
|
51
|
+
[Environment]::SetEnvironmentVariable("Path", $newPath, "User")
|
|
52
|
+
Write-Host "kj-install: added '$dir' to your user PATH. Open a NEW terminal to use 'kj'."
|
|
53
|
+
}
|
|
29
54
|
}
|
|
30
55
|
|
|
31
|
-
# Download to a temp folder; only install once the checksum matches, so a
|
|
32
|
-
# failure never leaves a half-installed binary behind.
|
|
33
56
|
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("kj-install-" + [System.Guid]::NewGuid().ToString("N"))
|
|
34
57
|
New-Item -ItemType Directory -Path $tmp -Force | Out-Null
|
|
58
|
+
|
|
35
59
|
try {
|
|
60
|
+
if ($mode -eq "full") {
|
|
61
|
+
if (Test-NodeOk) {
|
|
62
|
+
Write-Host "kj-install: Node $(& node --version) found — installing via npm (full product)..."
|
|
63
|
+
& npm install -g (Get-NpmPkg)
|
|
64
|
+
if ($LASTEXITCODE -ne 0) { throw "npm install failed (exit $LASTEXITCODE)." }
|
|
65
|
+
Write-Host "kj-install: installed. Run 'kj doctor' next, then 'kj install-tools' to complete the whole stack."
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Write-Host "kj-install: no usable Node (need >= $nodeMajor.$nodeMinMinor) — provisioning official Node LTS into ~\.karajan\node (nothing system-wide)..."
|
|
70
|
+
$dist = "https://nodejs.org/dist/latest-v$nodeMajor.x"
|
|
71
|
+
$shaFile = Join-Path $tmp "SHASUMS256.txt"
|
|
72
|
+
Invoke-WebRequest -Uri "$dist/SHASUMS256.txt" -OutFile $shaFile -UseBasicParsing
|
|
73
|
+
$shaLines = Get-Content $shaFile
|
|
74
|
+
$assetLine = $shaLines | Where-Object { $_ -match "node-v[0-9.]+-win-x64\.zip" } | Select-Object -First 1
|
|
75
|
+
if (-not $assetLine) { throw "no official Node build for win-x64 in $dist" }
|
|
76
|
+
$nodeAsset = ($assetLine -split "\s+")[1]
|
|
77
|
+
$expected = ($assetLine -split "\s+")[0].ToLower()
|
|
78
|
+
|
|
79
|
+
Write-Host "kj-install: downloading $nodeAsset..."
|
|
80
|
+
$zip = Join-Path $tmp $nodeAsset
|
|
81
|
+
Invoke-WebRequest -Uri "$dist/$nodeAsset" -OutFile $zip -UseBasicParsing
|
|
82
|
+
$actual = (Get-FileHash -Algorithm SHA256 -Path $zip).Hash.ToLower()
|
|
83
|
+
if ($expected -ne $actual) { throw "Node checksum mismatch — aborting, nothing installed." }
|
|
84
|
+
|
|
85
|
+
# Stage everything (extract + npm install) and only swap into place once
|
|
86
|
+
# EVERYTHING succeeded — a failure must never destroy a previous working
|
|
87
|
+
# ~\.karajan\node.
|
|
88
|
+
$nodeHome = Join-Path $env:USERPROFILE ".karajan\node"
|
|
89
|
+
$staging = "$nodeHome.staging.$PID"
|
|
90
|
+
$extract = Join-Path $tmp "extract"
|
|
91
|
+
if (Test-Path $staging) { Remove-Item -Recurse -Force $staging }
|
|
92
|
+
Expand-Archive -Path $zip -DestinationPath $extract -Force
|
|
93
|
+
# The zip wraps everything in a node-vX.Y.Z-win-x64\ top folder — unwrap it.
|
|
94
|
+
$inner = Get-ChildItem -Directory $extract | Select-Object -First 1
|
|
95
|
+
New-Item -ItemType Directory -Path (Split-Path $staging) -Force | Out-Null
|
|
96
|
+
Move-Item -Path $inner.FullName -Destination $staging
|
|
97
|
+
|
|
98
|
+
Write-Host "kj-install: installing karajan-code with the provisioned Node..."
|
|
99
|
+
$env:Path = "$staging;$env:Path"
|
|
100
|
+
# Explicit --prefix: the official Windows npm ships a builtin prefix of
|
|
101
|
+
# %AppData%\npm — without this the global shims would land OUTSIDE the
|
|
102
|
+
# self-contained ~\.karajan\node home (on Windows, prefix root IS the
|
|
103
|
+
# global bin dir, so shims land at $staging\kj.cmd).
|
|
104
|
+
& (Join-Path $staging "npm.cmd") install -g --prefix "$staging" (Get-NpmPkg)
|
|
105
|
+
if ($LASTEXITCODE -ne 0) { throw "npm install failed with the provisioned Node (exit $LASTEXITCODE)." }
|
|
106
|
+
foreach ($bin in @("kj", "karajan-mcp")) {
|
|
107
|
+
if (-not (Test-Path (Join-Path $staging "$bin.cmd"))) {
|
|
108
|
+
throw "npm reported success but the $bin shim is missing from the staged prefix — the full product needs both kj and karajan-mcp. Aborting, nothing swapped."
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Swap keeping the previous install recoverable: park it as a backup,
|
|
113
|
+
# move the staged one in, restore the backup if that move fails.
|
|
114
|
+
$backup = "$nodeHome.old.$PID"
|
|
115
|
+
if (Test-Path $nodeHome) { Move-Item -Path $nodeHome -Destination $backup -Force }
|
|
116
|
+
try {
|
|
117
|
+
Move-Item -Path $staging -Destination $nodeHome
|
|
118
|
+
} catch {
|
|
119
|
+
if (Test-Path $backup) { Move-Item -Path $backup -Destination $nodeHome -Force }
|
|
120
|
+
throw "could not move the staged install into place (previous install restored). $_"
|
|
121
|
+
}
|
|
122
|
+
if (Test-Path $backup) { Remove-Item -Recurse -Force $backup }
|
|
123
|
+
|
|
124
|
+
# Wrappers, not copies: npm's own .cmd shims live inside nodeHome and
|
|
125
|
+
# resolve node via their folder; these put nodeHome on PATH for children
|
|
126
|
+
# (kj spawns node tooling) and forward every argument.
|
|
127
|
+
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
|
|
128
|
+
foreach ($bin in @("kj", "karajan-mcp")) {
|
|
129
|
+
$shim = Join-Path $nodeHome "$bin.cmd"
|
|
130
|
+
$wrapper = @(
|
|
131
|
+
"@echo off",
|
|
132
|
+
"set `"PATH=$nodeHome;%PATH%`"",
|
|
133
|
+
"`"$shim`" %*"
|
|
134
|
+
)
|
|
135
|
+
Set-Content -Path (Join-Path $installDir "$bin.cmd") -Value $wrapper -Encoding ASCII
|
|
136
|
+
}
|
|
137
|
+
Add-UserPath $installDir
|
|
138
|
+
$installed = (& (Join-Path $installDir "kj.cmd") --version) 2>$null
|
|
139
|
+
Write-Host "kj-install: installed kj $installed (full product) — kj at $(Join-Path $installDir 'kj.cmd')"
|
|
140
|
+
Write-Host "kj-install: next — run 'kj doctor', then 'kj install-tools' to complete the whole stack."
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# --- Standalone route: prebuilt single binary (CLI only). ---
|
|
145
|
+
Write-Host "kj-install: standalone mode — single binary, NO native-module features (RAG, HU Board and MCP need the npm install)."
|
|
146
|
+
$asset = "kj-win-x64.exe"
|
|
147
|
+
$base = if ($version -eq "latest") { "https://github.com/$repo/releases/latest/download" }
|
|
148
|
+
else { "https://github.com/$repo/releases/download/$version" }
|
|
149
|
+
|
|
36
150
|
$binTmp = Join-Path $tmp "kj.exe"
|
|
37
151
|
$shaTmp = Join-Path $tmp "kj.exe.sha256"
|
|
38
|
-
|
|
39
152
|
Write-Host "kj-install: downloading $asset ($version)..."
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
}
|
|
153
|
+
Invoke-WebRequest -Uri "$base/$asset" -OutFile $binTmp -UseBasicParsing
|
|
154
|
+
Invoke-WebRequest -Uri "$base/$asset.sha256" -OutFile $shaTmp -UseBasicParsing
|
|
46
155
|
|
|
47
|
-
# certutil writes a multi-line file; keep only the hex of the hash line.
|
|
48
156
|
$shaLine = (Get-Content $shaTmp | Where-Object { $_ -notmatch "SHA256|CertUtil" } | Select-Object -First 1)
|
|
49
157
|
$expected = ($shaLine -replace "[^0-9A-Fa-f]", "").ToLower()
|
|
50
158
|
$actual = (Get-FileHash -Algorithm SHA256 -Path $binTmp).Hash.ToLower()
|
|
@@ -52,38 +160,15 @@ try {
|
|
|
52
160
|
throw "checksum mismatch (expected '$expected', got '$actual'). Aborting, nothing installed."
|
|
53
161
|
}
|
|
54
162
|
|
|
55
|
-
# Install: move into place, replacing any previous install (idempotent).
|
|
56
163
|
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
|
|
57
164
|
$dest = Join-Path $installDir "kj.exe"
|
|
58
165
|
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.
|
|
166
|
+
# Clear the mark-of-the-web on the copy we just checksum-verified ourselves.
|
|
63
167
|
Unblock-File -Path $dest -ErrorAction SilentlyContinue
|
|
64
|
-
|
|
168
|
+
Add-UserPath $installDir
|
|
65
169
|
$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
|
-
|
|
81
|
-
# The binary bundles no toolchain. kj orchestrates external tools, so name
|
|
82
|
-
# the hard requirements and let `kj doctor` check them for this machine.
|
|
83
|
-
Write-Host ""
|
|
84
|
-
Write-Host "kj-install: next step — run 'kj doctor' to check prerequisites."
|
|
85
|
-
Write-Host " Required: git, plus at least one agent CLI (Claude Code, Codex or Gemini)."
|
|
86
|
-
Write-Host " Optional: Docker (local models, SonarQube) and Node/npm (helper tools: Squeezr, qmd)."
|
|
170
|
+
Write-Host "kj-install: installed standalone kj $installed to $dest"
|
|
171
|
+
Write-Host "kj-install: next — run 'kj doctor'. For the full product later: re-run without KJ_STANDALONE."
|
|
87
172
|
} finally {
|
|
88
173
|
Remove-Item -Path $tmp -Recurse -Force -ErrorAction SilentlyContinue
|
|
89
174
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { BaseAgent } from "./base-agent.js";
|
|
2
|
+
import { resolveBin } from "./resolve-bin.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GitHub Copilot CLI (@github/copilot, binary `copilot`) — KJC-TSK-0666.
|
|
6
|
+
* The seventh built-in agent, and the user-chosen third AI for solomon
|
|
7
|
+
* arbitration (gemini tier retired, Qwen OAuth free tier discontinued).
|
|
8
|
+
*
|
|
9
|
+
* Verified live against copilot 1.0.73:
|
|
10
|
+
* - `-p <prompt>` runs non-interactive; copilot has NO stdin prompt channel
|
|
11
|
+
* (stdin is treated as context, spawning an agentic run) — so the prompt
|
|
12
|
+
* travels as an argument. Same documented E2BIG limit as claude-agent
|
|
13
|
+
* (KJC-BUG-0121) for multi-hundred-KB diffs.
|
|
14
|
+
* - `--output-format json` emits JSONL; the final text lives in the
|
|
15
|
+
* `assistant.message` event's `data.content`, run metadata in `result`.
|
|
16
|
+
* - Default model routing is automatic and varies per run (haiku, gpt-5-mini
|
|
17
|
+
* observed) — pin it per role via `roles.<role>.model` when neutrality
|
|
18
|
+
* matters (e.g. a gemini-family model for solomon).
|
|
19
|
+
*/
|
|
20
|
+
export class CopilotAgent extends BaseAgent {
|
|
21
|
+
async runTask(task) {
|
|
22
|
+
// Coder mode IS an agentic run: file edits and shell need permissions.
|
|
23
|
+
return this._exec(task, this.getRoleModel(task.role || "coder"), ["--allow-all-tools"]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async reviewTask(task) {
|
|
27
|
+
// Review/arbitration answers from the prompt alone — no tool grants, and
|
|
28
|
+
// an explicit no-tools instruction so copilot never stalls waiting for
|
|
29
|
+
// an approval that headless mode cannot give.
|
|
30
|
+
return this._exec(
|
|
31
|
+
{ ...task, prompt: `Do NOT use any tools. Answer directly from this prompt only.\n\n${task.prompt}` },
|
|
32
|
+
this.getRoleModel(task.role || "reviewer"),
|
|
33
|
+
[],
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async _exec(task, model, extraArgs) {
|
|
38
|
+
const args = ["-p", task.prompt, "--output-format", "json", "--no-color", ...extraArgs];
|
|
39
|
+
if (model) args.push("--model", model);
|
|
40
|
+
const res = await this.runCommand(resolveBin("copilot"), args, {
|
|
41
|
+
onOutput: task.onOutput,
|
|
42
|
+
silenceTimeoutMs: task.silenceTimeoutMs,
|
|
43
|
+
timeout: task.timeoutMs
|
|
44
|
+
});
|
|
45
|
+
const output = extractCopilotOutput(res.stdout);
|
|
46
|
+
return { ok: res.exitCode === 0, output: output ?? res.stdout, error: res.stderr, exitCode: res.exitCode };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Last `assistant.message` content from copilot's JSONL stream (null if none). */
|
|
51
|
+
export function extractCopilotOutput(stdout) {
|
|
52
|
+
if (!stdout) return null;
|
|
53
|
+
let content = null;
|
|
54
|
+
for (const line of stdout.split("\n")) {
|
|
55
|
+
const t = line.trim();
|
|
56
|
+
if (!t.startsWith("{")) continue;
|
|
57
|
+
try {
|
|
58
|
+
const obj = JSON.parse(t);
|
|
59
|
+
if (obj?.type === "assistant.message" && typeof obj?.data?.content === "string") {
|
|
60
|
+
content = obj.data.content;
|
|
61
|
+
}
|
|
62
|
+
} catch { /* non-JSON noise between events */ }
|
|
63
|
+
}
|
|
64
|
+
return content;
|
|
65
|
+
}
|
package/src/agents/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { GeminiAgent } from "./gemini-agent.js";
|
|
|
4
4
|
import { AiderAgent } from "./aider-agent.js";
|
|
5
5
|
import { OpenCodeAgent } from "./opencode-agent.js";
|
|
6
6
|
import { QwenAgent } from "./qwen-agent.js";
|
|
7
|
+
import { CopilotAgent } from "./copilot-agent.js";
|
|
7
8
|
|
|
8
9
|
const agentRegistry = new Map();
|
|
9
10
|
|
|
@@ -51,3 +52,4 @@ registerAgent("gemini", GeminiAgent, { bin: "gemini", installUrl: "https://githu
|
|
|
51
52
|
registerAgent("aider", AiderAgent, { bin: "aider", installUrl: "https://aider.chat/docs/install.html" });
|
|
52
53
|
registerAgent("opencode", OpenCodeAgent, { bin: "opencode", installUrl: "https://opencode.ai" });
|
|
53
54
|
registerAgent("qwen", QwenAgent, { bin: "qwen", installUrl: "https://github.com/QwenLM/qwen-code" });
|
|
55
|
+
registerAgent("copilot", CopilotAgent, { bin: "copilot", installUrl: "https://docs.github.com/copilot/how-tos/use-copilot-agents/use-copilot-cli" });
|
|
@@ -35,6 +35,11 @@ const SEVERITY_ORDER = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "MODERATE"];
|
|
|
35
35
|
* @returns {Promise<{available: boolean, reason?: string, total?: number, vulnerabilities?: object[]}>}
|
|
36
36
|
*/
|
|
37
37
|
export async function collectOsvFindings(projectDir, logger = null) {
|
|
38
|
+
// KJC-BUG-0122: same kill-switch as semgrep — external scanners never
|
|
39
|
+
// fire from the unit suite; e2e opts in with KJ_ALLOW_REAL_SCANS=1.
|
|
40
|
+
if (process.env.VITEST && process.env.KJ_ALLOW_REAL_SCANS !== "1") {
|
|
41
|
+
return { available: false, reason: "real scans disabled under the test runner (set KJ_ALLOW_REAL_SCANS=1 to opt in)" };
|
|
42
|
+
}
|
|
38
43
|
let stdout;
|
|
39
44
|
try {
|
|
40
45
|
const { stdout: out } = await execFileAsync(
|
|
@@ -34,6 +34,14 @@ const SEVERITY_ORDER = ["ERROR", "WARNING", "INFO"];
|
|
|
34
34
|
* @returns {Promise<{available: boolean, reason?: string, total?: number, findings?: object[]}>}
|
|
35
35
|
*/
|
|
36
36
|
export async function collectSemgrepFindings(projectDir, logger = null) {
|
|
37
|
+
// KJC-BUG-0122: a full `--config auto` scan must NEVER fire from the unit
|
|
38
|
+
// suite — on machines with the complete stack installed (the product
|
|
39
|
+
// default since v4.1.2) parallel vitest workers each launched a real
|
|
40
|
+
// whole-repo scan: load average 50, orphaned semgrep-core processes.
|
|
41
|
+
// E2E tests that genuinely want the real scan opt in explicitly.
|
|
42
|
+
if (process.env.VITEST && process.env.KJ_ALLOW_REAL_SCANS !== "1") {
|
|
43
|
+
return { available: false, reason: "real scans disabled under the test runner (set KJ_ALLOW_REAL_SCANS=1 to opt in)" };
|
|
44
|
+
}
|
|
37
45
|
let stdout;
|
|
38
46
|
try {
|
|
39
47
|
const { stdout: out } = await execFileAsync(
|
package/src/commands/hu.js
CHANGED
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
import { addHu, updateHuStatus } from "../plan/plan-hu-ops.js";
|
|
9
9
|
import { generatePlanId } from "../plan/plan-id.js";
|
|
10
10
|
import { savePlan, listPlans, loadPlan } from "../plan/plan-store.js";
|
|
11
|
+
import { detectHostAgent } from "../utils/agent-detect.js";
|
|
12
|
+
|
|
13
|
+
// KJC-TSK-0661 (Jorge's friction): every HU records WHO created it — the
|
|
14
|
+
// board must never show work nobody can trace. Host agent env → that agent;
|
|
15
|
+
// a human typing in a terminal → "human"; anything else headless → "agent".
|
|
16
|
+
export function creatorLabel() {
|
|
17
|
+
return detectHostAgent() || (process.stdin.isTTY ? "human" : "agent");
|
|
18
|
+
}
|
|
11
19
|
|
|
12
20
|
export const HU_STATUSES = ["pending", "running", "done", "failed", "skipped"];
|
|
13
21
|
const BACKLOG_NAME = "brain-backlog";
|
|
@@ -35,12 +43,22 @@ export async function huCommand({ config = null, action, args = [], flags = {} }
|
|
|
35
43
|
for (const meta of plans) {
|
|
36
44
|
const plan = await loadPlan(projectDir, meta.planId);
|
|
37
45
|
for (const h of plan.hus || []) {
|
|
38
|
-
rows.push({
|
|
46
|
+
rows.push({
|
|
47
|
+
id: h.id, short_id: h.short_id, title: h.title, status: h.status,
|
|
48
|
+
plan: plan.alias || plan.planId,
|
|
49
|
+
// KJC-TSK-0661: provenance — where a card came from is part of
|
|
50
|
+
// the card. Older HUs predate the stamp: shown as "?".
|
|
51
|
+
created_by: h.created_by || null, created_at: h.createdAt || null,
|
|
52
|
+
});
|
|
39
53
|
}
|
|
40
54
|
}
|
|
41
55
|
if (flags.json) { console.log(JSON.stringify(rows)); return rows; }
|
|
42
|
-
for (const r of rows)
|
|
56
|
+
for (const r of rows) {
|
|
57
|
+
const origin = `${r.plan} · by ${r.created_by || "?"}${r.created_at ? ` · ${r.created_at.slice(0, 10)}` : ""}`;
|
|
58
|
+
console.log(`${(r.short_id || r.id).padEnd(28)} ${r.status.padEnd(8)} ${r.title} [${origin}]`);
|
|
59
|
+
}
|
|
43
60
|
if (rows.length === 0) console.log("no HUs yet — create one with: kj hu add \"<story>\"");
|
|
61
|
+
else console.log("→ work one: ask your agent, or kj run --plan <plan> · discard one: kj hu move <id> skipped");
|
|
44
62
|
return rows;
|
|
45
63
|
}
|
|
46
64
|
|
|
@@ -53,9 +71,10 @@ export async function huCommand({ config = null, action, args = [], flags = {} }
|
|
|
53
71
|
short_id: flags.id || null,
|
|
54
72
|
acceptance_criteria: flags.criteria ? [flags.criteria] : [],
|
|
55
73
|
});
|
|
74
|
+
hu.created_by = creatorLabel();
|
|
56
75
|
await savePlan(projectDir, plan);
|
|
57
|
-
return emit({ id: hu.id, short_id: hu.short_id, status: hu.status },
|
|
58
|
-
`✓ HU created: ${hu.short_id || hu.id} (pending) — it shows up in \`kj board\``);
|
|
76
|
+
return emit({ id: hu.id, short_id: hu.short_id, status: hu.status, created_by: hu.created_by },
|
|
77
|
+
`✓ HU created: ${hu.short_id || hu.id} (pending, by ${hu.created_by}) — it shows up in \`kj board\``);
|
|
59
78
|
}
|
|
60
79
|
|
|
61
80
|
if (action === "move") {
|
package/src/commands/init.js
CHANGED
|
@@ -6,6 +6,7 @@ import { sonarUp, checkVmMaxMapCount } from "../sonar/manager.js";
|
|
|
6
6
|
import { ollamaUp, waitForOllamaReady, normalizeOllamaConfig } from "../rag/ollama-manager.js";
|
|
7
7
|
import { checkOllamaCapability, pullOllamaModel } from "../rag/ollama-capability.js";
|
|
8
8
|
import { exists, ensureDir } from "../utils/fs.js";
|
|
9
|
+
import { ensureContractBlockPresent } from "../review/gate-gitignore.js";
|
|
9
10
|
import { getKarajanHome } from "../utils/paths.js";
|
|
10
11
|
import { getTemplatesRoot } from "../utils/templates-root.js";
|
|
11
12
|
import { detectAvailableAgents } from "../utils/agent-detect.js";
|
|
@@ -672,6 +673,7 @@ const GITIGNORE_ENTRIES = [
|
|
|
672
673
|
{ pattern: ".kj/", comment: "Karajan runtime (logs, worktrees)" },
|
|
673
674
|
{ pattern: ".agent/", comment: "Agent skills (OpenSkills)" },
|
|
674
675
|
{ pattern: ".scannerwork/", comment: "SonarQube scanner temp" },
|
|
676
|
+
{ pattern: ".reviews/", comment: "Legacy review artifacts" },
|
|
675
677
|
];
|
|
676
678
|
|
|
677
679
|
async function ensureGitignoreEntries(projectDir, logger) {
|
|
@@ -685,16 +687,22 @@ async function ensureGitignoreEntries(projectDir, logger) {
|
|
|
685
687
|
if (!content) content = "";
|
|
686
688
|
|
|
687
689
|
const missing = GITIGNORE_ENTRIES.filter(e => !content.includes(e.pattern));
|
|
688
|
-
if (missing.length
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
690
|
+
if (missing.length > 0) {
|
|
691
|
+
const block = [
|
|
692
|
+
"",
|
|
693
|
+
"# Karajan Code",
|
|
694
|
+
...missing.map(e => e.pattern)
|
|
695
|
+
].join("\n") + "\n";
|
|
696
|
+
await fs.appendFile(gitignorePath, block, "utf8");
|
|
697
|
+
logger.info(`Added to .gitignore: ${missing.map(e => e.pattern).join(", ")}`);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// KJC-BUG-0123 (issue #1268): the .karajan entries were missing entirely —
|
|
701
|
+
// and they must land as the CONTRACT BLOCK (`.karajan/*` + re-includes),
|
|
702
|
+
// never a bare `.karajan/` dir-exclude, or git cannot re-include the
|
|
703
|
+
// review-gate/hooks/adrs the whole team inherits (KJC-TSK-0646).
|
|
704
|
+
const res = await ensureContractBlockPresent(projectDir);
|
|
705
|
+
if (res.changed) logger.info("Added the .karajan contract block to .gitignore (verdicts stay local; gate/hooks/adrs tracked)");
|
|
698
706
|
}
|
|
699
707
|
|
|
700
708
|
/**
|
|
@@ -15,6 +15,8 @@ export async function planAddHuCommand({ config, planId, title, type, deps, scop
|
|
|
15
15
|
scope: scope || null,
|
|
16
16
|
blocked_by: deps ? deps.split(",").map(d => d.trim()) : []
|
|
17
17
|
});
|
|
18
|
+
const { creatorLabel } = await import("../hu.js");
|
|
19
|
+
hu.created_by = creatorLabel(); // KJC-TSK-0661: provenance stamp
|
|
18
20
|
|
|
19
21
|
await savePlan(projectDir, plan);
|
|
20
22
|
console.log(`Added: ${hu.id} — ${hu.title}`);
|
|
@@ -229,6 +229,9 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
229
229
|
reviewer_model: models.reviewer_model,
|
|
230
230
|
reviewer_provider: models.reviewer_provider,
|
|
231
231
|
});
|
|
232
|
+
// KJC-TSK-0661: provenance stamp (addHu builds its object explicitly,
|
|
233
|
+
// so this goes on the returned reference, not in huData).
|
|
234
|
+
hu.created_by = "planner";
|
|
232
235
|
if (symbolicId) symbolicToHuId.set(symbolicId, hu.id);
|
|
233
236
|
const deps = typeof step === "object" && Array.isArray(step.dependencies) ? step.dependencies : [];
|
|
234
237
|
stepDeps.push(deps);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import fs from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { computeBaseRef, setSnapshot } from "../review/diff-generator.js";
|
|
8
|
+
import { ensureContractBlockPresent } from "../review/gate-gitignore.js";
|
|
8
9
|
import { revParse } from "../utils/git.js";
|
|
9
10
|
import { buildCoderPrompt } from "../prompts/coder.js";
|
|
10
11
|
import { buildReviewerPrompt } from "../prompts/reviewer.js";
|
|
@@ -55,8 +56,11 @@ export async function autoInit(projectDir, logger) {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
// Ensure .gitignore exists with universal entries only (stack-specific added after planner)
|
|
59
|
+
// KJC-BUG-0123: `.karajan/` is NOT in this list on purpose — a bare
|
|
60
|
+
// dir-exclude breaks the gate contract (git cannot re-include children);
|
|
61
|
+
// the canonical block is appended below from its single source.
|
|
58
62
|
const gitignorePath = path.join(projectDir, ".gitignore");
|
|
59
|
-
const universalIgnores = [".env", "*.log", ".DS_Store", ".
|
|
63
|
+
const universalIgnores = [".env", "*.log", ".DS_Store", ".reviews/"];
|
|
60
64
|
try {
|
|
61
65
|
let content = "";
|
|
62
66
|
if (await exists(gitignorePath)) {
|
|
@@ -68,6 +72,7 @@ export async function autoInit(projectDir, logger) {
|
|
|
68
72
|
await fs.appendFile(gitignorePath, append, "utf8");
|
|
69
73
|
logger.info(`Created .gitignore with universal entries`);
|
|
70
74
|
}
|
|
75
|
+
await ensureContractBlockPresent(projectDir);
|
|
71
76
|
} catch (err) {
|
|
72
77
|
logger.warn(`Failed to create .gitignore: ${err.message}`);
|
|
73
78
|
}
|
package/src/plan/plan-fixer.js
CHANGED
|
@@ -140,6 +140,7 @@ export function applyFixerPatch(plan, patch) {
|
|
|
140
140
|
acceptance_tests: a.acceptance_tests,
|
|
141
141
|
reuse: a.reuse || [],
|
|
142
142
|
});
|
|
143
|
+
hu.created_by = "plan-reviewer"; // KJC-TSK-0661: provenance stamp
|
|
143
144
|
if (symbolicId) symbolicToHuId.set(symbolicId, hu.id);
|
|
144
145
|
const depsSymbolic = Array.isArray(a.dependencies) ? a.dependencies : [];
|
|
145
146
|
addedHus.push({ hu, depsSymbolic });
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
import fs from "node:fs/promises";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// KJC-BUG-0123 (issue #1268): exported as the SINGLE source of truth — kj
|
|
16
|
+
// init and the orchestrator's autoInit write this same block instead of a
|
|
17
|
+
// bare `.karajan/` exclude (which git cannot re-include children of).
|
|
18
|
+
export const CONTRACT_BLOCK = [
|
|
16
19
|
".karajan/*",
|
|
17
20
|
"# …except the v4 environment contract, which the whole team inherits:",
|
|
18
21
|
"!.karajan/review-gate",
|
|
@@ -25,6 +28,22 @@ const CONTRACT_BLOCK = [
|
|
|
25
28
|
"!.karajan/adrs/",
|
|
26
29
|
];
|
|
27
30
|
|
|
31
|
+
/**
|
|
32
|
+
* KJC-BUG-0123: append the canonical contract block to .gitignore when the
|
|
33
|
+
* file has no `.karajan` mention at all (fresh projects — `kj init` path).
|
|
34
|
+
* Files that already mention .karajan are left to ensureGateTrackable,
|
|
35
|
+
* which rewrites legacy root excludes without touching anything else.
|
|
36
|
+
*/
|
|
37
|
+
export async function ensureContractBlockPresent(projectDir) {
|
|
38
|
+
const file = path.join(projectDir, ".gitignore");
|
|
39
|
+
let text = "";
|
|
40
|
+
try { text = (await fs.readFile(file, "utf8")) || ""; } catch { /* no .gitignore yet */ }
|
|
41
|
+
if (text.includes(".karajan")) return { changed: false };
|
|
42
|
+
const sep = text && !text.endsWith("\n") ? "\n" : "";
|
|
43
|
+
await fs.writeFile(file, `${text}${sep}# Karajan environment (verdicts stay local; the contract is tracked)\n${CONTRACT_BLOCK.join("\n")}\n`);
|
|
44
|
+
return { changed: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
28
47
|
export async function ensureGateTrackable(projectDir) {
|
|
29
48
|
const file = path.join(projectDir, ".gitignore");
|
|
30
49
|
let text;
|
|
@@ -8,7 +8,8 @@ const KNOWN_AGENTS = [
|
|
|
8
8
|
{ name: "gemini", install: getInstallCommand("gemini") },
|
|
9
9
|
{ name: "aider", install: getInstallCommand("aider") },
|
|
10
10
|
{ name: "opencode", install: getInstallCommand("opencode") },
|
|
11
|
-
{ name: "qwen", install: getInstallCommand("qwen") }
|
|
11
|
+
{ name: "qwen", install: getInstallCommand("qwen") },
|
|
12
|
+
{ name: "copilot", install: getInstallCommand("copilot") }
|
|
12
13
|
];
|
|
13
14
|
|
|
14
15
|
export async function checkBinary(name, versionArg = "--version") {
|
package/src/utils/os-detect.js
CHANGED
|
@@ -56,6 +56,11 @@ const INSTALL_COMMANDS = {
|
|
|
56
56
|
linux: "npm install -g @qwen-code/qwen-code",
|
|
57
57
|
windows: "npm install -g @qwen-code/qwen-code"
|
|
58
58
|
},
|
|
59
|
+
copilot: {
|
|
60
|
+
macos: "npm install -g @github/copilot",
|
|
61
|
+
linux: "npm install -g @github/copilot",
|
|
62
|
+
windows: "npm install -g @github/copilot"
|
|
63
|
+
},
|
|
59
64
|
docker: {
|
|
60
65
|
macos: "brew install --cask docker",
|
|
61
66
|
linux: "sudo apt install docker.io docker-compose-v2 (or see https://docs.docker.com/engine/install/)",
|