karajan-code 4.1.1 → 4.1.3
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.sh +119 -72
- package/src/cli/advanced-commands.js +1 -1
- package/src/cli/register-meta.js +55 -1
- package/src/commands/env.js +24 -6
- package/src/commands/hu.js +92 -0
- package/src/commands/install-tools.js +37 -2
- package/src/environment/adr.js +45 -0
- package/src/environment/playbook.js +4 -3
- package/src/harden/advisory.js +32 -16
- package/src/harden/config-engine.js +10 -0
- package/src/review/gate-gitignore.js +0 -0
- package/src/utils/pending-user-action.js +92 -0
package/package.json
CHANGED
|
@@ -1,31 +1,36 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
# Karajan Code —
|
|
2
|
+
# Karajan Code installer — guarantees a COMPLETE install (KJC-TSK-0658).
|
|
3
3
|
#
|
|
4
4
|
# curl -fsSL https://karajancode.com/install.sh | sh
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# Default route: npm — the full product (CLI + RAG + HU Board + MCP).
|
|
7
|
+
# 1. Node >= 22.12 present → npm install -g karajan-code
|
|
8
|
+
# 2. No usable Node → auto-provision official Node LTS into
|
|
9
|
+
# ~/.karajan/node (checksum-verified, nothing system-wide touched),
|
|
10
|
+
# install karajan-code with it, symlink kj into ~/.local/bin.
|
|
11
|
+
# Standalone route (CLI only, no native-module features — RAG/board/MCP
|
|
12
|
+
# unavailable): opt-in with `--standalone` or KJ_STANDALONE=1.
|
|
9
13
|
#
|
|
10
|
-
# POSIX sh only
|
|
14
|
+
# POSIX sh only. Env overrides: KJ_VERSION, KJ_INSTALL_DIR.
|
|
11
15
|
set -eu
|
|
12
16
|
|
|
13
17
|
REPO="manufosela/karajan-code"
|
|
14
18
|
VERSION="${KJ_VERSION:-latest}"
|
|
15
19
|
INSTALL_DIR="${KJ_INSTALL_DIR:-$HOME/.local/bin}"
|
|
20
|
+
NODE_MAJOR="22"
|
|
21
|
+
NODE_MIN_MINOR="12"
|
|
22
|
+
MODE="full"
|
|
23
|
+
[ "${1:-}" = "--standalone" ] && MODE="standalone"
|
|
24
|
+
[ "${KJ_STANDALONE:-0}" = "1" ] && MODE="standalone"
|
|
16
25
|
|
|
17
|
-
die() {
|
|
18
|
-
echo "kj-install: $1" >&2
|
|
19
|
-
exit 1
|
|
20
|
-
}
|
|
26
|
+
die() { echo "kj-install: $1" >&2; exit 1; }
|
|
21
27
|
|
|
22
|
-
# --- Detect OS/arch
|
|
23
|
-
os="$(uname -s)"
|
|
24
|
-
arch="$(uname -m)"
|
|
28
|
+
# --- Detect OS/arch (shared by both routes). ---
|
|
29
|
+
os="$(uname -s)"; arch="$(uname -m)"
|
|
25
30
|
case "$os" in
|
|
26
31
|
Linux) os="linux" ;;
|
|
27
32
|
Darwin) os="darwin" ;;
|
|
28
|
-
*) die "unsupported OS '$os'. Supported: Linux, macOS.
|
|
33
|
+
*) die "unsupported OS '$os'. Supported: Linux, macOS." ;;
|
|
29
34
|
esac
|
|
30
35
|
case "$arch" in
|
|
31
36
|
x86_64 | amd64) arch="x64" ;;
|
|
@@ -33,80 +38,122 @@ case "$arch" in
|
|
|
33
38
|
*) die "unsupported architecture '$arch'" ;;
|
|
34
39
|
esac
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
# --- Downloader + checksum tool. ---
|
|
42
|
+
if command -v curl >/dev/null 2>&1; then fetch() { curl -fsSL "$1" -o "$2"; }
|
|
43
|
+
elif command -v wget >/dev/null 2>&1; then fetch() { wget -qO "$2" "$1"; }
|
|
44
|
+
else die "need curl or wget"; fi
|
|
45
|
+
if command -v sha256sum >/dev/null 2>&1; then sha256() { sha256sum "$1" | cut -d' ' -f1; }
|
|
46
|
+
elif command -v shasum >/dev/null 2>&1; then sha256() { shasum -a 256 "$1" | cut -d' ' -f1; }
|
|
47
|
+
else die "need sha256sum or shasum to verify downloads"; fi
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if [ "$VERSION" = "latest" ]; then
|
|
46
|
-
base="https://github.com/${REPO}/releases/latest/download"
|
|
47
|
-
else
|
|
48
|
-
base="https://github.com/${REPO}/releases/download/${VERSION}"
|
|
49
|
-
fi
|
|
49
|
+
tmp="$(mktemp -d "${TMPDIR:-/tmp}/kj-install.XXXXXX")"
|
|
50
|
+
trap 'rm -rf "$tmp"' EXIT INT TERM
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
path_hint() {
|
|
53
|
+
case ":${PATH}:" in
|
|
54
|
+
*":$1:"*) ;;
|
|
55
|
+
*) echo "kj-install: add '$1' to your PATH: export PATH=\"$1:\$PATH\" (persist it in ~/.bashrc / ~/.zshrc)" ;;
|
|
56
|
+
esac
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# --- Is there a usable Node (>= NODE_MAJOR.NODE_MIN_MINOR)? ---
|
|
60
|
+
node_ok() {
|
|
61
|
+
command -v node >/dev/null 2>&1 || return 1
|
|
62
|
+
v="$(node --version 2>/dev/null | sed 's/^v//')"
|
|
63
|
+
major="${v%%.*}"; rest="${v#*.}"; minor="${rest%%.*}"
|
|
64
|
+
[ "$major" -gt "$NODE_MAJOR" ] 2>/dev/null && return 0
|
|
65
|
+
[ "$major" -eq "$NODE_MAJOR" ] 2>/dev/null && [ "$minor" -ge "$NODE_MIN_MINOR" ] 2>/dev/null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
npm_pkg() { if [ "$VERSION" = "latest" ]; then echo "karajan-code"; else echo "karajan-code@${VERSION#v}"; fi; }
|
|
69
|
+
|
|
70
|
+
if [ "$MODE" = "full" ]; then
|
|
71
|
+
if node_ok; then
|
|
72
|
+
echo "kj-install: Node $(node --version) found — installing via npm (full product)..."
|
|
73
|
+
npm install -g "$(npm_pkg)" || die "npm install failed. If it was a permissions error, set a user prefix (npm config set prefix ~/.local) and re-run."
|
|
74
|
+
echo "kj-install: installed $(kj --version 2>/dev/null || echo karajan-code). Run 'kj doctor' next."
|
|
75
|
+
exit 0
|
|
76
|
+
fi
|
|
59
77
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
78
|
+
echo "kj-install: no usable Node (need >= ${NODE_MAJOR}.${NODE_MIN_MINOR}) — provisioning official Node LTS into ~/.karajan/node (nothing system-wide)..."
|
|
79
|
+
dist="https://nodejs.org/dist/latest-v${NODE_MAJOR}.x"
|
|
80
|
+
fetch "${dist}/SHASUMS256.txt" "${tmp}/SHASUMS256.txt" || die "could not fetch the Node checksum list"
|
|
81
|
+
node_asset="$(grep -o "node-v[0-9.]*-${os}-${arch}\.tar\.gz" "${tmp}/SHASUMS256.txt" | head -1)"
|
|
82
|
+
[ -n "$node_asset" ] || die "no official Node build for ${os}-${arch}"
|
|
83
|
+
echo "kj-install: downloading ${node_asset}..."
|
|
84
|
+
fetch "${dist}/${node_asset}" "${tmp}/${node_asset}" || die "could not download Node"
|
|
85
|
+
expected="$(grep "${node_asset}\$" "${tmp}/SHASUMS256.txt" | head -1 | cut -d' ' -f1)"
|
|
86
|
+
actual="$(sha256 "${tmp}/${node_asset}")"
|
|
87
|
+
[ "$expected" = "$actual" ] || die "Node checksum mismatch — aborting, nothing installed"
|
|
88
|
+
|
|
89
|
+
# Stage the whole install (extract + npm install) in a sibling dir and only
|
|
90
|
+
# swap it into place once EVERYTHING succeeded — a failed download/extract/
|
|
91
|
+
# install must never destroy a previous working ~/.karajan/node.
|
|
92
|
+
node_home="$HOME/.karajan/node"
|
|
93
|
+
staging="${node_home}.staging.$$"
|
|
94
|
+
rm -rf "$staging"; mkdir -p "$staging"
|
|
95
|
+
trap 'rm -rf "$tmp" "$staging"' EXIT INT TERM
|
|
96
|
+
tar -xzf "${tmp}/${node_asset}" -C "$staging" --strip-components=1 || die "could not extract Node"
|
|
97
|
+
|
|
98
|
+
echo "kj-install: installing karajan-code with the provisioned Node..."
|
|
99
|
+
PATH="${staging}/bin:$PATH" "${staging}/bin/npm" install -g "$(npm_pkg)" || die "npm install failed with the provisioned Node"
|
|
100
|
+
|
|
101
|
+
# Swap keeping the previous install recoverable at EVERY instant: park it
|
|
102
|
+
# as a backup, arm a trap that restores it on any exit (signal included),
|
|
103
|
+
# move the staged one in, and only then disarm the trap and drop the
|
|
104
|
+
# backup — the user can never end up without a working install.
|
|
105
|
+
backup="${node_home}.old.$$"
|
|
106
|
+
if [ -e "$node_home" ]; then
|
|
107
|
+
mv "$node_home" "$backup" || die "could not park the previous install"
|
|
108
|
+
trap '[ -e "$node_home" ] || mv "$backup" "$node_home" 2>/dev/null; rm -rf "$tmp" "$staging"' EXIT INT TERM
|
|
109
|
+
fi
|
|
110
|
+
mv "$staging" "$node_home" || die "could not move the staged install into place (previous install restored)"
|
|
111
|
+
trap 'rm -rf "$tmp"' EXIT INT TERM
|
|
112
|
+
rm -rf "$backup"
|
|
113
|
+
mkdir -p "$INSTALL_DIR"
|
|
114
|
+
# Wrappers, not symlinks: the shebang (#!/usr/bin/env node) must find the
|
|
115
|
+
# provisioned Node even though it is not on the user's PATH.
|
|
116
|
+
for bin in kj karajan-mcp; do
|
|
117
|
+
[ -e "${node_home}/bin/${bin}" ] || continue
|
|
118
|
+
{
|
|
119
|
+
echo '#!/bin/sh'
|
|
120
|
+
echo "export PATH=\"${node_home}/bin:\$PATH\""
|
|
121
|
+
echo "exec \"${node_home}/bin/${bin}\" \"\$@\""
|
|
122
|
+
} >"${INSTALL_DIR}/${bin}"
|
|
123
|
+
chmod +x "${INSTALL_DIR}/${bin}"
|
|
124
|
+
done
|
|
125
|
+
installed="$("${INSTALL_DIR}/kj" --version 2>/dev/null || echo '?')"
|
|
126
|
+
echo "kj-install: installed kj ${installed} (full product) — kj at ${INSTALL_DIR}/kj"
|
|
127
|
+
path_hint "$INSTALL_DIR"
|
|
128
|
+
echo "kj-install: next — run 'kj doctor', then 'kj install-tools' to complete the whole stack."
|
|
129
|
+
exit 0
|
|
67
130
|
fi
|
|
68
131
|
|
|
69
|
-
# ---
|
|
70
|
-
|
|
71
|
-
|
|
132
|
+
# --- Standalone route: prebuilt single binary (CLI only). ---
|
|
133
|
+
echo "kj-install: standalone mode — single binary, NO native-module features (RAG, HU Board and MCP need the npm install)."
|
|
134
|
+
target="${os}-${arch}"
|
|
135
|
+
case "$target" in
|
|
136
|
+
linux-x64 | darwin-arm64) ;;
|
|
137
|
+
*) die "no prebuilt binary for '$target' (available: linux-x64, darwin-arm64) — run without --standalone for the npm route" ;;
|
|
138
|
+
esac
|
|
139
|
+
asset="kj-${target}"
|
|
140
|
+
if [ "$VERSION" = "latest" ]; then base="https://github.com/${REPO}/releases/latest/download"
|
|
141
|
+
else base="https://github.com/${REPO}/releases/download/${VERSION}"; fi
|
|
72
142
|
|
|
73
143
|
echo "kj-install: downloading ${asset} (${VERSION})..."
|
|
74
|
-
fetch "${base}/${asset}" "${tmp}/kj" || die "could not download ${base}/${asset}
|
|
144
|
+
fetch "${base}/${asset}" "${tmp}/kj" || die "could not download ${base}/${asset}"
|
|
75
145
|
fetch "${base}/${asset}.sha256" "${tmp}/kj.sha256" || die "could not download the checksum for ${asset}"
|
|
76
|
-
|
|
77
146
|
expected="$(cut -d' ' -f1 <"${tmp}/kj.sha256")"
|
|
78
147
|
actual="$(sha256 "${tmp}/kj")"
|
|
79
|
-
[ "$expected" = "$actual" ] || die "checksum mismatch
|
|
148
|
+
[ "$expected" = "$actual" ] || die "checksum mismatch — aborting, nothing installed"
|
|
80
149
|
|
|
81
|
-
# --- Install atomically: chmod on the temp file, then move into place. ---
|
|
82
150
|
chmod +x "${tmp}/kj"
|
|
83
151
|
mkdir -p "$INSTALL_DIR"
|
|
84
152
|
mv -f "${tmp}/kj" "${INSTALL_DIR}/kj"
|
|
85
|
-
|
|
86
|
-
# On macOS the binary is ad-hoc signed, not notarized with a paid Apple
|
|
87
|
-
# certificate, so Gatekeeper can quarantine it. Clear the flag on the copy
|
|
88
|
-
# we just checksummed ourselves (no-op on Linux / if the flag is absent).
|
|
89
153
|
if [ "$os" = "darwin" ] && command -v xattr >/dev/null 2>&1; then
|
|
90
154
|
xattr -d com.apple.quarantine "${INSTALL_DIR}/kj" 2>/dev/null || true
|
|
91
155
|
fi
|
|
92
|
-
|
|
93
156
|
installed="$("${INSTALL_DIR}/kj" --version 2>/dev/null || echo '?')"
|
|
94
|
-
echo "kj-install: installed kj ${installed} to ${INSTALL_DIR}/kj"
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
case ":${PATH}:" in
|
|
98
|
-
*":${INSTALL_DIR}:"*) echo "kj-install: '${INSTALL_DIR}' is on your PATH — run 'kj --help' to get started." ;;
|
|
99
|
-
*)
|
|
100
|
-
echo "kj-install: '${INSTALL_DIR}' is not on your PATH. Add it with:"
|
|
101
|
-
echo " export PATH=\"${INSTALL_DIR}:\$PATH\""
|
|
102
|
-
echo " (add that line to ~/.bashrc, ~/.zshrc or ~/.profile to make it permanent)"
|
|
103
|
-
;;
|
|
104
|
-
esac
|
|
105
|
-
|
|
106
|
-
# --- Prerequisites: the binary bundles no toolchain. kj orchestrates ---
|
|
107
|
-
# --- external tools, so name the hard requirements and let `kj doctor` ---
|
|
108
|
-
# --- check them precisely for this machine.
|
|
109
|
-
echo ""
|
|
110
|
-
echo "kj-install: next step — run 'kj doctor' to check prerequisites."
|
|
111
|
-
echo " Required: git, plus at least one agent CLI (Claude Code, Codex or Gemini)."
|
|
112
|
-
echo " Optional: Docker (local models, SonarQube) and Node/npm (helper tools: Squeezr, qmd)."
|
|
157
|
+
echo "kj-install: installed standalone kj ${installed} to ${INSTALL_DIR}/kj"
|
|
158
|
+
path_hint "$INSTALL_DIR"
|
|
159
|
+
echo "kj-install: next — run 'kj doctor'. For the full product later: re-run this installer without --standalone."
|
|
@@ -31,7 +31,7 @@ export const ADVANCED_GROUPS = [
|
|
|
31
31
|
{ title: "Análisis pre-run", commands: ["discover", "triage", "researcher", "architect", "onboard", "brief"] },
|
|
32
32
|
{ title: "Búsqueda / RAG", commands: ["rag", "qmd", "watch"] },
|
|
33
33
|
{ title: "Calidad / auditoría", commands: ["audit", "check", "mutate", "webperf", "sonar"] },
|
|
34
|
-
{ title: "Sesión / board", commands: ["resume", "report", "board", "undo", "standby"] },
|
|
34
|
+
{ title: "Sesión / board", commands: ["resume", "report", "board", "hu", "adr", "undo", "standby"] },
|
|
35
35
|
{ title: "Infra / setup", commands: ["install-tools", "ollama", "skills", "roles", "agents", "env"] },
|
|
36
36
|
{ title: "Mantenimiento", commands: ["clean", "sync", "telemetry", "report-issue"] },
|
|
37
37
|
];
|
package/src/cli/register-meta.js
CHANGED
|
@@ -22,6 +22,8 @@ import { telemetryPreviewCommand, telemetryStatusCommand } from "../commands/tel
|
|
|
22
22
|
import { envInstallCommand, briefCommand } from "../commands/env.js";
|
|
23
23
|
import { agentRunCommand } from "../commands/agent-run.js";
|
|
24
24
|
import { reportIssueCommand } from "../commands/report-issue.js";
|
|
25
|
+
import { huCommand } from "../commands/hu.js";
|
|
26
|
+
import { addAdr, listAdrs } from "../environment/adr.js";
|
|
25
27
|
import { formatAdvancedIndex } from "../commands/advanced.js";
|
|
26
28
|
import { withConfig } from "./_shared.js";
|
|
27
29
|
|
|
@@ -133,7 +135,10 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
133
135
|
.option("--no-rag", "Skip building the RAG index when the project has none (ENV-E1)")
|
|
134
136
|
.action(async (flags) => {
|
|
135
137
|
await withConfig(pkgVersion, "env-install", flags, async ({ config, logger }) => {
|
|
136
|
-
await envInstallCommand({ config, logger, flags });
|
|
138
|
+
const r = await envInstallCommand({ config, logger, flags });
|
|
139
|
+
// KJC-TSK-0659: exit 3 = pending user action (RAG cannot index) —
|
|
140
|
+
// a driving agent must stop and wait, never continue degraded.
|
|
141
|
+
if (Number.isInteger(r?.exitCode) && r.exitCode !== 0) process.exit(r.exitCode);
|
|
137
142
|
});
|
|
138
143
|
});
|
|
139
144
|
|
|
@@ -149,6 +154,55 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
149
154
|
});
|
|
150
155
|
});
|
|
151
156
|
|
|
157
|
+
// AB-H (KJC-TSK-0658): board writes + repo ADRs for the brain.
|
|
158
|
+
const hu = program.command("hu").description("Track work in the HU Board from any host agent (card first)");
|
|
159
|
+
hu.command("add <title>")
|
|
160
|
+
.option("--id <shortId>", "Human-readable short id")
|
|
161
|
+
.option("--criteria <text>", "Acceptance criteria")
|
|
162
|
+
.option("--json", "Machine-readable output")
|
|
163
|
+
.action(async (title, flags) => {
|
|
164
|
+
await withConfig(pkgVersion, "hu", flags, async ({ config }) => {
|
|
165
|
+
await huCommand({ config, action: "add", args: [title], flags });
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
hu.command("move <id> <status>")
|
|
169
|
+
.option("--json", "Machine-readable output")
|
|
170
|
+
.action(async (id, status, flags) => {
|
|
171
|
+
await withConfig(pkgVersion, "hu", flags, async ({ config }) => {
|
|
172
|
+
await huCommand({ config, action: "move", args: [id, status], flags });
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
hu.command("list")
|
|
176
|
+
.option("--json", "Machine-readable output")
|
|
177
|
+
.action(async (flags) => {
|
|
178
|
+
await withConfig(pkgVersion, "hu", flags, async ({ config }) => {
|
|
179
|
+
await huCommand({ config, action: "list", flags });
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const adr = program.command("adr").description("Architecture decision records in .karajan/adrs/ (git-tracked)");
|
|
184
|
+
adr.command("add <title>")
|
|
185
|
+
.requiredOption("--decision <text>", "The decision itself")
|
|
186
|
+
.option("--context <text>", "Why this came up")
|
|
187
|
+
.option("--consequences <text>", "Trade-offs accepted")
|
|
188
|
+
.option("--json", "Machine-readable output")
|
|
189
|
+
.action(async (title, flags) => {
|
|
190
|
+
await withConfig(pkgVersion, "adr", flags, async ({ config }) => {
|
|
191
|
+
const res = await addAdr(config?.projectDir || process.cwd(), { title, ...flags });
|
|
192
|
+
console.log(flags.json ? JSON.stringify(res) : `✓ ADR ${res.number} created: ${res.file} — commit it`);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
adr.command("list")
|
|
196
|
+
.option("--json", "Machine-readable output")
|
|
197
|
+
.action(async (flags) => {
|
|
198
|
+
await withConfig(pkgVersion, "adr", flags, async ({ config }) => {
|
|
199
|
+
const adrs = await listAdrs(config?.projectDir || process.cwd());
|
|
200
|
+
if (flags.json) { console.log(JSON.stringify(adrs)); return; }
|
|
201
|
+
for (const a of adrs) console.log(`${String(a.number).padStart(4, "0")} ${a.status.padEnd(10)} ${a.title}`);
|
|
202
|
+
if (adrs.length === 0) console.log("no ADRs yet — create one with: kj adr add \"<title>\" --decision \"...\"");
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
152
206
|
// AB-F (KJC-TSK-0655): self-healing — the brain files kj frictions upstream.
|
|
153
207
|
program
|
|
154
208
|
.command("report-issue")
|
package/src/commands/env.js
CHANGED
|
@@ -8,6 +8,7 @@ import { installPlaybook } from "../environment/playbook.js";
|
|
|
8
8
|
import { renderBrief, listBriefs } from "../environment/briefs.js";
|
|
9
9
|
import { openVecStore, projectSlug, getLastIndexedCommit } from "../rag/vec-store.js";
|
|
10
10
|
import { ragIndexCommand } from "./rag.js";
|
|
11
|
+
import { renderPendingBlock, PENDING_EXIT_CODE } from "../utils/pending-user-action.js";
|
|
11
12
|
|
|
12
13
|
function hasRagIndex(config, projectDir) {
|
|
13
14
|
const db = openVecStore({ dim: config?.rag?.embedder?.dim || 768 });
|
|
@@ -42,21 +43,38 @@ export async function envInstallCommand({ config = null, logger = null, flags =
|
|
|
42
43
|
console.log(`✓ Karajan playbook installed in: ${result.files.join(", ")}`);
|
|
43
44
|
|
|
44
45
|
// ENV-E1: RAG-first — the playbook orders "query the RAG before coding",
|
|
45
|
-
// so installing the environment guarantees the index exists.
|
|
46
|
-
//
|
|
46
|
+
// so installing the environment guarantees the index exists. KJC-TSK-0659
|
|
47
|
+
// stop-on-sudo: an index that cannot be built is a BLOCKING condition —
|
|
48
|
+
// "success" with 0 chunks would leave every future session running the
|
|
49
|
+
// method against an empty RAG (field-reproduced: 0/727 without Ollama).
|
|
50
|
+
// The playbook stays installed; the command exits 3 so the driving agent
|
|
51
|
+
// stops, shows the block, and waits for the user.
|
|
47
52
|
if (flags.rag !== false) {
|
|
53
|
+
const provider = config?.rag?.embedder?.provider || "ollama";
|
|
54
|
+
const blockRag = (why) => {
|
|
55
|
+
result.ragError = why;
|
|
56
|
+
result.exitCode = PENDING_EXIT_CODE;
|
|
57
|
+
const item = provider === "ollama"
|
|
58
|
+
? { tool: "ollama", action: "needs-user", reason: `the RAG cannot index: ${why}` }
|
|
59
|
+
: { tool: `${provider} embedder`, action: "needs-user", reason: `the RAG cannot index: ${why} — check rag.embedder in kj config`, manualUrl: "kj config" };
|
|
60
|
+
console.log(renderPendingBlock([item], { retry: "kj rag index --with-sources (then re-run: kj env install)" }));
|
|
61
|
+
};
|
|
48
62
|
try {
|
|
49
63
|
if (hasRagIndex(config, projectDir)) {
|
|
50
64
|
console.log("✓ RAG index present");
|
|
51
65
|
} else {
|
|
52
66
|
console.log("⏳ no RAG index for this project — building it (first time only)…");
|
|
53
|
-
await ragIndexCommand({ config, logger, flags: { withSources: true } });
|
|
67
|
+
const totals = await ragIndexCommand({ config, logger, flags: { withSources: true } });
|
|
68
|
+
if ((totals?.indexed ?? 0) === 0 && (totals?.files ?? 0) > 0) {
|
|
69
|
+
blockRag(`0 of ${totals.files} files indexed — is the embedder running?`);
|
|
70
|
+
}
|
|
54
71
|
}
|
|
55
72
|
} catch (err) {
|
|
56
|
-
|
|
57
|
-
console.log(`⚠ RAG index could not be built (${err.message}) — run \`kj rag index --with-sources\` later`);
|
|
73
|
+
blockRag(err.message);
|
|
58
74
|
}
|
|
59
75
|
}
|
|
60
|
-
|
|
76
|
+
if (result.exitCode !== PENDING_EXIT_CODE) {
|
|
77
|
+
console.log(" The host agent now follows the method: RAG first, TDD, cross-AI review before commit.");
|
|
78
|
+
}
|
|
61
79
|
return result;
|
|
62
80
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `kj hu add|move|list` (AB-H, KJC-TSK-0658) — board writes for the brain.
|
|
3
|
+
* The v4 playbook orders "card first", but until now only the headless
|
|
4
|
+
* planner could create HUs. These commands operate on a per-project
|
|
5
|
+
* "brain-backlog" plan (created on first use) so any host agent can track
|
|
6
|
+
* work in the HU Board without the subprocess pipeline.
|
|
7
|
+
*/
|
|
8
|
+
import { addHu, updateHuStatus } from "../plan/plan-hu-ops.js";
|
|
9
|
+
import { generatePlanId } from "../plan/plan-id.js";
|
|
10
|
+
import { savePlan, listPlans, loadPlan } from "../plan/plan-store.js";
|
|
11
|
+
|
|
12
|
+
export const HU_STATUSES = ["pending", "running", "done", "failed", "skipped"];
|
|
13
|
+
const BACKLOG_NAME = "brain-backlog";
|
|
14
|
+
|
|
15
|
+
async function backlogPlan(projectDir) {
|
|
16
|
+
const plans = await listPlans(projectDir);
|
|
17
|
+
const existing = plans.find((p) => p.alias === BACKLOG_NAME || p.name === BACKLOG_NAME);
|
|
18
|
+
if (existing) return loadPlan(projectDir, existing.planId);
|
|
19
|
+
const plan = {
|
|
20
|
+
version: 2, planId: generatePlanId(), name: BACKLOG_NAME,
|
|
21
|
+
task: "Host-agent tracked work (v4 environment)",
|
|
22
|
+
status: "ready", hus: [], createdAt: new Date().toISOString(),
|
|
23
|
+
};
|
|
24
|
+
const planId = await savePlan(projectDir, plan);
|
|
25
|
+
return loadPlan(projectDir, planId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function huCommand({ config = null, action, args = [], flags = {} }) {
|
|
29
|
+
const projectDir = config?.projectDir || process.cwd();
|
|
30
|
+
const emit = (obj, human) => { console.log(flags.json ? JSON.stringify(obj) : human); return obj; };
|
|
31
|
+
|
|
32
|
+
if (action === "list") {
|
|
33
|
+
const plans = await listPlans(projectDir);
|
|
34
|
+
const rows = [];
|
|
35
|
+
for (const meta of plans) {
|
|
36
|
+
const plan = await loadPlan(projectDir, meta.planId);
|
|
37
|
+
for (const h of plan.hus || []) {
|
|
38
|
+
rows.push({ id: h.id, short_id: h.short_id, title: h.title, status: h.status, plan: plan.alias || plan.planId });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (flags.json) { console.log(JSON.stringify(rows)); return rows; }
|
|
42
|
+
for (const r of rows) console.log(`${(r.short_id || r.id).padEnd(28)} ${r.status.padEnd(8)} ${r.title}`);
|
|
43
|
+
if (rows.length === 0) console.log("no HUs yet — create one with: kj hu add \"<story>\"");
|
|
44
|
+
return rows;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (action === "add") {
|
|
48
|
+
const title = args[0];
|
|
49
|
+
if (!title || !title.trim()) throw new Error("kj hu add requires a title: kj hu add \"<story>\"");
|
|
50
|
+
const plan = await backlogPlan(projectDir);
|
|
51
|
+
const hu = addHu(plan, {
|
|
52
|
+
title,
|
|
53
|
+
short_id: flags.id || null,
|
|
54
|
+
acceptance_criteria: flags.criteria ? [flags.criteria] : [],
|
|
55
|
+
});
|
|
56
|
+
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\``);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (action === "move") {
|
|
62
|
+
const [ref, status] = args;
|
|
63
|
+
if (!ref || !status) throw new Error("usage: kj hu move <id> <status>");
|
|
64
|
+
if (!HU_STATUSES.includes(status)) {
|
|
65
|
+
throw new Error(`invalid status "${status}" — valid: ${HU_STATUSES.join(", ")}`);
|
|
66
|
+
}
|
|
67
|
+
// Exact canonical id wins outright (it IS the disambiguator); only then
|
|
68
|
+
// fall back to short_id matches — which can repeat between plans, so
|
|
69
|
+
// silently moving the first hit could move the wrong card.
|
|
70
|
+
const byId = [];
|
|
71
|
+
const byShort = [];
|
|
72
|
+
for (const meta of await listPlans(projectDir)) {
|
|
73
|
+
const plan = await loadPlan(projectDir, meta.planId);
|
|
74
|
+
for (const hu of plan.hus || []) {
|
|
75
|
+
if (hu.id === ref) byId.push({ plan, hu });
|
|
76
|
+
else if (hu.short_id === ref) byShort.push({ plan, hu });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const matches = byId.length > 0 ? byId : byShort;
|
|
80
|
+
if (matches.length === 0) throw new Error(`HU "${ref}" not found — see kj hu list`);
|
|
81
|
+
if (matches.length > 1) {
|
|
82
|
+
const ids = matches.map((m) => m.hu.id).join(", ");
|
|
83
|
+
throw new Error(`"${ref}" is ambiguous (${matches.length} matches: ${ids}) — use the full id`);
|
|
84
|
+
}
|
|
85
|
+
const { plan, hu } = matches[0];
|
|
86
|
+
updateHuStatus(plan, hu.id, status);
|
|
87
|
+
await savePlan(projectDir, plan);
|
|
88
|
+
return emit({ id: hu.id, status }, `✓ ${hu.short_id || hu.id} → ${status}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw new Error(`unknown action "${action}" — use: add | move | list`);
|
|
92
|
+
}
|
|
@@ -23,12 +23,23 @@ import { detectProjectStack } from "../utils/stack-detect.js";
|
|
|
23
23
|
import { resolveStandalone } from "../utils/binary-sources.js";
|
|
24
24
|
import { downloadBinary, binDir, runInstallCommand } from "../utils/tool-installer.js";
|
|
25
25
|
import { dockerInstallPlan } from "../utils/docker-install.js";
|
|
26
|
+
import { collectPending, renderPendingBlock, PENDING_EXIT_CODE } from "../utils/pending-user-action.js";
|
|
26
27
|
|
|
27
28
|
const execFileAsync = promisify(execFile);
|
|
28
29
|
|
|
29
30
|
// git and the agent CLI lead the list: both are `kj doctor` *required* tools,
|
|
30
31
|
// so a blank machine wants them before the optional audit tools.
|
|
31
|
-
|
|
32
|
+
// KJC-TSK-0657: rtk/squeezr/qmd (token+context optimizers, previously
|
|
33
|
+
// init-only) complete the list — one pass leaves the machine 100%
|
|
34
|
+
// operational, per the quality-default-on product rule.
|
|
35
|
+
const ALL_TOOLS = ["git", "agent-cli", "semgrep", "osv-scanner", "lighthouse", "docker", "sonar", "rtk", "squeezr", "qmd"];
|
|
36
|
+
|
|
37
|
+
// Optimizer tools reuse the init installers (same {ok, version, error} contract).
|
|
38
|
+
const OPTIMIZER_INSTALLERS = {
|
|
39
|
+
rtk: () => import("../utils/rtk-install.js").then((m) => m.installRtk),
|
|
40
|
+
squeezr: () => import("../utils/squeezr-install.js").then((m) => m.installSqueezr),
|
|
41
|
+
qmd: () => import("../utils/qmd-install.js").then((m) => m.installQmd),
|
|
42
|
+
};
|
|
32
43
|
|
|
33
44
|
// The default pipeline is coder=claude, reviewer=codex, so `agent-cli` installs
|
|
34
45
|
// exactly those two. gemini stays out of the default: it is a supported
|
|
@@ -335,6 +346,22 @@ export async function installToolsCommand(opts = {}) {
|
|
|
335
346
|
continue;
|
|
336
347
|
}
|
|
337
348
|
|
|
349
|
+
if (OPTIMIZER_INSTALLERS[tool]) {
|
|
350
|
+
if (dryRun) {
|
|
351
|
+
results.push({ tool, action: "planned", command: `kj install-tools --only ${tool}` });
|
|
352
|
+
logger.info?.(`▸ ${tool}: would install (init installer)`);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const proceed = yes || await promptYesNo(`Install ${tool}?`);
|
|
356
|
+
if (!proceed) { results.push({ tool, action: "declined" }); continue; }
|
|
357
|
+
const install = await OPTIMIZER_INSTALLERS[tool]();
|
|
358
|
+
const r = await install(logger);
|
|
359
|
+
results.push(r.ok
|
|
360
|
+
? { tool, action: "installed", version: r.version }
|
|
361
|
+
: { tool, action: "failed", error: r.error });
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
|
|
338
365
|
if (tool === "git") {
|
|
339
366
|
const result = await handleGit({ available, dryRun, yes, logger });
|
|
340
367
|
results.push(result);
|
|
@@ -390,8 +417,16 @@ export async function installToolsCommand(opts = {}) {
|
|
|
390
417
|
}
|
|
391
418
|
}
|
|
392
419
|
|
|
420
|
+
// KJC-TSK-0659 stop-on-sudo: anything that still needs the user's hands
|
|
421
|
+
// becomes ONE block with the exact per-OS commands, and a distinctive exit
|
|
422
|
+
// code so a driving agent stops and waits instead of continuing degraded.
|
|
423
|
+
const pending = dryRun ? [] : collectPending(results, { yes });
|
|
424
|
+
if (pending.length > 0) {
|
|
425
|
+
logger.warn?.(renderPendingBlock(pending));
|
|
426
|
+
return { results, pending, exitCode: PENDING_EXIT_CODE };
|
|
427
|
+
}
|
|
393
428
|
const failures = results.filter((r) => r.action === "failed").length;
|
|
394
|
-
return { results, exitCode: failures > 0 ? 1 : 0 };
|
|
429
|
+
return { results, pending, exitCode: failures > 0 ? 1 : 0 };
|
|
395
430
|
}
|
|
396
431
|
|
|
397
432
|
export const __test = { parseOnlyList, isInstalled, ALL_TOOLS };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADRs in the repo (AB-H, KJC-TSK-0658) — `kj adr add|list`. Architecture
|
|
3
|
+
* decisions live as numbered markdown under .karajan/adrs/, git-tracked so
|
|
4
|
+
* the whole team (and every brain) inherits them. The architect brief's
|
|
5
|
+
* "check the ADRs before deciding" now has a target on every backend.
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
const ADR_DIR = path.join(".karajan", "adrs");
|
|
11
|
+
|
|
12
|
+
const slugify = (t) => t.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-|-$/g, "").slice(0, 60);
|
|
13
|
+
|
|
14
|
+
export async function listAdrs(projectDir) {
|
|
15
|
+
const dir = path.join(projectDir, ADR_DIR);
|
|
16
|
+
let files;
|
|
17
|
+
try { files = await fs.readdir(dir); } catch { return []; }
|
|
18
|
+
const adrs = [];
|
|
19
|
+
for (const f of files.filter((f) => /^\d{4}-.*\.md$/.test(f)).sort()) {
|
|
20
|
+
const text = await fs.readFile(path.join(dir, f), "utf8");
|
|
21
|
+
const title = text.match(/^# (.+)$/m)?.[1] || f;
|
|
22
|
+
const status = text.match(/^Status: (.+)$/m)?.[1] || "accepted";
|
|
23
|
+
adrs.push({ file: path.join(ADR_DIR, f), number: Number(f.slice(0, 4)), title, status });
|
|
24
|
+
}
|
|
25
|
+
return adrs;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function addAdr(projectDir, { title, decision, context = "", consequences = "" }) {
|
|
29
|
+
if (!title?.trim() || !decision?.trim()) {
|
|
30
|
+
throw new Error("kj adr add requires a title and --decision");
|
|
31
|
+
}
|
|
32
|
+
const existing = await listAdrs(projectDir);
|
|
33
|
+
const number = (existing.at(-1)?.number || 0) + 1;
|
|
34
|
+
const file = path.join(ADR_DIR, `${String(number).padStart(4, "0")}-${slugify(title)}.md`);
|
|
35
|
+
const body = [
|
|
36
|
+
`# ${title}`, "",
|
|
37
|
+
`Status: accepted`, `Date: ${new Date().toISOString().slice(0, 10)}`, "",
|
|
38
|
+
...(context ? ["## Context", "", context, ""] : []),
|
|
39
|
+
"## Decision", "", decision, "",
|
|
40
|
+
...(consequences ? ["## Consequences", "", consequences, ""] : []),
|
|
41
|
+
].join("\n");
|
|
42
|
+
await fs.mkdir(path.join(projectDir, ADR_DIR), { recursive: true });
|
|
43
|
+
await fs.writeFile(path.join(projectDir, file), body);
|
|
44
|
+
return { number, file, title };
|
|
45
|
+
}
|
|
@@ -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 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.",
|
|
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,8 +58,9 @@ 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
|
|
62
|
-
\`kj
|
|
61
|
+
architect, tester, security, audit) · \`kj hu add|move|list\` · \`kj adr add|list\` ·
|
|
62
|
+
\`kj review --staged\` · \`kj review --check\` · \`kj solomon --position\` ·
|
|
63
|
+
\`kj agent run <agent>\` · \`kj report\` · \`kj check\`
|
|
63
64
|
|
|
64
65
|
Hit a kj bug or friction? Diagnose it and file it upstream with
|
|
65
66
|
\`kj report-issue\` (sanitized; ask your user before \`--publish\`).
|
package/src/harden/advisory.js
CHANGED
|
@@ -21,11 +21,38 @@ const BLOCK_VERSION = 1; // current version every markered config ships at
|
|
|
21
21
|
// Other filenames/formats for the same artifact, so kj never reports a false
|
|
22
22
|
// "missing". `pkgKey` is a package.json field that can hold inline config.
|
|
23
23
|
const EQUIVALENTS = {
|
|
24
|
-
commitlint: { files: [".commitlintrc", ".commitlintrc.js", ".commitlintrc.json", ".commitlintrc.yaml"], pkgKey: "commitlint" },
|
|
25
|
-
eslint: { files: ["eslint.config.mjs", ".eslintrc.js", ".eslintrc.cjs", ".eslintrc.json", ".eslintrc.yaml"], pkgKey: "eslintConfig" },
|
|
26
|
-
prettier: { files: [".prettierrc", ".prettierrc.js", ".prettierrc.yaml", "prettier.config.js"], pkgKey: "prettier" },
|
|
24
|
+
commitlint: { files: [".commitlintrc", ".commitlintrc.js", ".commitlintrc.cjs", ".commitlintrc.mjs", ".commitlintrc.json", ".commitlintrc.yaml", ".commitlintrc.yml", "commitlint.config.mjs", "commitlint.config.cjs", "commitlint.config.ts"], pkgKey: "commitlint" },
|
|
25
|
+
eslint: { files: ["eslint.config.mjs", "eslint.config.cjs", "eslint.config.ts", "eslint.config.mts", "eslint.config.cts", ".eslintrc.js", ".eslintrc.cjs", ".eslintrc.json", ".eslintrc.yaml", ".eslintrc.yml"], pkgKey: "eslintConfig" },
|
|
26
|
+
prettier: { files: [".prettierrc", ".prettierrc.js", ".prettierrc.yaml", ".prettierrc.yml", ".prettierrc.json5", ".prettierrc.toml", "prettier.config.js", "prettier.config.mjs", "prettier.config.cjs", "prettier.config.ts"], pkgKey: "prettier" },
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* KJC-BUG-0119 — same-tool filename variant for `artifactId` in any of
|
|
31
|
+
* `searchDirs` (file or inline package.json config); null when none exists.
|
|
32
|
+
* Seeding kj's default filename NEXT TO a variant eclipses the user's real
|
|
33
|
+
* config (eslint resolves eslint.config.js before .mjs), so config-engine
|
|
34
|
+
* must consult this before treating an artifact as absent.
|
|
35
|
+
*/
|
|
36
|
+
export function findEquivalentIn(searchDirs, artifactId) {
|
|
37
|
+
const spec = EQUIVALENTS[artifactId];
|
|
38
|
+
if (!spec) return null;
|
|
39
|
+
for (const dir of searchDirs) {
|
|
40
|
+
for (const rel of spec.files) {
|
|
41
|
+
if (existsSync(join(dir, rel))) return rel;
|
|
42
|
+
}
|
|
43
|
+
if (spec.pkgKey && existsSync(join(dir, "package.json"))) {
|
|
44
|
+
try {
|
|
45
|
+
if (JSON.parse(readFileSync(join(dir, "package.json"), "utf8"))[spec.pkgKey] != null) {
|
|
46
|
+
return `package.json#${spec.pkgKey}`;
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
/* unreadable package.json → not a match */
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
29
56
|
// Concrete gains kj's standard would add to a USER_OWNED config, detected by
|
|
30
57
|
// substring probes (no AST parse): each probe absent from the user's content
|
|
31
58
|
// becomes one improvement. Heuristic by design — conservative, never a false
|
|
@@ -70,19 +97,8 @@ function toArtifact(cfg) {
|
|
|
70
97
|
|
|
71
98
|
/** Locate the user's file for an artifact under `searchDir`; null if absent. */
|
|
72
99
|
function findArtifactFile(searchDir, artifact) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
if (artifact.pkgKey && existsSync(join(searchDir, "package.json"))) {
|
|
77
|
-
try {
|
|
78
|
-
if (JSON.parse(readFileSync(join(searchDir, "package.json"), "utf8"))[artifact.pkgKey] != null) {
|
|
79
|
-
return `package.json#${artifact.pkgKey}`;
|
|
80
|
-
}
|
|
81
|
-
} catch {
|
|
82
|
-
/* unreadable package.json → not a match */
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return null;
|
|
100
|
+
if (existsSync(join(searchDir, artifact.file))) return artifact.file;
|
|
101
|
+
return findEquivalentIn([searchDir], artifact.id);
|
|
86
102
|
}
|
|
87
103
|
|
|
88
104
|
/** Read the artifact body, or the JSON of an inline package.json key. */
|
|
@@ -9,6 +9,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
10
|
|
|
11
11
|
import { upsertManagedBlock } from "../utils/managed-markers.js";
|
|
12
|
+
import { findEquivalentIn } from "./advisory.js";
|
|
12
13
|
import { artifactIdForConfig, findAlternative } from "./alternatives.js";
|
|
13
14
|
import { CONFIGS_BY_LANGUAGE, UNIVERSAL_CONFIGS } from "./config-templates.js";
|
|
14
15
|
|
|
@@ -37,6 +38,15 @@ function seedInto(targetDir, configs, dryRun, prefix, results, altDirs = [target
|
|
|
37
38
|
results.push({ file, action: "covered", by: alt.foundAt });
|
|
38
39
|
continue;
|
|
39
40
|
}
|
|
41
|
+
// KJC-BUG-0119: a same-tool variant (eslint.config.mjs, .eslintrc.json,
|
|
42
|
+
// package.json#eslintConfig…) means the config already EXISTS — seeding
|
|
43
|
+
// kj's default filename next to it would silently ECLIPSE the user's
|
|
44
|
+
// real config (eslint resolves eslint.config.js before .mjs).
|
|
45
|
+
const variant = findEquivalentIn(altDirs, artifactIdForConfig(cfg));
|
|
46
|
+
if (variant) {
|
|
47
|
+
results.push({ file, action: "covered", by: variant });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
if (cfg.json) {
|
|
Binary file
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stop-on-sudo policy (KJC-TSK-0659). When a tool cannot be installed
|
|
3
|
+
* automatically (sudo needed, no package-manager route, or the attempt
|
|
4
|
+
* failed), kj must NOT continue degraded — a RAG with 0 chunks because
|
|
5
|
+
* Docker was missing helps nobody. Instead: emit ONE block with the exact
|
|
6
|
+
* commands for this machine's OS and exit with a distinctive code so a
|
|
7
|
+
* driving agent stops, shows the block, and waits for the user.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const PENDING_EXIT_CODE = 3;
|
|
11
|
+
|
|
12
|
+
const PENDING = new Set(["manual", "failed", "needs-user"]);
|
|
13
|
+
|
|
14
|
+
// Exact per-OS commands for the tools kj cannot install unattended.
|
|
15
|
+
// Multiple lines when the route depends on the distro/package manager.
|
|
16
|
+
const OS_COMMANDS = {
|
|
17
|
+
docker: {
|
|
18
|
+
linux: ["sudo apt-get install -y docker.io # Debian/Ubuntu", "sudo dnf install -y docker # Fedora/RHEL"],
|
|
19
|
+
darwin: ["brew install --cask docker # Docker Desktop, then open it once"],
|
|
20
|
+
win32: ["winget install Docker.DockerDesktop", "wsl --install # if WSL2 is not set up yet"],
|
|
21
|
+
},
|
|
22
|
+
git: {
|
|
23
|
+
linux: ["sudo apt-get install -y git # Debian/Ubuntu", "sudo dnf install -y git # Fedora/RHEL"],
|
|
24
|
+
darwin: ["brew install git # or: xcode-select --install"],
|
|
25
|
+
win32: ["winget install Git.Git"],
|
|
26
|
+
},
|
|
27
|
+
semgrep: {
|
|
28
|
+
linux: ["python3 -m pip install --user semgrep # or: pipx install semgrep"],
|
|
29
|
+
darwin: ["brew install semgrep"],
|
|
30
|
+
win32: ["python -m pip install --user semgrep"],
|
|
31
|
+
},
|
|
32
|
+
"osv-scanner": {
|
|
33
|
+
linux: ["go install github.com/google/osv-scanner/v2/cmd/osv-scanner@v2 # or download from GitHub releases"],
|
|
34
|
+
darwin: ["brew install osv-scanner"],
|
|
35
|
+
win32: ["winget install Google.OSVScanner # or download from GitHub releases"],
|
|
36
|
+
},
|
|
37
|
+
lighthouse: {
|
|
38
|
+
linux: ["npm install -g lighthouse"],
|
|
39
|
+
darwin: ["npm install -g lighthouse"],
|
|
40
|
+
win32: ["npm install -g lighthouse"],
|
|
41
|
+
},
|
|
42
|
+
ollama: {
|
|
43
|
+
linux: ["curl -fsSL https://ollama.com/install.sh | sh # official installer (uses sudo)", "ollama pull nomic-embed-text"],
|
|
44
|
+
darwin: ["brew install ollama && brew services start ollama", "ollama pull nomic-embed-text"],
|
|
45
|
+
win32: ["winget install Ollama.Ollama", "ollama pull nomic-embed-text"],
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Exact commands to install `tool` on `platform` (empty when unknown). */
|
|
50
|
+
export function osCommandsFor(tool, platform = process.platform) {
|
|
51
|
+
return OS_COMMANDS[tool]?.[platform] ?? [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Results that require the USER's hands. `manual`/`failed`/`needs-user`
|
|
56
|
+
* always qualify; `declined` only when nobody actually declined — a non-TTY
|
|
57
|
+
* run without --yes auto-answers the default, which for sudo installs is
|
|
58
|
+
* "no". A real interactive decline stays the user's own decision.
|
|
59
|
+
*/
|
|
60
|
+
export function collectPending(results, { tty = Boolean(process.stdin.isTTY), yes = false } = {}) {
|
|
61
|
+
return results.filter((r) => PENDING.has(r.action) || (r.action === "declined" && !tty && !yes));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The PENDING USER ACTION block: exact commands for THIS OS, then wait. */
|
|
65
|
+
export function renderPendingBlock(pending, { platform = process.platform, retry = "kj install-tools" } = {}) {
|
|
66
|
+
const lines = [
|
|
67
|
+
"════ PENDING USER ACTION ════════════════════════════════════════",
|
|
68
|
+
"kj could not finish this install by itself (sudo or a platform",
|
|
69
|
+
"installer is required). Karajan needs a COMPLETE environment —",
|
|
70
|
+
"do not continue degraded.",
|
|
71
|
+
"",
|
|
72
|
+
"Run in YOUR terminal:",
|
|
73
|
+
"",
|
|
74
|
+
];
|
|
75
|
+
for (const r of pending) {
|
|
76
|
+
const why = r.error ? `install failed: ${r.error}` : r.reason || "no automatic route on this machine";
|
|
77
|
+
lines.push(` ▸ ${r.tool} (${why})`);
|
|
78
|
+
const commands = r.commands
|
|
79
|
+
?? (r.command ? [r.command] : null)
|
|
80
|
+
?? (r.suggested ? [r.suggested] : null)
|
|
81
|
+
?? (osCommandsFor(r.tool, platform).length > 0 ? osCommandsFor(r.tool, platform) : null)
|
|
82
|
+
?? [`see ${r.manualUrl || "the tool's install docs"}`];
|
|
83
|
+
for (const c of commands) lines.push(` ${c}`);
|
|
84
|
+
lines.push("");
|
|
85
|
+
}
|
|
86
|
+
lines.push(
|
|
87
|
+
`When done, re-run: ${retry}`,
|
|
88
|
+
"Agents: STOP here, show this block to your user, and WAIT for their",
|
|
89
|
+
`go-ahead. Exit code ${PENDING_EXIT_CODE} means "pending user action" — never proceed past it.`,
|
|
90
|
+
);
|
|
91
|
+
return lines.join("\n");
|
|
92
|
+
}
|