@holmes-lab/holmes-kit 0.1.3 → 0.1.5

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/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+ <!-- @implements A-SPEC-209 -->
8
+ ## [0.1.5] - 2026-08-19
9
+
10
+ ### Added
11
+ - **Subprocess Secret Isolation (REQ-205)**: Standardized `cleanSubprocessEnv` to scrub sensitive environment variables (`HOLMES_ROLE`, `HOLMES_ENV_CHANNEL`, `HOLMES_APPROVAL`, `HOLMES_LEDGER_KEY`) across all child process executions.
12
+ - **Multi-Ledger Dynamic Indexing Engine (REQ-206)**: High-performance mtime/size-based indexing cache with immutable deep cloning (`cloneEvents`) and fail-closed error handling in `ProvenanceLedger`.
13
+ - **Doctor Self-Healing Engine (REQ-207)**: Integrated `runDoctor({ autoFix: true })` auto-remediation loop and `Check.remediate()` callbacks for system diagnostic repair.
14
+ - **CPG Incremental Indexer (REQ-208)**: SHA-256 content-hash AST parsing skip in `ScanFileCache` & `CpgScanner` with scoped directory mark-and-sweep pruning and deep `DataFlowFacts` cloning.
15
+ - **Automated RTM & Security Taint Heatmap Generator (REQ-212)**: Self-contained interactive HTML/SVG report builder (`generateRtmHeatmap`) for spec chain coverage and security taint reachability.
16
+ - **CI/CD Governance Gate Runner (REQ-213)**: Non-interactive headless CI runner (`holmes-kit ci`) for GitHub Actions / GitLab CI with path traversal protection and gitignore filtering.
17
+
18
+ ### Documentation
19
+ - **Sherlock Holmes Vision & Philosophy**: Updated `README.md` to detail the Sherlock Holmes deduction, defect analysis, full SDLC coverage, enterprise tool harness vision, and supported AI coding tools matrix.
20
+
21
+ ## [0.1.4] - 2026-08-18
22
+
23
+ ### Documentation
24
+ - **Supported Languages & Environments Matrix**: Added comprehensive compatibility tables in `README.md` covering 7 AST/CPG supported languages (TypeScript/JS, Python, Go, Rust, Java, C/C++, C#) and tier-based OS environments (macOS, Linux, Windows/WSL2).
7
25
 
8
26
  ## [0.1.3] - 2026-08-18
9
27
 
package/README.md CHANGED
@@ -6,7 +6,32 @@
6
6
 
7
7
  > **"No Spec, No Code"** — Deterministic Agentic Software Engineering (ASE) harness with causal traceability.
8
8
 
9
- Holmes-Kit is a **deterministic control plane** for AI coding agents (**Claude Code**, **Antigravity CLI**, **Codex CLI**). It enforces architectural discipline at the hook layer so that every line of code is causally traceable back to approved specifications (`REQ → H-SPEC → A-SPEC → T-SPEC`).
9
+ ---
10
+
11
+ ## 🕵️ Philosophy & Vision: Beyond Code Generation
12
+
13
+ **Holmes-Kit** is not merely an AI code generator. Inspired by **Sherlock Holmes**, Holmes-Kit is built to **deduce, track down, and analyze code defects, structural drift, and security vulnerabilities** across the full Software Development Life Cycle (**SDLC**).
14
+
15
+ While most AI tools focus solely on generating code snippets, Holmes-Kit acts as a **harmonious, flexible governance harness** that bridges:
16
+ - 📋 **Requirements & Specification Management** (`REQ → H-SPEC → A-SPEC → T-SPEC`)
17
+ - 🐞 **Issue Tracking & Root Cause Localization** (Tree-sitter Code Property Graph + Recursive-CTE RTM)
18
+ - 🧪 **Empirical Verification & Self-Healing** (`holmes-kit doctor --fix`)
19
+ - 🔌 **Enterprise SDLC Tool Integration** (Harmonious integration with Jira, GitHub Issues, and external linters/SAST)
20
+
21
+ ---
22
+
23
+ ## 🤖 Supported AI Coding Tools & Environments (CLI-First Focus)
24
+
25
+ Holmes-Kit prioritizes **CLI-based AI Coding Agents** where OS-level process hooks, subshell isolation, and deterministic control plane enforcement are natively guaranteed:
26
+
27
+ | AI Coding Agent / Framework | Harness Tier | Integration & Enforcement Mechanisms |
28
+ | :--- | :--- | :--- |
29
+ | 🤖 **Claude Code CLI** | 🥇 Tier 1 (Native) | OS PreToolUse & Stop hooks (`.claude/settings.local.json`), MCP server (`.mcp.json`) |
30
+ | 🚀 **Antigravity CLI (AGY)** | 🥇 Tier 1 (Native) | AGY Hooks (`hooks.json`), MCP config (`.agents/mcp_config.json`), Governance Skills |
31
+ | 💻 **Codex CLI / Agentic Shell** | 🥇 Tier 1 (Native) | Codex MCP integration (`.codex/mcp_config.json`), Subshell Isolation Gates |
32
+ | 🧩 **Google Antigravity SDK** | 🥇 Tier 1 (Native) | Autonomous Agent SDK bindings and cryptographic provenance verification |
33
+
34
+ > **Note**: Holmes-Kit focuses strictly on CLI-based autonomous agents to guarantee 100% deterministic OS hook gating (`deny` enforcement) before file modifications occur.
10
35
 
11
36
  ---
12
37
 
@@ -88,6 +113,33 @@ If you prefer to configure MCP manually or integrate with other IDEs, add the fo
88
113
 
89
114
  ---
90
115
 
116
+ ## 🌐 Supported Languages & Environments
117
+
118
+ Holmes-Kit embeds native AST & Code Property Graph (D-CPG) analyzers to track causal relationships and anchor implementations across diverse technology stacks.
119
+
120
+ ### 💻 Supported Programming Languages
121
+ | Language | CPG Parser | Anchor Syntax | Dataflow Taint & RTM |
122
+ | :--- | :--- | :--- | :---: |
123
+ | **TypeScript / JavaScript** | Tree-sitter TypeScript/JS | `// @implements A-SPEC-XXX` | ✅ Full Support |
124
+ | **Python** | Tree-sitter Python | `# @implements A-SPEC-XXX` | ✅ Full Support |
125
+ | **Go** | Tree-sitter Go | `// @implements A-SPEC-XXX` | ✅ Full Support |
126
+ | **Rust** | Tree-sitter Rust | `// @implements A-SPEC-XXX` | ✅ Full Support |
127
+ | **Java** | Tree-sitter Java | `// @implements A-SPEC-XXX` | ✅ Full Support |
128
+ | **C / C++** | Tree-sitter C/C++ | `// @implements A-SPEC-XXX` | ✅ Full Support |
129
+ | **C# (.NET)** | Tree-sitter C# | `// @implements A-SPEC-XXX` | ✅ Full Support |
130
+
131
+ ### 🖥️ Supported Operating Systems & Runtimes
132
+ | OS / Platform | Architecture | Status | Notes |
133
+ | :--- | :--- | :---: | :--- |
134
+ | **macOS** | Apple Silicon (arm64) / Intel (x64) | ✅ Tier 1 | macOS 12+ (Full hook enforcement) |
135
+ | **Linux** | x86_64 / arm64 | ✅ Tier 1 | Ubuntu, Debian, Fedora, Arch, RHEL |
136
+ | **Windows (WSL2)** | x86_64 | ✅ Tier 1 | WSL2 Ubuntu/Debian recommended |
137
+ | **Windows Native** | x86_64 | ✅ Tier 2 | Windows 10/11 (Node.js 20+ with C++ build tools) |
138
+
139
+ > **Runtime Requirement**: Node.js `>= 20.0.0` (LTS recommended)
140
+
141
+ ---
142
+
91
143
  ## 🏛️ Key Capabilities
92
144
 
93
145
  - **Deterministic Hook Enforcement**: Gating is enforced via OS-level PreToolUse & Stop hooks, not easily bypassed prompts.
package/dist/.build-id CHANGED
@@ -1 +1 @@
1
- 09e6af7-msxtsdgq
1
+ c34e494-msyt6an1
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.runCiGate = runCiGate;
37
+ // @implements A-SPEC-213
38
+ const fs = __importStar(require("node:fs"));
39
+ const path = __importStar(require("node:path"));
40
+ const cpg_scanner_1 = require("../cpg/cpg-scanner");
41
+ const pre_tool_use_1 = require("../hooks/pre-tool-use");
42
+ const ignore_1 = require("../project/ignore");
43
+ /**
44
+ * Headless CI/CD Governance Gate Runner for GitHub Actions / GitLab CI.
45
+ *
46
+ * @implements A-SPEC-213
47
+ */
48
+ function runCiGate(targetDir, options) {
49
+ const root = path.resolve(targetDir);
50
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
51
+ return {
52
+ ok: false,
53
+ violations: [{ reason: `Target directory does not exist or is not a directory: ${root}` }],
54
+ summary: `[Holmes-Kit CI Gate Refusal] Target directory is invalid — ${root}`,
55
+ };
56
+ }
57
+ const specsDirName = options?.specsDir ?? (0, pre_tool_use_1.wiredSpecsDir)(process.argv, process.env);
58
+ const specsDir = path.resolve(root, specsDirName);
59
+ if (!(0, pre_tool_use_1.isGovernedProject)(specsDir, process.env)) {
60
+ return {
61
+ ok: true,
62
+ violations: [],
63
+ summary: `[Holmes-Kit CI Gate] Project is not governed (${specsDirName} absent) — skipping CI gate checks.`,
64
+ };
65
+ }
66
+ const ignore = (0, ignore_1.loadIgnore)(root);
67
+ const scanner = new cpg_scanner_1.CpgScanner();
68
+ const scannedFiles = scanner.scan(root, root);
69
+ const violations = [];
70
+ for (const sf of scannedFiles) {
71
+ const rel = path.relative(root, sf.path).split(path.sep).join('/');
72
+ if (ignore.isIgnored(rel))
73
+ continue;
74
+ // Check if scannable source file has no approved implements anchor
75
+ if (sf.implementsSpecs.length === 0) {
76
+ violations.push({
77
+ file: rel,
78
+ reason: `Missing line 1 // @implements anchor comment`,
79
+ });
80
+ }
81
+ if (sf.unanchoredImplements.length > 0) {
82
+ for (const wrongKind of sf.unanchoredImplements) {
83
+ violations.push({
84
+ file: rel,
85
+ specId: wrongKind,
86
+ reason: `Invalid anchor kind @implements ${wrongKind} — only A-SPEC anchors are valid code implementation anchors`,
87
+ });
88
+ }
89
+ }
90
+ }
91
+ if (violations.length > 0) {
92
+ return {
93
+ ok: false,
94
+ violations,
95
+ summary: `[Holmes-Kit CI Gate Refusal] Governance Violations Detected — ${violations.length} un-anchored file(s) or invalid spec anchors found in PR commit range.`,
96
+ };
97
+ }
98
+ return {
99
+ ok: true,
100
+ violations: [],
101
+ summary: `[Holmes-Kit CI Gate Success] 100% Governance Conformance — All scanned files carry valid approved spec anchors.`,
102
+ };
103
+ }
@@ -37,6 +37,7 @@ exports.STRIPPED_FOR_PROBE = void 0;
37
37
  exports.probeEnv = probeEnv;
