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.
@@ -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.7.0",
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.0.0",
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 (specs / domain / onboarding)", `- Files scanned: ${inj.scanned ?? 0}`, `- Findings: ${inj.total ?? 0}`];
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) {
@@ -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
  });
@@ -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);
@@ -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
- const project = flags.project === "all" ? null : (flags.project || detected || null);
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
- const where = flags.where || null;
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) {
@@ -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
+ }
@@ -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
- try {
71
- basalCost = await measureBasalCost(projectDir);
72
- const previous = await loadPreviousAudit(projectDir);
73
- growthDelta = computeGrowthDelta(basalCost, previous);
74
- } catch { /* basal cost is best-effort */ }
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
- try {
84
- webperf = collectWebPerfInput(stack, this.config);
85
- } catch { /* webperf input is best-effort */ }
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.