karajan-code 4.39.0 → 4.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/audit/ai-slop-findings.js +4 -2
- package/src/audit/circular-deps.js +8 -7
- package/src/audit/webperf-input.js +3 -1
- package/src/cli/advanced-commands.js +1 -1
- package/src/cli/register-meta.js +112 -1
- package/src/commands/init.js +6 -2
- package/src/commands/rules-approve.js +59 -0
- package/src/commands/rules-compile.js +116 -0
- package/src/commands/rules-decide.js +51 -0
- package/src/commands/rules-review.js +44 -0
- package/src/commands/rules.js +169 -0
- package/src/config/loader.js +23 -1
- package/src/environment/adr.js +5 -2
- package/src/harden/config-templates.js +3 -0
- package/src/harden/human-act.js +70 -0
- package/src/harden/phone-sign.js +26 -0
- package/src/harden/sentinel/pretooluse-rules.mjs +25 -0
- package/src/harden/sentinel/sentinel-bash-write.mjs +2 -6
- package/src/harden/sentinel/sentinel-discard.mjs +4 -2
- package/src/harden/sentinel/sentinel-rules.mjs +86 -0
- package/src/harden/sentinel/sentinel-shell.mjs +17 -0
- package/src/harden/sentinel/sessionstart.mjs +18 -9
- package/src/harden/sentinel-hooks.js +23 -2
- package/src/harden/supervisor-commit.js +13 -56
- package/src/mcp/handlers/run-handler.js +5 -0
- package/src/mcp/sovereignty-guard.js +16 -15
- package/src/orchestrator/preflight-checks.js +4 -3
- package/src/policy/supervisor-verify.js +12 -2
- package/src/privacy/scan.js +7 -0
- package/src/review/gate-gitignore.js +8 -0
- package/src/rules/approval-view.js +49 -0
- package/src/rules/compiled.js +108 -0
- package/src/rules/coverage.js +23 -0
- package/src/rules/evaluate.js +60 -0
- package/src/rules/inventory.js +118 -0
- package/src/sonar/config-resolver.js +19 -1
- package/src/utils/run-log.js +74 -2
- package/src/utils/stack-detect.js +12 -0
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
* Consumers: scanner.js, preflight-checks.js, api.js.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
8
10
|
import { loadSonarCredentials } from "./credentials.js";
|
|
11
|
+
import { getKarajanHome } from "../utils/paths.js";
|
|
12
|
+
|
|
13
|
+
/** KJC-BUG-0266: the places a token is looked for, named in every "no token" message. */
|
|
14
|
+
export const sonarTokenSources = () =>
|
|
15
|
+
`KJ_SONAR_TOKEN, sonarqube.token in kj.config.yml, SONAR_TOKEN, ${path.join(getKarajanHome(), "sonar-credentials.json")} or ${sonarTokenFilePath()}`;
|
|
9
16
|
|
|
10
17
|
const DEFAULT_HOST = "http://localhost:9000";
|
|
11
18
|
|
|
@@ -54,7 +61,18 @@ export async function resolveSonarTokenAsync(config = {}) {
|
|
|
54
61
|
|
|
55
62
|
// Check credentials file for persisted token
|
|
56
63
|
const fileCreds = (await loadSonarCredentials()) || {};
|
|
57
|
-
return fileCreds.token || null;
|
|
64
|
+
return fileCreds.token || readSonarTokenFile() || null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Where the token bootstrap saves the token (mode 0600), in the active Karajan home. */
|
|
68
|
+
export const sonarTokenFilePath = () => path.join(getKarajanHome(), "sonar.token");
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* KJC-BUG-0266: the token bootstrap persisted the token to <karajan home>/sonar.token
|
|
72
|
+
* and no resolver read it back, so a home with only that file looked tokenless.
|
|
73
|
+
*/
|
|
74
|
+
export function readSonarTokenFile() {
|
|
75
|
+
try { return fs.readFileSync(sonarTokenFilePath(), "utf8").trim() || null; } catch { return null; }
|
|
58
76
|
}
|
|
59
77
|
|
|
60
78
|
/**
|
package/src/utils/run-log.js
CHANGED
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
* Log location: <projectDir>/.kj/run.log (overwritten each run)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
11
12
|
import fs from "node:fs";
|
|
12
13
|
import path from "node:path";
|
|
13
14
|
|
|
14
15
|
const LOG_FILENAME = "run.log";
|
|
16
|
+
const LOCK_FILENAME = "run.lock";
|
|
15
17
|
|
|
16
18
|
function resolveLogDir(baseDir) {
|
|
17
19
|
return path.join(baseDir || process.cwd(), ".kj");
|
|
@@ -54,8 +56,15 @@ export function createRunLog(projectDir) {
|
|
|
54
56
|
const logDir = resolveLogDir(projectDir);
|
|
55
57
|
ensureDir(logDir);
|
|
56
58
|
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
+
// KJC-BUG-0250: the run lock, taken atomically and owned by this run's token.
|
|
60
|
+
// close() releases it only while it is still ours (every caller closes in a
|
|
61
|
+
// finally); a lock whose process is gone is stale and taken over.
|
|
62
|
+
const startedAt = new Date().toISOString();
|
|
63
|
+
const lockPath = path.join(logDir, LOCK_FILENAME);
|
|
64
|
+
const token = randomUUID();
|
|
65
|
+
const owned = acquireLock(lockPath, { pid: process.pid, startedAt, token });
|
|
66
|
+
// Truncate/create the log file, unless another live run owns it.
|
|
67
|
+
if (owned) fs.writeFileSync(logPath, `--- Karajan run started at ${startedAt} ---\n`);
|
|
59
68
|
|
|
60
69
|
// Audit recommendation (perf-LOW): suggested createWriteStream + queue.
|
|
61
70
|
// Tried it; broke the contract. `readRunLog` (used by `kj_status`,
|
|
@@ -102,16 +111,79 @@ export function createRunLog(projectDir) {
|
|
|
102
111
|
fd = null;
|
|
103
112
|
}
|
|
104
113
|
} catch { /* best-effort */ }
|
|
114
|
+
if (owned && readLock(lockPath)?.token === token) {
|
|
115
|
+
try { fs.rmSync(lockPath, { force: true }); } catch { /* best-effort */ }
|
|
116
|
+
}
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
return {
|
|
108
120
|
logEvent,
|
|
109
121
|
logText,
|
|
110
122
|
close,
|
|
123
|
+
/** false when another live run already held the project */
|
|
124
|
+
owned,
|
|
111
125
|
get path() { return logPath; }
|
|
112
126
|
};
|
|
113
127
|
}
|
|
114
128
|
|
|
129
|
+
const readLock = (lockPath) => {
|
|
130
|
+
try { return JSON.parse(fs.readFileSync(lockPath, "utf8")); } catch { return null; }
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const isPidAlive = (pid) => {
|
|
134
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
135
|
+
try { process.kill(pid, 0); return true; } catch (err) { return err.code === "EPERM"; }
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const createExclusive = (file, body) => {
|
|
139
|
+
try { fs.writeFileSync(file, body, { flag: "wx" }); return true; } catch { return false; }
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// The lock appears with its whole content or not at all: written to a private
|
|
143
|
+
// file first, then hard-linked into place (link fails if the path exists), so
|
|
144
|
+
// nobody can read a half-written lock and take it for stale.
|
|
145
|
+
function linkLock(lockPath, body, token) {
|
|
146
|
+
const tmp = `${lockPath}.${token}.tmp`;
|
|
147
|
+
try {
|
|
148
|
+
fs.writeFileSync(tmp, body);
|
|
149
|
+
fs.linkSync(tmp, lockPath);
|
|
150
|
+
return true;
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
} finally {
|
|
154
|
+
try { fs.rmSync(tmp, { force: true }); } catch { /* leftover, harmless */ }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function acquireLock(lockPath, holder) {
|
|
159
|
+
const body = JSON.stringify(holder);
|
|
160
|
+
if (linkLock(lockPath, body, holder.token)) return true;
|
|
161
|
+
if (isPidAlive(readLock(lockPath)?.pid)) return false;
|
|
162
|
+
// Stale (its process is gone, or unreadable). Only one racer at a time may take
|
|
163
|
+
// it over: the takeover mutex is itself an exclusive create. Inside it the lock
|
|
164
|
+
// is read again and replaced only if it is still stale; nobody else removes it,
|
|
165
|
+
// and the exclusive create that follows loses to anyone who got there first.
|
|
166
|
+
const mutex = `${lockPath}.takeover`;
|
|
167
|
+
clearStaleMutex(mutex);
|
|
168
|
+
if (!createExclusive(mutex, holder.token)) return false;
|
|
169
|
+
try {
|
|
170
|
+
if (isPidAlive(readLock(lockPath)?.pid)) return false;
|
|
171
|
+
try { fs.rmSync(lockPath, { force: true }); } catch { return false; }
|
|
172
|
+
return linkLock(lockPath, body, holder.token);
|
|
173
|
+
} finally {
|
|
174
|
+
try { fs.rmSync(mutex, { force: true }); } catch { /* leftover, cleared when stale */ }
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// A takeover takes milliseconds; a mutex older than this was left by a process
|
|
179
|
+
// that died inside one.
|
|
180
|
+
const TAKEOVER_STALE_MS = 10_000;
|
|
181
|
+
function clearStaleMutex(mutex) {
|
|
182
|
+
try {
|
|
183
|
+
if (Date.now() - fs.statSync(mutex).mtimeMs > TAKEOVER_STALE_MS) fs.rmSync(mutex, { force: true });
|
|
184
|
+
} catch { /* none */ }
|
|
185
|
+
}
|
|
186
|
+
|
|
115
187
|
const KJ_TOOLS = ["kj_run", "kj_code", "kj_plan"];
|
|
116
188
|
|
|
117
189
|
function detectRunStart(line, status) {
|
|
@@ -46,6 +46,9 @@ const LANGUAGE_FILE_MARKERS = [
|
|
|
46
46
|
{ file: "Package.swift", language: "swift", type: "backend" },
|
|
47
47
|
];
|
|
48
48
|
|
|
49
|
+
/** A page served as it is: where a static site (or the static half of an app) keeps it. */
|
|
50
|
+
const STATIC_SITE_MARKERS = ["index.html", path.join("public", "index.html"), path.join("src", "index.html")];
|
|
51
|
+
|
|
49
52
|
/**
|
|
50
53
|
* Maps framework names to skill names for auto-install suggestions.
|
|
51
54
|
*/
|
|
@@ -109,6 +112,15 @@ export async function detectProjectStack(projectDir) {
|
|
|
109
112
|
} catch { /* file not found */ }
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
// 3. KJC-BUG-0267: a static site has a frontend with no framework to show for it.
|
|
116
|
+
for (const page of STATIC_SITE_MARKERS) {
|
|
117
|
+
try {
|
|
118
|
+
await fs.access(path.join(projectDir, page));
|
|
119
|
+
hasFrontend = true;
|
|
120
|
+
break;
|
|
121
|
+
} catch { /* file not found */ }
|
|
122
|
+
}
|
|
123
|
+
|
|
112
124
|
const frameworkList = Array.from(frameworks);
|
|
113
125
|
const isFullstack = hasFrontend && hasBackend;
|
|
114
126
|
|