38
38
  exports.runDoctor = runDoctor;
39
39
  exports.formatChecks = formatChecks;
40
+ // @implements A-SPEC-207
40
41
  // @implements A-SPEC-100.2
41
42
  const fs = __importStar(require("node:fs"));
42
43
  const path = __importStar(require("node:path"));
@@ -95,7 +96,7 @@ function wiredSettingsPath(target) {
95
96
  return (0, init_1.settingsPathOf)(target, 'project');
96
97
  return (0, init_1.settingsPathOf)(target, fs.existsSync((0, init_1.settingsPathOf)(target, 'local')) ? 'local' : 'project');
97
98
  }
98
- async function runDoctor(packageRoot, target, opts) {
99
+ async function runDoctor(packageRoot, target, opts, extraChecks) {
99
100
  const checks = [];
100
101
  const add = (name, level, detail, fix) => checks.push({ name, level, detail, fix });
101
102
  // 1. Build output — also the "prepare didn't run" detector.
@@ -451,6 +452,23 @@ async function runDoctor(packageRoot, target, opts) {
451
452
  }
452
453
  add('gate blind spots', 'PASS', (0, blind_spots_1.blindSpotSummary)());
453
454
  add('environment', 'PASS', present);
455
+ if (extraChecks) {
456
+ checks.push(...extraChecks);
457
+ }
458
+ if (opts?.autoFix) {
459
+ for (const c of checks) {
460
+ if ((c.level === 'FAIL' || c.level === 'WARN') && c.remediate) {
461
+ try {
462
+ const ok = c.remediate();
463
+ if (ok) {
464
+ c.level = 'PASS';
465
+ c.detail = `[REPAIRED] ${c.detail}`;
466
+ }
467
+ }
468
+ catch { /* remediation failed */ }
469
+ }
470
+ }
471
+ }
454
472
  return checks;
455
473
  }
456
474
  /**
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.packageRoot = packageRoot;
37
37
  exports.main = main;
38
38
  // @implements A-SPEC-100.2
39
+ // @implements A-SPEC-213
39
40
  const fs = __importStar(require("node:fs"));
40
41
  const path = __importStar(require("node:path"));
41
42
  const init_1 = require("./init");
@@ -69,6 +70,7 @@ const KNOWN_FLAGS = {
69
70
  init: ['help', 'target', 'mode', 'specs-dir', 'settings', 'matcher', 'dry-run', 'no-mcp', 'remove', 'force', 'agent'],
70
71
  doctor: ['help', 'target', 'json'],
71
72
  skills: ['help', 'target'],
73
+ ci: ['help', 'target', 'specs-dir', 'json'],
72
74
  };
73
75
  class UnknownFlagError extends Error {
74
76
  }
@@ -290,6 +292,39 @@ async function main(argv) {
290
292
  process.stderr.write(`Could not refresh recovery skill ${skillLabel(f.name)}: ${f.reason}\n`);
291
293
  return r.failed.length > 0 ? 1 : 0;
292
294
  }
295
+ // @implements A-SPEC-213
296
+ if (cmd === 'ci') {
297
+ if (typeof flags.target === 'string') {
298
+ const t = path.resolve(flags.target);
299
+ if (!fs.existsSync(t) || !fs.statSync(t).isDirectory()) {
300
+ process.stderr.write(`--target ${t} is not an existing directory\n\n${USAGE}`);
301
+ return 2;
302
+ }
303
+ }
304
+ const specsDir = typeof flags['specs-dir'] === 'string' ? flags['specs-dir'] : undefined;
305
+ if (specsDir !== undefined) {
306
+ if (path.isAbsolute(specsDir) || specsDir.split(/[\\/]/).includes('..') || specsDir.startsWith('~')
307
+ || /["'`$\\;&|<>\n\r]/.test(specsDir)) {
308
+ process.stderr.write(`--specs-dir ${specsDir} must be a plain relative path inside the target (no '..', no '~', no quotes or shell metacharacters)\n\n${USAGE}`);
309
+ return 2;
310
+ }
311
+ }
312
+ const target = typeof flags.target === 'string' ? flags.target : process.cwd();
313
+ const { runCiGate } = require('./ci-gate');
314
+ const res = runCiGate(target, { specsDir });
315
+ if (flags.json === true) {
316
+ process.stdout.write(JSON.stringify(res, null, 2) + '\n');
317
+ }
318
+ else {
319
+ process.stdout.write(res.summary + '\n');
320
+ for (const v of res.violations) {
321
+ const fileLabel = v.file ? ` [${v.file}]` : '';
322
+ const specLabel = v.specId ? ` (${v.specId})` : '';
323
+ process.stderr.write(` - ${fileLabel}${specLabel}: ${v.reason}\n`);
324
+ }
325
+ }
326
+ return res.ok ? 0 : 1;
327
+ }
293
328
  if (cmd === 'init') {
294
329
  const mode = (typeof flags.mode === 'string' ? flags.mode : 'guardrail');
295
330
  // 형제 열거 오류와 같은 모양(round 7): --settings·--target 은 USAGE 와 함께 exit 2 인데
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.CpgScanner = exports.isTestFile = exports.TEST_FILE_PATTERNS = exports.SCANNABLE_EXTENSIONS = exports.SKIP = void 0;
37
+ // @implements A-SPEC-208
37
38
  const fs = __importStar(require("node:fs"));
38
39
  const path = __importStar(require("node:path"));
39
40
  const language_parser_1 = require("./language-parser");
@@ -206,7 +207,8 @@ class CpgScanner {
206
207
  }
207
208
  };
208
209
  walk(rootDir);
209
- this.cache?.save(); // batched persist + mark-and-sweep prune, once per scan
210
+ const scopeRel = (0, source_path_1.toSourcePath)(repoRoot, rootDir);
211
+ this.cache?.save(scopeRel === '.' ? undefined : scopeRel); // batched persist + scoped mark-and-sweep prune
210
212
  return out;
211
213
  }
212
214
  }
@@ -34,10 +34,26 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ScanFileCache = exports.SCAN_CACHE_VERSION = void 0;
37
+ // @implements A-SPEC-208
37
38
  // @implements A-SPEC-113.1
38
39
  const fs = __importStar(require("node:fs"));
39
40
  const path = __importStar(require("node:path"));
40
41
  const node_crypto_1 = require("node:crypto");
42
+ function cloneExpr(e) {
43
+ if (!e)
44
+ return { text: '', refs: [], callees: [] };
45
+ return { text: e.text, refs: [...(e.refs ?? [])], callees: [...(e.callees ?? [])] };
46
+ }
47
+ function cloneDataFlow(df) {
48
+ if (!df)
49
+ return undefined;
50
+ return {
51
+ defs: (df.defs ?? []).map((d) => ({ ...d, expr: cloneExpr(d.expr) })),
52
+ calls: (df.calls ?? []).map((c) => ({ ...c, args: (c.args ?? []).map(cloneExpr) })),
53
+ params: (df.params ?? []).map((p) => ({ ...p })),
54
+ returns: (df.returns ?? []).map((r) => ({ ...r, expr: cloneExpr(r.expr) })),
55
+ };
56
+ }
41
57
  // Bump on ANY change to symbol extraction, grammar versions, or the ScannedFile schema (review D3:
42
58
  // without a version stamp, cached entries kept serving OLD-parser symbols indefinitely after an
43
59
  // upgrade — content hashes match, so nothing else invalidates them). A mismatch drops the whole
@@ -75,7 +91,14 @@ class ScanFileCache {
75
91
  this.hits++;
76
92
  // Default array fields so an entry from a slightly older writer can never yield `undefined`
77
93
  // where a fresh scan yields [] (review D3b — downstream iterates these).
78
- return { ...e.file, symbols: e.file.symbols ?? [], implementsSpecs: e.file.implementsSpecs ?? [], unanchoredImplements: e.file.unanchoredImplements ?? [] };
94
+ return {
95
+ ...e.file,
96
+ symbols: (e.file.symbols ?? []).map((s) => ({ ...s })),
97
+ implementsSpecs: [...(e.file.implementsSpecs ?? [])],
98
+ unanchoredImplements: [...(e.file.unanchoredImplements ?? [])],
99
+ edges: e.file.edges ? e.file.edges.map((edge) => ({ ...edge })) : undefined,
100
+ dataflow: cloneDataFlow(e.file.dataflow),
101
+ };
79
102
  }
80
103
  this.misses++;
81
104
  return undefined;
@@ -89,20 +112,29 @@ class ScanFileCache {
89
112
  * Persist once per scan. Mark-and-sweep: entries whose path was NOT seen this scan belong to
90
113
  * deleted/renamed files — pruned so the cache cannot grow unboundedly. No-op when nothing changed.
91
114
  */
92
- save() {
93
- const stale = Object.keys(this.entries).filter((p) => !this.seen.has(p));
115
+ save(scannedScopeDir) {
116
+ const scopePrefix = scannedScopeDir ? (scannedScopeDir.endsWith('/') ? scannedScopeDir : scannedScopeDir + '/') : undefined;
117
+ const stale = Object.keys(this.entries).filter((p) => {
118
+ if (this.seen.has(p))
119
+ return false;
120
+ // If a scoped directory was scanned, only prune stale entries within that directory
121
+ if (scopePrefix && p !== scannedScopeDir && !p.startsWith(scopePrefix))
122
+ return false;
123
+ return true;
124
+ });
94
125
  for (const p of stale) {
95
126
  delete this.entries[p];
96
127
  this.dirty = true;
97
128
  }
98
- if (!this.dirty)
99
- return;
100
129
  try {
101
- fs.mkdirSync(this.dir, { recursive: true });
102
- fs.writeFileSync(this.file, JSON.stringify({ v: exports.SCAN_CACHE_VERSION, entries: this.entries }), 'utf8');
103
- this.dirty = false;
130
+ if (this.dirty) {
131
+ fs.mkdirSync(this.dir, { recursive: true });
132
+ fs.writeFileSync(this.file, JSON.stringify({ v: exports.SCAN_CACHE_VERSION, entries: this.entries }), 'utf8');
133
+ this.dirty = false;
134
+ }
104
135
  }
105
136
  catch { /* cache persistence is best-effort — next scan just runs colder */ }
137
+ this.seen.clear();
106
138
  }
107
139
  }
108
140
  exports.ScanFileCache = ScanFileCache;
@@ -51,6 +51,7 @@ const test_evidence_1 = require("../review/test-evidence");
51
51
  const pre_tool_use_1 = require("./pre-tool-use");
52
52
  const governance_history_1 = require("../guardrail/governance-history");
53
53
  const constitution_debt_1 = require("../governance/constitution-debt");
54
+ const root_1 = require("../project/root");
54
55
  /**
55
56
  * @implements A-SPEC-175
56
57
  * The turn-boundary answer to "this project is governed but its specs are gone".
@@ -339,7 +340,7 @@ if (require.main === module) {
339
340
  // stale record can never vouch for changed code. Absent/stale -> syntactic fallback.
340
341
  let executedByAspec;
341
342
  try {
342
- const head = (0, node_child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd: stopProjectRoot(), stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
343
+ const head = (0, node_child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd: stopProjectRoot(), stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)() }).toString().trim();
343
344
  const ev = (0, test_evidence_1.readTestEvidence)(stopProjectRoot());
344
345
  if ((0, test_evidence_1.isFresh)(ev, head))
345
346
  executedByAspec = ev.executedByAspec;
@@ -53,6 +53,7 @@ const spec_types_1 = require("../spec/spec-types");
53
53
  const legacy_format_1 = require("../spec/legacy-format");
54
54
  const cpg_scanner_1 = require("../cpg/cpg-scanner");
55
55
  const scan_cache_1 = require("../cpg/scan-cache");
56
+ const root_1 = require("../project/root");
56
57
  // N7: every whole-tree scan goes through the file-key incremental cache under the PROJECT root's
57
58
  // .ax/cpg_cache — unchanged files (content-hash hit) skip the tree-sitter parse entirely.
58
59
  // The cache lives under the RESOLVED project root, not the raw scan root: scanning a SUBDIRECTORY
@@ -65,7 +66,7 @@ const scan_cache_1 = require("../cpg/scan-cache");
65
66
  // left open on the scan path. When the store is bound, the cache is the bound project's; otherwise
66
67
  // only an anchored answer may be written to, and an unanchored one falls back to a temp cache.
67
68
  const cacheDirFor = (root) => {
68
- const r = (0, root_1.resolveProjectRoot)(root);
69
+ const r = (0, root_2.resolveProjectRoot)(root);
69
70
  if (r.marker !== 'given')
70
71
  return path.join(r.root, '.ax', 'cpg_cache');
71
72
  if (fs.existsSync(path.join(r.root, '.ax')))
@@ -126,7 +127,7 @@ function isHandlerRefusal(e) {
126
127
  return !!e && typeof e === 'object' && e.holmesRefusal === true;
127
128
  }
128
129
  // @implements A-SPEC-128
129
- const root_1 = require("../project/root");
130
+ const root_2 = require("../project/root");
130
131
  const change_source_1 = require("../project/change-source");
131
132
  const baseline_1 = require("../project/baseline");
132
133
  const ignore_1 = require("../project/ignore");
@@ -146,7 +147,7 @@ const anchor_1 = require("../reverse/anchor");
146
147
  // yields top-level-relative paths while scan(root, root) tags root-relative ones, so a disagreement
147
148
  // silently produces wrong impact sets. `GitChangeSource` now enforces exactly that, and only there.
148
149
  function projectRootOf(root) {
149
- return (0, root_1.resolveProjectRoot)(root).root;
150
+ return (0, root_2.resolveProjectRoot)(root).root;
150
151
  }
151
152
  // @implements A-SPEC-191 §10 — WHERE a single-use approval is spent must not be a caller's choice.
152
153
  // r8 measured all three escapes at once: a second `root`, a `root` naming a subdirectory, and an
@@ -1190,7 +1191,7 @@ function makeRawHandlers(store) {
1190
1191
  const executedByAspec = (0, test_evidence_1.computeExecutedByAspec)(result.executedByFile ?? {}, anchors);
1191
1192
  let head = '';
1192
1193
  try {
1193
- head = (0, node_child_process_1.execFileSync)('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
1194
+ head = (0, node_child_process_1.execFileSync)('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8', env: (0, root_1.cleanSubprocessEnv)() }).trim();
1194
1195
  }
1195
1196
  catch { /* non-git */ }
