karajan-code 4.7.0 → 4.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/library/logical-clocks-lamport.md +40 -0
- package/package.json +3 -2
- package/src/audit/deterministic-summary.js +1 -1
- package/src/audit/injection-findings.js +9 -0
- package/src/cli/register-meta.js +3 -0
- package/src/commands/audit.js +5 -1
- package/src/commands/rag.js +18 -2
- package/src/config/overrides.js +4 -1
- package/src/environment/briefs.js +4 -0
- package/src/environment/playbook.js +5 -0
- package/src/rag/library.js +62 -0
- package/src/roles/audit-role.js +19 -11
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Logical clocks & happened-before (Lamport, 1978)
|
|
2
|
+
|
|
3
|
+
## Problem signature
|
|
4
|
+
|
|
5
|
+
Events across processes/services need a consistent order, and wall clocks
|
|
6
|
+
disagree: "last write wins" flip-flops between replicas, audit logs interleave
|
|
7
|
+
impossibly, a consumer sees an update before the create it depends on.
|
|
8
|
+
|
|
9
|
+
## Reach for it when
|
|
10
|
+
|
|
11
|
+
- Two or more writers/emitters produce events whose CAUSAL order matters
|
|
12
|
+
(A caused B must never be observed as B before A).
|
|
13
|
+
- You are tempted to "fix it with NTP" or timestamps — clock skew makes
|
|
14
|
+
timestamp ordering a race, not an order.
|
|
15
|
+
- You need to detect concurrent (conflicting) updates, not just order them:
|
|
16
|
+
that is the vector-clock extension (one counter per node; compare
|
|
17
|
+
component-wise; incomparable = concurrent — see Dynamo's usage).
|
|
18
|
+
|
|
19
|
+
## Do NOT reach for it when
|
|
20
|
+
|
|
21
|
+
- A single writer (or a single database) already totally orders the events —
|
|
22
|
+
its sequence/autoincrement/WAL IS the clock. Most CRUD apps live here.
|
|
23
|
+
- You only need "roughly recent first" for humans (feeds, logs for reading):
|
|
24
|
+
wall-clock timestamps are fine and far simpler.
|
|
25
|
+
- You need a TOTAL order agreed by all nodes: logical clocks give a partial
|
|
26
|
+
causal order; total order needs consensus (Raft/Paxos) or a sequencer.
|
|
27
|
+
|
|
28
|
+
## Trade-offs
|
|
29
|
+
|
|
30
|
+
- Lamport clocks are one counter: tiny, but they cannot tell concurrent from
|
|
31
|
+
ordered. Vector clocks can, at O(nodes) metadata per event that must be
|
|
32
|
+
carried, stored and pruned.
|
|
33
|
+
- Causality tracking pushes conflict RESOLUTION to the reader (semantic
|
|
34
|
+
merge, CRDTs, or ask-the-user) — ordering was the easy half.
|
|
35
|
+
|
|
36
|
+
## Canonical source
|
|
37
|
+
|
|
38
|
+
Leslie Lamport, "Time, Clocks, and the Ordering of Events in a Distributed
|
|
39
|
+
System", Communications of the ACM, 1978. The happened-before relation and
|
|
40
|
+
the clock condition come verbatim from it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "karajan-code",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.9.0",
|
|
4
4
|
"description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"files": [
|
|
49
49
|
"src/",
|
|
50
50
|
"bin/",
|
|
51
|
+
"library/",
|
|
51
52
|
"templates/",
|
|
52
53
|
"scripts/",
|
|
53
54
|
"vendor/tree-sitter-grammars/",
|
|
@@ -108,7 +109,7 @@
|
|
|
108
109
|
"express-rate-limit": "^8.5.0",
|
|
109
110
|
"helmet": "^8.1.0",
|
|
110
111
|
"js-yaml": "^4.2.0",
|
|
111
|
-
"karajan-core": "^1.
|
|
112
|
+
"karajan-core": "^1.4.0",
|
|
112
113
|
"karajan-rag": "^1.2.0",
|
|
113
114
|
"knip": "^6.15.0",
|
|
114
115
|
"madge": "^8.0.0",
|
|
@@ -65,7 +65,7 @@ function formatAiSlopBlock(slop) {
|
|
|
65
65
|
|
|
66
66
|
function formatInjectionBlock(inj) {
|
|
67
67
|
if (!inj.available) return ["### Prompt-injection scan", `- Status: not available — ${inj.reason || "scan failed"}`, ""];
|
|
68
|
-
const lines = ["### Prompt-injection scan (
|
|
68
|
+
const lines = ["### Prompt-injection scan (agent-context surface: CLAUDE/AGENTS/GEMINI.md, templates, .rulesync, .karajan)", `- Files scanned: ${inj.scanned ?? 0}`, `- Findings: ${inj.total ?? 0}`];
|
|
69
69
|
if ((inj.total ?? 0) > 0) {
|
|
70
70
|
for (const [severity, items] of Object.entries(groupInjectionBySeverity(inj.findings || []))) {
|
|
71
71
|
if (items.length === 0) continue;
|
|
@@ -17,6 +17,15 @@ const SCAN_TARGETS = [
|
|
|
17
17
|
{ rel: ".karajan/specs", kind: "dir" },
|
|
18
18
|
{ rel: ".karajan/onboarding", kind: "dir" },
|
|
19
19
|
{ rel: ".karajan/domain.md", kind: "file" },
|
|
20
|
+
// KJC-TSK-0695: the agent-context surface — whatever lands in the host
|
|
21
|
+
// agent's context every session is the real injection vector.
|
|
22
|
+
{ rel: "CLAUDE.md", kind: "file" },
|
|
23
|
+
{ rel: "AGENTS.md", kind: "file" },
|
|
24
|
+
{ rel: "GEMINI.md", kind: "file" },
|
|
25
|
+
{ rel: ".cursorrules", kind: "file" },
|
|
26
|
+
{ rel: path.join(".github", "copilot-instructions.md"), kind: "file" },
|
|
27
|
+
{ rel: "templates", kind: "dir" },
|
|
28
|
+
{ rel: ".rulesync", kind: "dir" },
|
|
20
29
|
];
|
|
21
30
|
|
|
22
31
|
export async function collectInjectionFindings(rootDir, logger = null) {
|
package/src/cli/register-meta.js
CHANGED
|
@@ -294,6 +294,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
294
294
|
.option("--rag-mode <mode>", "hybrid (default) | semantic (cosine only) | keyword (BM25 only)", "hybrid")
|
|
295
295
|
.option("--alpha <n>", "Hybrid weight for semantic component (0..1). Default 0.6", "0.6")
|
|
296
296
|
.option("--where <clause>", "Metadata filter, e.g. 'symbol=loadConfig' or 'hu_id=HU-003 AND kind=plan'")
|
|
297
|
+
.option("--library", "Query the distilled engineering canon (pattern cards shipped with kj + ~/.karajan/library + project .karajan/library) instead of the project corpus. Cards carry problem signatures, when-NOT-to-apply and canonical citations (KJC-TSK-0697).")
|
|
297
298
|
.option("--rerank", "Apply cross-encoder rerank to top-K (needs @huggingface/transformers, slower but more precise)")
|
|
298
299
|
.option("--rerank-model <name>", "Cross-encoder model id. Default: Xenova/ms-marco-MiniLM-L-6-v2")
|
|
299
300
|
.option("--json", "Output the hits as JSON")
|
|
@@ -379,6 +380,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
379
380
|
.option("--trend", "Append a sparkline of the harness score across the last 10 runs (uses .karajan/audit-history.db). KJC-TSK-0473.")
|
|
380
381
|
.option("--report-file <path>", "Write the audit report to disk in addition to stdout. <path> may be a file (extension drives format: .md or .json) or a directory (creates audit-<ISO>.<md|json> inside). $KJ_AUDIT_REPORT_DIR env var is used as default directory if no --report-file is given.")
|
|
381
382
|
.option("--deterministic-only", "Skip the LLM analysis entirely. Print/persist only the deterministic findings (basalCost, sonar, stack, growth-delta, webperf). Zero tokens spent. Compatible with --report-file and --json.")
|
|
383
|
+
.option("--security", "Focused deterministic security pass: prompt-injection scan over the agent-context surface (CLAUDE.md/AGENTS.md/templates/.rulesync/.karajan) + OSV + Semgrep + Sonar. Skips basal-cost, webperf, madge, knip, ai-slop, harness and the LLM — zero tokens. Designed for agents to self-invoke when a task touches sensitive surface (KJC-TSK-0695).")
|
|
382
384
|
.option("-y, --yes", "Auto-confirm the 'Continue with LLM analysis?' prompt. Useful in scripts that want the full audit non-interactively. CI/non-TTY paths already auto-confirm without this flag.")
|
|
383
385
|
.action(async (task, flags) => {
|
|
384
386
|
await withConfig(pkgVersion, "audit", flags, async ({ config, logger }) => {
|
|
@@ -402,6 +404,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
402
404
|
noAiSlop: flags.aiSlop === false,
|
|
403
405
|
reportFile: flags.reportFile || null,
|
|
404
406
|
deterministicOnly: Boolean(flags.deterministicOnly),
|
|
407
|
+
security: Boolean(flags.security),
|
|
405
408
|
yes: Boolean(flags.yes),
|
|
406
409
|
trend: Boolean(flags.trend),
|
|
407
410
|
});
|
package/src/commands/audit.js
CHANGED
|
@@ -237,7 +237,10 @@ function describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, noHar
|
|
|
237
237
|
return parts.join(" ");
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
export async function auditCommand({ task, config, logger, dimensions, json, agentReadiness, path: pathArg, noSonar = false, noOsv = false, noSemgrep = false, noHarness = false, noAiSlop = false, reportFile = null, deterministicOnly = false, yes = false, trend = false, promptFn = null }) {
|
|
240
|
+
export async function auditCommand({ task, config, logger, dimensions, json, agentReadiness, path: pathArg, noSonar = false, noOsv = false, noSemgrep = false, noHarness = false, noAiSlop = false, reportFile = null, deterministicOnly = false, yes = false, trend = false, promptFn = null, security = false }) {
|
|
241
|
+
// KJC-TSK-0695: --security = the focused pass an agent self-invokes.
|
|
242
|
+
// Deterministic by definition (zero tokens) and skips non-security stages.
|
|
243
|
+
if (security) { deterministicOnly = true; noHarness = true; noAiSlop = true; }
|
|
241
244
|
// --agent-readiness is a STANDALONE, deterministic, LLM-free audit
|
|
242
245
|
// dimension. It scores any third-party repo for AI-agent readability
|
|
243
246
|
// (llms.txt presence, page token budgets, robots allowlist, etc.).
|
|
@@ -281,6 +284,7 @@ export async function auditCommand({ task, config, logger, dimensions, json, age
|
|
|
281
284
|
noOsv,
|
|
282
285
|
noSemgrep,
|
|
283
286
|
noAiSlop,
|
|
287
|
+
...(security ? { securityOnly: true } : {}),
|
|
284
288
|
};
|
|
285
289
|
const deterministicCtx = await role.collectDeterministic(roleInput);
|
|
286
290
|
const deterministicMd = formatDeterministicSummary(deterministicCtx);
|
package/src/commands/rag.js
CHANGED
|
@@ -7,6 +7,7 @@ import { makeGovernedEmbedder } from "../rag/governed-embedder.js";
|
|
|
7
7
|
import { indexProject, indexProjectDelta } from "../rag/indexer.js";
|
|
8
8
|
import { query } from "../rag/retriever.js";
|
|
9
9
|
import { installPostMergeHook, maybeAutoUpdate } from "../rag/auto-update.js";
|
|
10
|
+
import { indexLibrary, LIBRARY_PROJECT } from "../rag/library.js";
|
|
10
11
|
import { loadGoldenQueries, runEval } from "../rag/eval.js";
|
|
11
12
|
import { getKarajanHome } from "../utils/paths.js";
|
|
12
13
|
|
|
@@ -47,6 +48,13 @@ export async function ragIndexCommand({ config, logger, flags = {} }) {
|
|
|
47
48
|
});
|
|
48
49
|
}
|
|
49
50
|
if (totals.head) setLastIndexedCommit(db, slug, totals.head);
|
|
51
|
+
// KJC-TSK-0697 — the library corpus refreshes on every index run.
|
|
52
|
+
// Best-effort and cheap: a handful of cards, content-hash dedup.
|
|
53
|
+
try {
|
|
54
|
+
await indexLibrary({ db, embedder, logger, projectDir });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
logger.warn?.(`[rag] library index failed: ${err.message}`);
|
|
57
|
+
}
|
|
50
58
|
if (flags.json) {
|
|
51
59
|
process.stdout.write(`${JSON.stringify(totals)}\n`);
|
|
52
60
|
} else {
|
|
@@ -87,7 +95,10 @@ export async function ragQueryCommand({ text, config, logger, flags = {} }) {
|
|
|
87
95
|
// <slug>` overrides. Pre-v2.27 chunks with NULL slug are only visible
|
|
88
96
|
// when no filter is in effect.
|
|
89
97
|
const detected = projectSlug(config?.projectDir || process.cwd());
|
|
90
|
-
|
|
98
|
+
// KJC-TSK-0697 — `--library` targets the distilled-canon collection:
|
|
99
|
+
// its own project namespace plus the kind filter.
|
|
100
|
+
const library = Boolean(flags.library);
|
|
101
|
+
const project = library ? LIBRARY_PROJECT : (flags.project === "all" ? null : (flags.project || detected || null));
|
|
91
102
|
// KJC-BUG-0061 follow-up: align the CLI `--json` shape with the MCP
|
|
92
103
|
// handler. The MCP tool responds `{ hits: [], empty: true, topK, scope }`
|
|
93
104
|
// so agents (and the `/kj-rag-query` skill from Camino B) have a
|
|
@@ -101,7 +112,12 @@ export async function ragQueryCommand({ text, config, logger, flags = {} }) {
|
|
|
101
112
|
}
|
|
102
113
|
const mode = flags.mode || "hybrid";
|
|
103
114
|
const alpha = Math.max(0, Math.min(1, Number(flags.alpha) || 0.6));
|
|
104
|
-
|
|
115
|
+
// Safe by grammar: parseWhere (core where-parser.js) accepts ONLY
|
|
116
|
+
// AND-joined key=value pairs — an OR or parens never parses, so the
|
|
117
|
+
// composed clause cannot leak past the kind filter; it fails loudly.
|
|
118
|
+
const where = library
|
|
119
|
+
? (flags.where ? `kind=library AND ${flags.where}` : "kind=library")
|
|
120
|
+
: (flags.where || null);
|
|
105
121
|
const rerankOpts = flags.rerank ? { model: flags.rerankModel } : null;
|
|
106
122
|
const hits = await query(db, makeGovernedEmbedder(config), text, { topK, scope, project, mode, alpha, where, rerankOpts });
|
|
107
123
|
if (flags.json) {
|
package/src/config/overrides.js
CHANGED
|
@@ -77,8 +77,11 @@ const UNDEF_CHECK_FLAGS = [
|
|
|
77
77
|
];
|
|
78
78
|
|
|
79
79
|
function applyRoleOverrides(out, flags) {
|
|
80
|
+
// A provider override is always a string (`--security codex`). A bare
|
|
81
|
+
// boolean means the flag belongs to the command itself (`kj audit
|
|
82
|
+
// --security`, KJC-TSK-0695) and must not become a provider.
|
|
80
83
|
for (const [flag, role] of ROLE_PROVIDER_FLAGS) {
|
|
81
|
-
if (flags[flag]) out.roles[role].provider = flags[flag];
|
|
84
|
+
if (typeof flags[flag] === "string" && flags[flag]) out.roles[role].provider = flags[flag];
|
|
82
85
|
}
|
|
83
86
|
// coder/reviewer also update top-level aliases
|
|
84
87
|
if (flags.coder) out.coder = flags.coder;
|
|
@@ -36,6 +36,7 @@ const B = {
|
|
|
36
36
|
"Behavior changes carry their test in the same step.",
|
|
37
37
|
"Data-model or API changes never share a step with anything else.",
|
|
38
38
|
"No PR beyond ~150 net lines — partition upfront, not at the gate.",
|
|
39
|
+
"Non-trivial task? Name at least two approaches — one as if the codebase didn't exist — and say why the winner won.",
|
|
39
40
|
],
|
|
40
41
|
"numbered-free plan: steps {description, files}, risks, out-of-scope."),
|
|
41
42
|
},
|
|
@@ -56,6 +57,8 @@ const B = {
|
|
|
56
57
|
[
|
|
57
58
|
"Check the ADRs before deciding — never against an accepted one.",
|
|
58
59
|
"Prefer the design that deletes code over the one that adds layers.",
|
|
60
|
+
"One alternative answers: what would you build as if the codebase didn't exist? Picking the legacy-shaped path is fine — picking it by inertia is not.",
|
|
61
|
+
"Ground that alternative in the canon: `kj rag query --library \"<problem signature>\"` serves distilled pattern cards with when-NOT-to-apply and citations.",
|
|
59
62
|
],
|
|
60
63
|
"chosen design, discarded alternatives with reasons, affected boundaries."),
|
|
61
64
|
},
|
|
@@ -79,6 +82,7 @@ const B = {
|
|
|
79
82
|
[
|
|
80
83
|
"These findings are never overridable — not even by Solomon arbitration.",
|
|
81
84
|
"PII is never logged; new dependencies need a reason and a pinned version.",
|
|
85
|
+
"Sensitive surface (auth, user input, secrets, network, deps)? Self-invoke `kj audit --security` — deterministic, zero tokens: prompt-injection over the agent-context files + OSV + Semgrep + Sonar — and remediate the findings before requesting review.",
|
|
82
86
|
],
|
|
83
87
|
"findings with severity, category and file:line — or an explicit 'no security surface touched'."),
|
|
84
88
|
},
|
|
@@ -66,10 +66,15 @@ Invariants (the git gates enforce these — they are not suggestions):
|
|
|
66
66
|
and it must be reviewed again. Disagree with a rejection? \`kj solomon\`.
|
|
67
67
|
- Security findings are never overridable — not even by arbitration. You
|
|
68
68
|
absorb the security role: \`kj brief security\` states what must be true.
|
|
69
|
+
Task touches auth, user input, secrets, network or deps? Run
|
|
70
|
+
\`kj audit --security\` (zero tokens) and remediate BEFORE the review.
|
|
69
71
|
- A design decision the card's AC don't cover belongs to the USER: record
|
|
70
72
|
it as a proposed ADR (\`kj adr add\`) and ask — never bury it in a PR
|
|
71
73
|
bullet. An oversized-diff warning is not an opinion either: partition,
|
|
72
74
|
or get an explicit OK.
|
|
75
|
+
- A non-trivial plan names at least two approaches — one as if the
|
|
76
|
+
codebase didn't exist — and says why the winner won. Following the
|
|
77
|
+
legacy line is a choice, never a default.
|
|
73
78
|
- Branch first: never commit on the base branch — every change reaches it
|
|
74
79
|
through an atomic PR (~150 net lines, Conventional Commits). More than one
|
|
75
80
|
task, or the base tree must stay untouched? \`kj worktree start <slug>\`
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* library — the distilled engineering canon as its own RAG collection
|
|
3
|
+
* (KJC-TSK-0697). Markdown cards from `<pkg>/library/`, `~/.karajan/library/`
|
|
4
|
+
* and `<project>/.karajan/library/` index into the SAME global rag.db,
|
|
5
|
+
* isolated by project="library" + kind="library", so `kj rag query --library`
|
|
6
|
+
* reaches the canon and normal project queries never see it. The architect
|
|
7
|
+
* consults it to ground the greenfield alternative (KJC-TSK-0696).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { extname, join } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
|
|
16
|
+
import { chunkMarkdown } from "./chunker.js";
|
|
17
|
+
import { insertChunk, deleteChunksBySource, findChunkByHash } from "./vec-store.js";
|
|
18
|
+
|
|
19
|
+
export const LIBRARY_PROJECT = "library";
|
|
20
|
+
|
|
21
|
+
const SHIPPED_LIBRARY_DIR = fileURLToPath(new URL("../../library", import.meta.url));
|
|
22
|
+
|
|
23
|
+
/** Existing library dirs, shipped canon first. */
|
|
24
|
+
export function libraryDirs({ pkgLibraryDir = SHIPPED_LIBRARY_DIR, home = os.homedir(), projectDir = process.cwd() } = {}) {
|
|
25
|
+
const candidates = [pkgLibraryDir, join(home, ".karajan", "library"), join(projectDir, ".karajan", "library")];
|
|
26
|
+
return candidates.filter((d) => existsSync(d));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Index every markdown card found in the library dirs. Idempotent: chunks
|
|
31
|
+
* re-index per source and identical bodies skip the embedder (content-hash
|
|
32
|
+
* dedup within the library project).
|
|
33
|
+
*/
|
|
34
|
+
export async function indexLibrary({ db, embedder, logger = console, pkgLibraryDir, home, projectDir } = {}) {
|
|
35
|
+
let indexed = 0, failed = 0, files = 0;
|
|
36
|
+
for (const dir of libraryDirs({ pkgLibraryDir, home, projectDir })) {
|
|
37
|
+
for (const name of readdirSync(dir)) {
|
|
38
|
+
if (extname(name).toLowerCase() !== ".md") continue;
|
|
39
|
+
const path = join(dir, name);
|
|
40
|
+
files += 1;
|
|
41
|
+
const chunks = chunkMarkdown(readFileSync(path, "utf8"), { path, kind: "library" });
|
|
42
|
+
const hashed = chunks.map((ch) => ({ ch, contentHash: createHash("sha256").update(ch.text).digest("hex") }));
|
|
43
|
+
// Unchanged card (every chunk body already known) → skip before the
|
|
44
|
+
// delete, so idempotent re-runs never touch the embedder.
|
|
45
|
+
if (hashed.length && hashed.every(({ contentHash }) => findChunkByHash(db, contentHash, LIBRARY_PROJECT))) continue;
|
|
46
|
+
deleteChunksBySource(db, path);
|
|
47
|
+
for (const { ch, contentHash } of hashed) {
|
|
48
|
+
try {
|
|
49
|
+
if (findChunkByHash(db, contentHash, LIBRARY_PROJECT)) continue;
|
|
50
|
+
const embedding = await embedder.embed(ch.text);
|
|
51
|
+
insertChunk(db, { source: path, kind: "library", text: ch.text, metadata: ch.metadata, embedding, project: LIBRARY_PROJECT, contentHash });
|
|
52
|
+
indexed += 1;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
failed += 1;
|
|
55
|
+
logger.warn?.(`[rag-library] embed failed for ${path}: ${err.message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (files) logger.info?.(`[rag-library] ${files} card(s) → ${indexed} chunk(s) (${failed} failed)`);
|
|
61
|
+
return { files, indexed, failed };
|
|
62
|
+
}
|
package/src/roles/audit-role.js
CHANGED
|
@@ -48,13 +48,17 @@ export class AuditRole extends AgentRole {
|
|
|
48
48
|
* execute() would gather internally)
|
|
49
49
|
*/
|
|
50
50
|
async collectDeterministic(input) {
|
|
51
|
+
// KJC-TSK-0695: securityOnly = the focused pass an agent self-invokes
|
|
52
|
+
// when a task touches sensitive surface. Only the security collectors
|
|
53
|
+
// run (sonar, osv, semgrep, injection) — zero tokens, fast.
|
|
54
|
+
const securityOnly = typeof input === "object" ? Boolean(input?.securityOnly) : false;
|
|
51
55
|
const noSonar = typeof input === "object" ? Boolean(input?.noSonar) : false;
|
|
52
56
|
const noOsv = typeof input === "object" ? Boolean(input?.noOsv) : false;
|
|
53
57
|
const noSemgrep = typeof input === "object" ? Boolean(input?.noSemgrep) : false;
|
|
54
|
-
const noMadge = typeof input === "object" ? Boolean(input?.noMadge) : false;
|
|
55
|
-
const noKnip = typeof input === "object" ? Boolean(input?.noKnip) : false;
|
|
58
|
+
const noMadge = (typeof input === "object" ? Boolean(input?.noMadge) : false) || securityOnly;
|
|
59
|
+
const noKnip = (typeof input === "object" ? Boolean(input?.noKnip) : false) || securityOnly;
|
|
56
60
|
const noInjectionScan = typeof input === "object" ? Boolean(input?.noInjectionScan) : false;
|
|
57
|
-
const noAiSlop = typeof input === "object" ? Boolean(input?.noAiSlop) : false;
|
|
61
|
+
const noAiSlop = (typeof input === "object" ? Boolean(input?.noAiSlop) : false) || securityOnly;
|
|
58
62
|
const projectDir = this.config?.projectDir || process.cwd();
|
|
59
63
|
let basalCost = null;
|
|
60
64
|
let growthDelta = null;
|
|
@@ -67,11 +71,13 @@ export class AuditRole extends AgentRole {
|
|
|
67
71
|
let deadExports = null;
|
|
68
72
|
let injectionFindings = null;
|
|
69
73
|
let aiSlop = null;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
if (!securityOnly) {
|
|
75
|
+
try {
|
|
76
|
+
basalCost = await measureBasalCost(projectDir);
|
|
77
|
+
const previous = await loadPreviousAudit(projectDir);
|
|
78
|
+
growthDelta = computeGrowthDelta(basalCost, previous);
|
|
79
|
+
} catch { /* basal cost is best-effort */ }
|
|
80
|
+
}
|
|
75
81
|
try {
|
|
76
82
|
stack = await detectProjectStack(projectDir);
|
|
77
83
|
} catch { /* stack detect is best-effort */ }
|
|
@@ -80,9 +86,11 @@ export class AuditRole extends AgentRole {
|
|
|
80
86
|
sonarFindings = await collectSonarFindings(this.config, this.logger);
|
|
81
87
|
} catch { /* sonar fetch is best-effort */ }
|
|
82
88
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
if (!securityOnly) {
|
|
90
|
+
try {
|
|
91
|
+
webperf = collectWebPerfInput(stack, this.config);
|
|
92
|
+
} catch { /* webperf input is best-effort */ }
|
|
93
|
+
}
|
|
86
94
|
// OSV vulnerabilities — KJC-TSK-0365. Best-effort: missing
|
|
87
95
|
// osv-scanner binary returns available:false and the audit
|
|
88
96
|
// continues without the section.
|