karajan-code 4.1.6 → 4.1.8
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/packages/hu-board/package.json +1 -1
- package/packages/hu-board/src/routes/api.js +9 -14
- package/packages/hu-board/src/sync.js +13 -1
- package/scripts/install-binary.ps1 +135 -50
- package/src/audit/osv-findings.js +5 -0
- package/src/audit/semgrep-findings.js +8 -0
- package/src/commands/hu.js +36 -4
- package/src/commands/init.js +18 -10
- package/src/commands/plan/add-hu.js +2 -0
- package/src/commands/plan/fix.js +1 -1
- package/src/commands/plan/generate.js +5 -2
- package/src/environment/briefs.js +12 -0
- package/src/environment/playbook.js +2 -2
- package/src/orchestrator/config-init.js +6 -1
- package/src/plan/plan-fixer.js +27 -4
- package/src/review/gate-gitignore.js +20 -1
package/package.json
CHANGED
|
@@ -32,7 +32,6 @@ import {
|
|
|
32
32
|
getSessionsByProject,
|
|
33
33
|
getSessionDetail,
|
|
34
34
|
deleteProject,
|
|
35
|
-
deleteStory,
|
|
36
35
|
deleteSession,
|
|
37
36
|
getKjHome,
|
|
38
37
|
getHuBoardRunsDir,
|
|
@@ -462,19 +461,15 @@ router.delete('/prompts/:promptId', async (req, res) => {
|
|
|
462
461
|
* it. Pre-tombstones this endpoint was a DB-only soft hide that the next
|
|
463
462
|
* chokidar event undid silently.
|
|
464
463
|
*/
|
|
465
|
-
router.delete('/stories/:id',
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
res.json({ deleted: true, dirRemoved });
|
|
475
|
-
} catch (err) {
|
|
476
|
-
res.status(500).json({ error: err.message });
|
|
477
|
-
}
|
|
464
|
+
router.delete('/stories/:id', (req, res) => {
|
|
465
|
+
// KJC-TSK-0671 (absolute product rule): stories are NEVER deleted — the
|
|
466
|
+
// delete-and-recreate "fix" agents improvise loses history and produced
|
|
467
|
+
// the field saga this closes. Discard goes through the lifecycle instead.
|
|
468
|
+
res.status(405).json({
|
|
469
|
+
error: 'Stories are never deleted. Discard one with: kj hu move <id> skipped '
|
|
470
|
+
+ '(archived, history kept). Dashboard warnings are fixed with the command '
|
|
471
|
+
+ 'they name — see kj brief board.',
|
|
472
|
+
});
|
|
478
473
|
});
|
|
479
474
|
|
|
480
475
|
/**
|
|
@@ -358,7 +358,19 @@ export function syncPlanFile(filePath) {
|
|
|
358
358
|
try {
|
|
359
359
|
const raw = readFileSync(filePath, 'utf-8');
|
|
360
360
|
const data = JSON.parse(raw);
|
|
361
|
-
|
|
361
|
+
// KJC-BUG-0124 (issue #1277): a strict `version !== 2` silently hid
|
|
362
|
+
// every plan whose version drifted — the board showed 0 stories with no
|
|
363
|
+
// trace, and users concluded their work was lost. Tolerant now: any
|
|
364
|
+
// v2+ plan syncs, with a LOUD warn on version drift (fired before the
|
|
365
|
+
// empty check so drift is visible even on empty plans). Pre-v2 plans
|
|
366
|
+
// keep their historical silent skip (they never synced and carry no
|
|
367
|
+
// hus[]), and an empty v2 plan has nothing to sync — skipping it
|
|
368
|
+
// loses nothing.
|
|
369
|
+
if (typeof data.version !== 'number' || data.version < 2) return;
|
|
370
|
+
if (data.version > 2) {
|
|
371
|
+
console.warn(`[sync] ${filePath}: plan version ${data.version} (expected 2) — syncing anyway; re-save with current kj to normalize`);
|
|
372
|
+
}
|
|
373
|
+
if (!Array.isArray(data.hus) || data.hus.length === 0) return;
|
|
362
374
|
|
|
363
375
|
// Bug fix: pre-patch, `projectId = data.planId` — every plan was a separate
|
|
364
376
|
// "project" on the board, so 2 464 plans across the same repo produced
|
|
@@ -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
|
}
|
|
@@ -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
|
|
|
@@ -48,14 +66,28 @@ export async function huCommand({ config = null, action, args = [], flags = {} }
|
|
|
48
66
|
const title = args[0];
|
|
49
67
|
if (!title || !title.trim()) throw new Error("kj hu add requires a title: kj hu add \"<story>\"");
|
|
50
68
|
const plan = await backlogPlan(projectDir);
|
|
69
|
+
// KJC-TSK-0669 (absolute rule): cards are permanent. The delete-and-
|
|
70
|
+
// recreate "fix" an agent improvises loses history and duplicates ids —
|
|
71
|
+
// refuse it with the norm spelled out.
|
|
72
|
+
if (flags.id) {
|
|
73
|
+
const dup = (plan.hus || []).find((h) => h.short_id === flags.id);
|
|
74
|
+
if (dup) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`HU "${flags.id}" already exists (status: ${dup.status}). HUs are never deleted or recreated — `
|
|
77
|
+
+ `update its state with: kj hu move ${flags.id} <pending|running|done|failed|skipped>, `
|
|
78
|
+
+ `or discard it with: kj hu move ${flags.id} skipped`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
51
82
|
const hu = addHu(plan, {
|
|
52
83
|
title,
|
|
53
84
|
short_id: flags.id || null,
|
|
54
85
|
acceptance_criteria: flags.criteria ? [flags.criteria] : [],
|
|
55
86
|
});
|
|
87
|
+
hu.created_by = creatorLabel();
|
|
56
88
|
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\``);
|
|
89
|
+
return emit({ id: hu.id, short_id: hu.short_id, status: hu.status, created_by: hu.created_by },
|
|
90
|
+
`✓ HU created: ${hu.short_id || hu.id} (pending, by ${hu.created_by}) — it shows up in \`kj board\``);
|
|
59
91
|
}
|
|
60
92
|
|
|
61
93
|
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}`);
|
package/src/commands/plan/fix.js
CHANGED
|
@@ -60,7 +60,7 @@ export async function planFixCommand({ config, planId, prompt, logger, json }) {
|
|
|
60
60
|
});
|
|
61
61
|
if (!fix.ok) { fixProgress.finish("failed"); break; }
|
|
62
62
|
const ops = applyFixerPatch(plan, fix.patch);
|
|
63
|
-
if (ops.added + ops.depsAdded + ops.
|
|
63
|
+
if (ops.added + ops.depsAdded + ops.archived === 0) { fixProgress.finish("done"); break; }
|
|
64
64
|
const review2 = await reviewPlan({
|
|
65
65
|
agent: planner, task: enrichedTask, hus: plan.hus,
|
|
66
66
|
onOutput: fixProgress.onOutput, silenceTimeoutMs, timeoutMs,
|
|
@@ -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);
|
|
@@ -367,8 +370,8 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
367
370
|
break;
|
|
368
371
|
}
|
|
369
372
|
const ops = applyFixerPatch(plan, fix.patch);
|
|
370
|
-
runLog.logText(`[planner] self-fix iter ${i} applied: +${ops.added} HU, +${ops.depsAdded} dep,
|
|
371
|
-
if (ops.added + ops.depsAdded + ops.
|
|
373
|
+
runLog.logText(`[planner] self-fix iter ${i} applied: +${ops.added} HU, +${ops.depsAdded} dep, ${ops.archived} archived`);
|
|
374
|
+
if (ops.added + ops.depsAdded + ops.archived === 0) {
|
|
372
375
|
fixProgress.finish("done");
|
|
373
376
|
runLog.logText(`[planner] self-fix iter ${i} empty patch — stopping`);
|
|
374
377
|
break;
|
|
@@ -82,6 +82,18 @@ const B = {
|
|
|
82
82
|
],
|
|
83
83
|
"findings with severity, category and file:line — or an explicit 'no security surface touched'."),
|
|
84
84
|
},
|
|
85
|
+
board: {
|
|
86
|
+
purpose: "What the HU Board is and how to act on it without breaking it",
|
|
87
|
+
render: () => brief("Board",
|
|
88
|
+
"the HU Board is the permanent record of work: every card traces to who created it and what happened to it — a dashboard warning is fixed with the command it names, never by rebuilding the board.",
|
|
89
|
+
[
|
|
90
|
+
"Cards are NEVER deleted or recreated — not to fix warnings, not to tidy up. Discard = `kj hu move <id> skipped` (archived, history kept).",
|
|
91
|
+
"States move only via `kj hu move <id> <pending|running|done|failed|skipped>`.",
|
|
92
|
+
"A duplicate short_id means the card already exists: update that card, never add a twin.",
|
|
93
|
+
"A warning is information, not damage: run the command it names; if it looks like a kj bug, `kj report-issue` — never work around it by destroying state.",
|
|
94
|
+
],
|
|
95
|
+
"the board reflects reality: the same cards before and after your action, with truthful states and provenance intact."),
|
|
96
|
+
},
|
|
85
97
|
audit: {
|
|
86
98
|
purpose: "Final look before shipping — does the whole thing hold?",
|
|
87
99
|
render: () => brief("Audit",
|
|
@@ -29,7 +29,7 @@ const TARGET_FILES = {
|
|
|
29
29
|
// ENV-D1 (KJC-TSK-0642): the tracking invariant names the CHOSEN state
|
|
30
30
|
// backend — a playbook that says "board or PG" makes the host guess.
|
|
31
31
|
const BACKEND_TRACKING = {
|
|
32
|
-
"hu-board": "Every piece of work has a tracked story/bug in the HU Board (`kj hu add` / `kj board`) before it starts.",
|
|
32
|
+
"hu-board": "Every piece of work has a tracked story/bug in the HU Board (`kj hu add` / `kj board`) before it starts. Cards are permanent — never delete or recreate one; discard with `kj hu move <id> skipped`. `kj brief board` explains the board.",
|
|
33
33
|
"planning-game": "Every piece of work has a tracked card in the Planning Game MCP before it starts (In Progress while you work it).",
|
|
34
34
|
};
|
|
35
35
|
|
|
@@ -58,7 +58,7 @@ Invariants (the git gates enforce these — they are not suggestions):
|
|
|
58
58
|
through an atomic PR (~150 net lines, Conventional Commits).
|
|
59
59
|
|
|
60
60
|
Commands: \`kj rag query\` · \`kj brief <role>\` (triage, planner, researcher,
|
|
61
|
-
architect, tester, security, audit) · \`kj hu add|move|list\` · \`kj adr add|list\` ·
|
|
61
|
+
architect, tester, security, audit, board) · \`kj hu add|move|list\` · \`kj adr add|list\` ·
|
|
62
62
|
\`kj review --staged\` · \`kj review --check\` · \`kj solomon --position\` ·
|
|
63
63
|
\`kj agent run <agent>\` · \`kj report\` · \`kj check\`
|
|
64
64
|
|
|
@@ -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
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { extractFirstJson } from "../utils/json-extract.js";
|
|
13
|
-
import { addHu
|
|
13
|
+
import { addHu } from "./plan-hu-ops.js";
|
|
14
14
|
import { normaliseAcceptanceTests } from "./plan-schema.js";
|
|
15
15
|
import { withBrainRecovery } from "../brain/with-brain-recovery.js";
|
|
16
16
|
|
|
@@ -111,9 +111,31 @@ export async function applyReviewerFeedback({ agent, task, hus, findings, onOutp
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
export function applyFixerPatch(plan, patch) {
|
|
114
|
-
const counts = { added: 0, depsAdded: 0,
|
|
114
|
+
const counts = { added: 0, depsAdded: 0, archived: 0 };
|
|
115
|
+
// KJC-TSK-0669 (absolute rule): HUs are NEVER deleted — not even by the
|
|
116
|
+
// fix loop. Overlap resolutions ARCHIVE in place (status skipped +
|
|
117
|
+
// provenance), so history survives and ids stay referenceable. The
|
|
118
|
+
// references are cleaned exactly like removeHu used to, because a
|
|
119
|
+
// skipped dependency never satisfies the scheduler (deadlock otherwise).
|
|
115
120
|
for (const huId of patch.deletions || []) {
|
|
116
|
-
|
|
121
|
+
const hu = plan.hus.find(h => h.id === huId);
|
|
122
|
+
if (!hu) continue;
|
|
123
|
+
if (hu.status !== "skipped") {
|
|
124
|
+
hu.status = "skipped";
|
|
125
|
+
hu.archived_by = "plan-reviewer";
|
|
126
|
+
hu.archive_reason = "scope overlap resolved by the plan-review fix loop";
|
|
127
|
+
hu.updatedAt = new Date().toISOString();
|
|
128
|
+
counts.archived += 1;
|
|
129
|
+
}
|
|
130
|
+
for (const other of plan.hus) {
|
|
131
|
+
if (other.id === huId) continue;
|
|
132
|
+
if (Array.isArray(other.blocked_by) && other.blocked_by.includes(huId)) {
|
|
133
|
+
other.blocked_by = other.blocked_by.filter(d => d !== huId);
|
|
134
|
+
}
|
|
135
|
+
if (Array.isArray(other.reuse) && other.reuse.includes(huId)) {
|
|
136
|
+
other.reuse = other.reuse.filter(d => d !== huId);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
117
139
|
}
|
|
118
140
|
// KJC-BUG-0053: las HUs añadidas por el fixer dejaban `short_id` y
|
|
119
141
|
// `blocked_by` vacíos porque addHu no recibía esos campos. Resultado:
|
|
@@ -140,6 +162,7 @@ export function applyFixerPatch(plan, patch) {
|
|
|
140
162
|
acceptance_tests: a.acceptance_tests,
|
|
141
163
|
reuse: a.reuse || [],
|
|
142
164
|
});
|
|
165
|
+
hu.created_by = "plan-reviewer"; // KJC-TSK-0661: provenance stamp
|
|
143
166
|
if (symbolicId) symbolicToHuId.set(symbolicId, hu.id);
|
|
144
167
|
const depsSymbolic = Array.isArray(a.dependencies) ? a.dependencies : [];
|
|
145
168
|
addedHus.push({ hu, depsSymbolic });
|
|
@@ -162,7 +185,7 @@ export function applyFixerPatch(plan, patch) {
|
|
|
162
185
|
hu.blocked_by = [...blocked, on];
|
|
163
186
|
counts.depsAdded += 1;
|
|
164
187
|
}
|
|
165
|
-
if (counts.added + counts.depsAdded + counts.
|
|
188
|
+
if (counts.added + counts.depsAdded + counts.archived > 0) {
|
|
166
189
|
plan.updatedAt = new Date().toISOString();
|
|
167
190
|
}
|
|
168
191
|
return counts;
|
|
@@ -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;
|