1196
1197
  // Record ONLY an actually-executed, GREEN run (review C4/C7): a red suite must not stand as
@@ -1828,7 +1829,7 @@ function makeRawHandlers(store) {
1828
1829
  return { ...plan, ...applied, blockers };
1829
1830
  },
1830
1831
  async spec_slice_init(a) {
1831
- const root = a.root ? (0, root_1.resolveProjectRoot)(a.root).root : process.cwd();
1832
+ const root = a.root ? (0, root_2.resolveProjectRoot)(a.root).root : process.cwd();
1832
1833
  const specsDir = path.join(root, '.ax', 'specs');
1833
1834
  const existing = await store.list();
1834
1835
  let maxId = 200;
@@ -1990,7 +1991,7 @@ depends_on:
1990
1991
  return { ok: true, specsCreated: [reqId, hspecId, aspecId, tspecId] };
1991
1992
  },
1992
1993
  async spec_slice_approve(a) {
1993
- const root = a.root ? (0, root_1.resolveProjectRoot)(a.root).root : process.cwd();
1994
+ const root = a.root ? (0, root_2.resolveProjectRoot)(a.root).root : process.cwd();
1994
1995
  const rawStore = new spec_store_1.LocalMarkdownRepository(path.join(root, '.ax', 'specs'));
1995
1996
  const specs = await rawStore.list();
1996
1997
  const inSlice = specs.filter((s) => {
@@ -2030,7 +2031,7 @@ depends_on:
2030
2031
  return { ok: true, approvedSpecs };
2031
2032
  },
2032
2033
  async spec_remediate(a) {
2033
- const root = a.root ? (0, root_1.resolveProjectRoot)(a.root).root : process.cwd();
2034
+ const root = a.root ? (0, root_2.resolveProjectRoot)(a.root).root : process.cwd();
2034
2035
  const actionsTaken = [];
2035
2036
  if (a.targetFile && a.aspecId) {
2036
2037
  const fullPath = path.isAbsolute(a.targetFile) ? a.targetFile : path.join(root, a.targetFile);
@@ -36,15 +36,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.MARKER = void 0;
37
37
  exports.cleanSubprocessEnv = cleanSubprocessEnv;
38
38
  exports.resolveProjectRoot = resolveProjectRoot;
39
+ // @implements A-SPEC-205
39
40
  // @implements A-SPEC-128
40
41
  const fs = __importStar(require("node:fs"));
41
42
  const path = __importStar(require("node:path"));
42
43
  const node_child_process_1 = require("node:child_process");
43
44
  // @implements A-SPEC-194
44
45
  // @implements A-SPEC-196
46
+ // @implements A-SPEC-205
45
47
  function cleanSubprocessEnv(env = process.env) {
46
48
  const cleaned = { ...env };
47
- const SCRUB_KEYS = new Set(['HOLMES_APPROVAL', 'HOLMES_LEDGER_KEY']);
49
+ const SCRUB_KEYS = new Set(['HOLMES_APPROVAL', 'HOLMES_LEDGER_KEY', 'HOLMES_ROLE', 'HOLMES_ENV_CHANNEL']);
48
50
  for (const k of Object.keys(cleaned)) {
49
51
  if (SCRUB_KEYS.has(k.toUpperCase())) {
50
52
  delete cleaned[k];
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateRtmHeatmap = generateRtmHeatmap;
4
+ /**
5
+ * Generate a standalone interactive HTML/SVG Heatmap Report for RTM Traceability & Taint Coverage.
6
+ *
7
+ * @implements A-SPEC-212
8
+ */
9
+ function generateRtmHeatmap(specs, options) {
10
+ const safeSpecs = Array.isArray(specs) ? specs : [];
11
+ const totalSpecs = safeSpecs.length;
12
+ const coveredSpecs = safeSpecs.filter((s) => s && s.covered === true).length;
13
+ const coveragePct = totalSpecs > 0 ? Math.round((coveredSpecs / totalSpecs) * 100) : 0;
14
+ const title = options?.title ?? 'Holmes-Kit RTM & Security Taint Heatmap';
15
+ const isDark = options?.theme !== 'light';
16
+ const bgColor = isDark ? '#0f172a' : '#f8fafc';
17
+ const cardBg = isDark ? '#1e293b' : '#ffffff';
18
+ const textColor = isDark ? '#f8fafc' : '#0f172a';
19
+ const coveredBg = isDark ? '#065f46' : '#d1fae5';
20
+ const coveredText = isDark ? '#34d399' : '#065f46';
21
+ const uncoveredBg = isDark ? '#991b1b' : '#fee2e2';
22
+ const uncoveredText = isDark ? '#f87171' : '#991b1b';
23
+ const cellsHtml = safeSpecs
24
+ .map((s) => {
25
+ const isCovered = s && s.covered === true;
26
+ const bg = isCovered ? coveredBg : uncoveredBg;
27
+ const fg = isCovered ? coveredText : uncoveredText;
28
+ const cls = isCovered ? 'badge-covered' : 'badge-uncovered';
29
+ const statusLabel = isCovered ? 'COVERED' : 'UNCOVERED';
30
+ return `
31
+ <div class="card ${cls}" style="background-color: ${bg}; color: ${fg}; padding: 12px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1);">
32
+ <div style="font-weight: bold; font-size: 14px;">${escapeHtml(s?.id)}</div>
33
+ <div style="font-size: 12px; margin-top: 4px; opacity: 0.9;">${escapeHtml(s?.title)}</div>
34
+ <span style="display: inline-block; margin-top: 8px; font-size: 10px; font-weight: 800; text-transform: uppercase;">${statusLabel}</span>
35
+ </div>`;
36
+ })
37
+ .join('\n');
38
+ const html = `<!DOCTYPE html>
39
+ <html lang="en">
40
+ <head>
41
+ <meta charset="UTF-8">
42
+ <title>${escapeHtml(title)}</title>
43
+ <style>
44
+ body { font-family: system-ui, -apple-system, sans-serif; background-color: ${bgColor}; color: ${textColor}; margin: 0; padding: 24px; }
45
+ .header { margin-bottom: 24px; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 16px; }
46
+ .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
47
+ .stat-badge { font-size: 32px; font-weight: 800; color: ${coveragePct >= 80 ? '#10b981' : coveragePct >= 50 ? '#f59e0b' : '#ef4444'}; }
48
+ </style>
49
+ </head>
50
+ <body>
51
+ <div class="header">
52
+ <h1>🔍 ${escapeHtml(title)}</h1>
53
+ <div>Specification Coverage: <span class="stat-badge">${coveragePct}%</span> (${coveredSpecs} / ${totalSpecs} Specs Covered)</div>
54
+ </div>
55
+ <div class="grid">
56
+ ${cellsHtml}
57
+ </div>
58
+ </body>
59
+ </html>`;
60
+ return { html, coveragePct, totalSpecs, coveredSpecs };
61
+ }
62
+ function escapeHtml(val) {
63
+ return String(val ?? '')
64
+ .replace(/&/g, '&amp;')
65
+ .replace(/</g, '&lt;')
66
+ .replace(/>/g, '&gt;')
67
+ .replace(/"/g, '&quot;')
68
+ .replace(/'/g, '&#039;');
69
+ }
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
+ "//": "@implements A-SPEC-209",
2
3
  "name": "@holmes-lab/holmes-kit",
3
- "version": "0.1.3",
4
+ "version": "0.1.5",
4
5
  "description": "Holmes-Kit — deterministic Agentic Software Engineering (ASE) harness with causal traceability (spec chain + D-CPG + RTM + phase guardrail)",
5
6
  "main": "dist/holmes/mcp/server.js",
6
7
  "bin": {