sentinelayer-cli 0.8.0 → 0.8.2
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/README.md +23 -2
- package/package.json +4 -4
- package/src/agents/ai-governance/index.js +12 -0
- package/src/agents/ai-governance/tools/base.js +171 -0
- package/src/agents/ai-governance/tools/eval-regression.js +47 -0
- package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
- package/src/agents/ai-governance/tools/index.js +52 -0
- package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
- package/src/agents/ai-governance/tools/provenance-check.js +69 -0
- package/src/agents/backend/index.js +12 -0
- package/src/agents/backend/tools/base.js +189 -0
- package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
- package/src/agents/backend/tools/idempotency-audit.js +105 -0
- package/src/agents/backend/tools/index.js +87 -0
- package/src/agents/backend/tools/retry-audit.js +132 -0
- package/src/agents/backend/tools/timeout-audit.js +144 -0
- package/src/agents/code-quality/index.js +12 -0
- package/src/agents/code-quality/tools/base.js +159 -0
- package/src/agents/code-quality/tools/complexity-measure.js +197 -0
- package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
- package/src/agents/code-quality/tools/cycle-detect.js +49 -0
- package/src/agents/code-quality/tools/dep-graph.js +196 -0
- package/src/agents/code-quality/tools/index.js +89 -0
- package/src/agents/data-layer/index.js +12 -0
- package/src/agents/data-layer/tools/base.js +181 -0
- package/src/agents/data-layer/tools/index-audit.js +165 -0
- package/src/agents/data-layer/tools/index.js +83 -0
- package/src/agents/data-layer/tools/migration-scan.js +135 -0
- package/src/agents/data-layer/tools/query-explain.js +120 -0
- package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
- package/src/agents/documentation/index.js +12 -0
- package/src/agents/documentation/tools/api-diff.js +91 -0
- package/src/agents/documentation/tools/base.js +151 -0
- package/src/agents/documentation/tools/dead-link-check.js +58 -0
- package/src/agents/documentation/tools/docstring-coverage.js +78 -0
- package/src/agents/documentation/tools/index.js +52 -0
- package/src/agents/documentation/tools/readme-freshness.js +61 -0
- package/src/agents/envelope/fix-cycle.js +45 -0
- package/src/agents/envelope/index.js +31 -0
- package/src/agents/envelope/loop.js +150 -0
- package/src/agents/envelope/pulse.js +18 -0
- package/src/agents/envelope/stream.js +40 -0
- package/src/agents/infrastructure/index.js +12 -0
- package/src/agents/infrastructure/tools/base.js +171 -0
- package/src/agents/infrastructure/tools/checkov-run.js +32 -0
- package/src/agents/infrastructure/tools/drift-detect.js +59 -0
- package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
- package/src/agents/infrastructure/tools/index.js +52 -0
- package/src/agents/infrastructure/tools/tflint-run.js +31 -0
- package/src/agents/jules/loop.js +7 -4
- package/src/agents/jules/swarm/sub-agent.js +5 -1
- package/src/agents/jules/tools/auth-audit.js +10 -1
- package/src/agents/mode.js +113 -0
- package/src/agents/observability/index.js +12 -0
- package/src/agents/observability/tools/alert-audit.js +39 -0
- package/src/agents/observability/tools/base.js +181 -0
- package/src/agents/observability/tools/dashboard-gap.js +42 -0
- package/src/agents/observability/tools/index.js +54 -0
- package/src/agents/observability/tools/log-schema-check.js +74 -0
- package/src/agents/observability/tools/span-coverage.js +74 -0
- package/src/agents/persona-visuals.js +38 -0
- package/src/agents/release/index.js +12 -0
- package/src/agents/release/tools/base.js +181 -0
- package/src/agents/release/tools/changelog-diff.js +86 -0
- package/src/agents/release/tools/feature-flag-audit.js +126 -0
- package/src/agents/release/tools/index.js +61 -0
- package/src/agents/release/tools/rollback-verify.js +129 -0
- package/src/agents/release/tools/semver-check.js +109 -0
- package/src/agents/reliability/index.js +12 -0
- package/src/agents/reliability/tools/backpressure-check.js +129 -0
- package/src/agents/reliability/tools/base.js +181 -0
- package/src/agents/reliability/tools/chaos-probe.js +109 -0
- package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
- package/src/agents/reliability/tools/health-check-audit.js +111 -0
- package/src/agents/reliability/tools/index.js +87 -0
- package/src/agents/run-persona.js +109 -0
- package/src/agents/security/index.js +12 -0
- package/src/agents/security/tools/authz-audit.js +134 -0
- package/src/agents/security/tools/base.js +190 -0
- package/src/agents/security/tools/crypto-review.js +175 -0
- package/src/agents/security/tools/index.js +97 -0
- package/src/agents/security/tools/sast-scan.js +175 -0
- package/src/agents/security/tools/secrets-scan.js +216 -0
- package/src/agents/supply-chain/index.js +12 -0
- package/src/agents/supply-chain/tools/attestation-check.js +42 -0
- package/src/agents/supply-chain/tools/base.js +151 -0
- package/src/agents/supply-chain/tools/index.js +52 -0
- package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
- package/src/agents/supply-chain/tools/package-verify.js +56 -0
- package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
- package/src/agents/testing/index.js +12 -0
- package/src/agents/testing/tools/base.js +202 -0
- package/src/agents/testing/tools/coverage-gap.js +144 -0
- package/src/agents/testing/tools/flake-detect.js +125 -0
- package/src/agents/testing/tools/index.js +85 -0
- package/src/agents/testing/tools/mutation-test.js +143 -0
- package/src/agents/testing/tools/snapshot-diff.js +103 -0
- package/src/auth/gate.js +65 -37
- package/src/cli.js +1 -1
- package/src/commands/chat.js +3 -10
- package/src/commands/legacy-args.js +10 -0
- package/src/commands/omargate.js +36 -2
- package/src/commands/persona.js +46 -1
- package/src/commands/scan.js +3 -10
- package/src/commands/session.js +654 -6
- package/src/commands/spec.js +3 -10
- package/src/coord/events-log.js +141 -0
- package/src/coord/handshake.js +719 -0
- package/src/coord/index.js +35 -0
- package/src/coord/paths.js +84 -0
- package/src/coord/priority.js +62 -0
- package/src/coord/tarjan.js +157 -0
- package/src/cost/tokenizer.js +160 -0
- package/src/cost/tracker.js +61 -0
- package/src/daemon/artifact-lineage.js +362 -0
- package/src/daemon/assignment-ledger.js +117 -0
- package/src/daemon/ast-drift.js +496 -0
- package/src/daemon/ingest-refresh.js +69 -2
- package/src/ingest/engine.js +15 -0
- package/src/ingest/ownership.js +380 -0
- package/src/legacy-cli.js +68 -1
- package/src/orchestrator/kai-chen.js +126 -0
- package/src/review/ai-review.js +3 -10
- package/src/review/compliance-pack.js +389 -0
- package/src/review/investor-dd-config.js +54 -0
- package/src/review/investor-dd-file-loop.js +303 -0
- package/src/review/investor-dd-file-router.js +406 -0
- package/src/review/investor-dd-html-report.js +233 -0
- package/src/review/investor-dd-notification.js +120 -0
- package/src/review/investor-dd-orchestrator.js +405 -0
- package/src/review/investor-dd-persona-runner.js +275 -0
- package/src/review/live-validator.js +253 -0
- package/src/review/omargate-orchestrator.js +90 -2
- package/src/review/persona-prompts.js +244 -56
- package/src/review/reconciliation-rules.js +329 -0
- package/src/review/reproducibility-chain.js +136 -0
- package/src/review/scan-modes.js +102 -3
- package/src/session/agent-registry.js +7 -0
- package/src/session/analytics.js +479 -0
- package/src/session/daemon.js +609 -14
- package/src/session/file-locks.js +666 -0
- package/src/session/paths.js +4 -0
- package/src/session/recap.js +567 -0
- package/src/session/redact.js +82 -0
- package/src/session/runtime-bridge.js +24 -1
- package/src/session/scoring.js +406 -0
- package/src/session/setup-guides.js +304 -0
- package/src/session/store.js +318 -2
- package/src/session/stream.js +9 -1
- package/src/session/sync.js +753 -0
- package/src/session/tasks.js +1054 -0
- package/src/session/templates.js +188 -0
- package/src/swarm/runtime.js +1 -8
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// alert-audit — check that alert definitions exist (#A20).
|
|
2
|
+
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { createFinding, toPosix, walkRepoFiles } from "./base.js";
|
|
7
|
+
|
|
8
|
+
const ALERT_SIGNATURES = [
|
|
9
|
+
/(^|\/)alerts?\/[^/]+\.(ya?ml|json|tf)$/i,
|
|
10
|
+
/(^|\/)prometheus\/rules?\//i,
|
|
11
|
+
/(^|\/)alertmanager\//i,
|
|
12
|
+
/(^|\/)monitors?\/[^/]+\.(ya?ml|json|tf)$/i,
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export async function runAlertAudit({ rootPath } = {}) {
|
|
16
|
+
const resolvedRoot = path.resolve(String(rootPath || "."));
|
|
17
|
+
let found = false;
|
|
18
|
+
for await (const { relativePath } of walkRepoFiles({ rootPath: resolvedRoot })) {
|
|
19
|
+
const rel = toPosix(relativePath);
|
|
20
|
+
if (ALERT_SIGNATURES.some((p) => p.test(rel))) {
|
|
21
|
+
found = true;
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (found) return [];
|
|
26
|
+
return [
|
|
27
|
+
createFinding({
|
|
28
|
+
tool: "alert-audit",
|
|
29
|
+
kind: "observability.no-alerts",
|
|
30
|
+
severity: "P2",
|
|
31
|
+
file: "",
|
|
32
|
+
line: 0,
|
|
33
|
+
evidence: "No alert definitions found under alerts/, monitors/, prometheus/rules/, or alertmanager/",
|
|
34
|
+
rootCause: "Without declarative alert definitions we can't tell what production failures the team is actually notified of.",
|
|
35
|
+
recommendedFix: "Define alerts in code (Prometheus rules, Datadog Terraform monitors, SLO burn-rate alerts). Require every critical endpoint to have an error-rate + latency alert.",
|
|
36
|
+
confidence: 0.6,
|
|
37
|
+
}),
|
|
38
|
+
];
|
|
39
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Shared helpers for Sofia's (observability) domain tools (#A20).
|
|
2
|
+
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
|
|
7
|
+
import ignore from "ignore";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_IGNORED_DIRS = new Set([
|
|
10
|
+
".git",
|
|
11
|
+
"node_modules",
|
|
12
|
+
".venv",
|
|
13
|
+
".next",
|
|
14
|
+
"dist",
|
|
15
|
+
"build",
|
|
16
|
+
"coverage",
|
|
17
|
+
".sentinelayer",
|
|
18
|
+
".sentinel",
|
|
19
|
+
".turbo",
|
|
20
|
+
".idea",
|
|
21
|
+
".vscode",
|
|
22
|
+
"__pycache__",
|
|
23
|
+
".cache",
|
|
24
|
+
]);
|
|
25
|
+
const MAX_FILE_SIZE_BYTES = 1024 * 1024;
|
|
26
|
+
const SEVERITIES = Object.freeze(["P0", "P1", "P2", "P3"]);
|
|
27
|
+
|
|
28
|
+
export function toPosix(value) {
|
|
29
|
+
return String(value || "").replace(/\\/g, "/");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function normalizeSeverity(value) {
|
|
33
|
+
const normalized = String(value || "").trim().toUpperCase();
|
|
34
|
+
if (SEVERITIES.includes(normalized)) {
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
return "P2";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createFinding({
|
|
41
|
+
severity,
|
|
42
|
+
kind,
|
|
43
|
+
file,
|
|
44
|
+
line = 0,
|
|
45
|
+
evidence = "",
|
|
46
|
+
rootCause = "",
|
|
47
|
+
recommendedFix = "",
|
|
48
|
+
confidence = null,
|
|
49
|
+
tool = "",
|
|
50
|
+
persona = "observability",
|
|
51
|
+
} = {}) {
|
|
52
|
+
return {
|
|
53
|
+
persona,
|
|
54
|
+
tool: String(tool || "").trim(),
|
|
55
|
+
kind: String(kind || "").trim() || "observability",
|
|
56
|
+
severity: normalizeSeverity(severity),
|
|
57
|
+
file: toPosix(file || ""),
|
|
58
|
+
line: Number.isFinite(Number(line)) ? Math.max(0, Math.floor(Number(line))) : 0,
|
|
59
|
+
evidence: String(evidence || "").trim().slice(0, 400),
|
|
60
|
+
rootCause: String(rootCause || "").trim(),
|
|
61
|
+
recommendedFix: String(recommendedFix || "").trim(),
|
|
62
|
+
confidence:
|
|
63
|
+
confidence === null || confidence === undefined
|
|
64
|
+
? null
|
|
65
|
+
: Math.max(0, Math.min(1, Number(confidence) || 0)),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function readIgnorePatterns(filePath) {
|
|
70
|
+
try {
|
|
71
|
+
const raw = await fsp.readFile(filePath, "utf-8");
|
|
72
|
+
return String(raw || "")
|
|
73
|
+
.split(/\r?\n/)
|
|
74
|
+
.map((line) => line.trim())
|
|
75
|
+
.filter((line) => line && !line.startsWith("#"));
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err && typeof err === "object" && err.code === "ENOENT") {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function createIgnoreMatcher(rootPath) {
|
|
85
|
+
const matcher = ignore();
|
|
86
|
+
const gitignore = await readIgnorePatterns(path.join(rootPath, ".gitignore"));
|
|
87
|
+
const sentinel = await readIgnorePatterns(
|
|
88
|
+
path.join(rootPath, ".sentinelayerignore")
|
|
89
|
+
);
|
|
90
|
+
matcher.add([...gitignore, ...sentinel]);
|
|
91
|
+
return (relativePath, isDirectory) => {
|
|
92
|
+
const normalized = toPosix(relativePath);
|
|
93
|
+
if (!normalized) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const candidate = isDirectory ? `${normalized}/` : normalized;
|
|
97
|
+
return matcher.ignores(candidate);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function* walkRepoFiles({
|
|
102
|
+
rootPath = process.cwd(),
|
|
103
|
+
extensions = new Set(),
|
|
104
|
+
maxFileSize = MAX_FILE_SIZE_BYTES,
|
|
105
|
+
} = {}) {
|
|
106
|
+
const resolvedRoot = path.resolve(rootPath);
|
|
107
|
+
const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
|
|
108
|
+
const wantedExtensions =
|
|
109
|
+
extensions instanceof Set
|
|
110
|
+
? extensions
|
|
111
|
+
: new Set(Array.isArray(extensions) ? extensions : []);
|
|
112
|
+
const stack = [resolvedRoot];
|
|
113
|
+
while (stack.length > 0) {
|
|
114
|
+
const current = stack.pop();
|
|
115
|
+
let entries = [];
|
|
116
|
+
try {
|
|
117
|
+
entries = await fsp.readdir(current, { withFileTypes: true });
|
|
118
|
+
} catch {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
const fullPath = path.join(current, entry.name);
|
|
123
|
+
const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
|
|
124
|
+
if (entry.isDirectory()) {
|
|
125
|
+
if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (ignoreMatcher(relativePath, true)) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
stack.push(fullPath);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!entry.isFile()) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (ignoreMatcher(relativePath, false)) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
141
|
+
if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
let stat = null;
|
|
145
|
+
try {
|
|
146
|
+
stat = await fsp.stat(fullPath);
|
|
147
|
+
} catch {
|
|
148
|
+
stat = null;
|
|
149
|
+
}
|
|
150
|
+
if (!stat || stat.size > maxFileSize) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
yield { fullPath, relativePath };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function findLineMatches(content, pattern) {
|
|
159
|
+
const text = String(content || "");
|
|
160
|
+
if (!pattern) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
const global = new RegExp(
|
|
164
|
+
pattern.source,
|
|
165
|
+
pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`
|
|
166
|
+
);
|
|
167
|
+
const matches = [];
|
|
168
|
+
let match;
|
|
169
|
+
while ((match = global.exec(text)) !== null) {
|
|
170
|
+
const lineIndex = text.slice(0, match.index).split(/\r?\n/).length;
|
|
171
|
+
matches.push({ index: match.index, line: lineIndex, match: match[0] });
|
|
172
|
+
}
|
|
173
|
+
return matches;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function getLineContent(content, line) {
|
|
177
|
+
const lines = String(content || "").split(/\r?\n/);
|
|
178
|
+
return (lines[Math.max(0, (Number(line) || 1) - 1)] || "").trim();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export { DEFAULT_IGNORED_DIRS, MAX_FILE_SIZE_BYTES, SEVERITIES };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// dashboard-gap — check that a dashboard config exists somewhere (#A20).
|
|
2
|
+
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { createFinding, toPosix, walkRepoFiles } from "./base.js";
|
|
7
|
+
|
|
8
|
+
const MANIFEST_SIGNATURES = [
|
|
9
|
+
/(^|\/)dashboards?\//,
|
|
10
|
+
/(^|\/)grafana\//,
|
|
11
|
+
/(^|\/)datadog\//,
|
|
12
|
+
/(^|\/)observability\//,
|
|
13
|
+
];
|
|
14
|
+
const DASHBOARD_FILES = /\.json$|\.yaml$|\.yml$/i;
|
|
15
|
+
|
|
16
|
+
export async function runDashboardGap({ rootPath } = {}) {
|
|
17
|
+
const resolvedRoot = path.resolve(String(rootPath || "."));
|
|
18
|
+
let found = false;
|
|
19
|
+
for await (const { relativePath } of walkRepoFiles({ rootPath: resolvedRoot })) {
|
|
20
|
+
const rel = toPosix(relativePath);
|
|
21
|
+
if (MANIFEST_SIGNATURES.some((p) => p.test(rel)) && DASHBOARD_FILES.test(rel)) {
|
|
22
|
+
found = true;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (found) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
return [
|
|
30
|
+
createFinding({
|
|
31
|
+
tool: "dashboard-gap",
|
|
32
|
+
kind: "observability.no-dashboard",
|
|
33
|
+
severity: "P3",
|
|
34
|
+
file: "",
|
|
35
|
+
line: 0,
|
|
36
|
+
evidence: "No dashboards/, grafana/, datadog/, or observability/ directory with JSON / YAML configs",
|
|
37
|
+
rootCause: "No dashboard config checked into the repo. Ad-hoc dashboards created in the UI are invisible to code review and drift over time.",
|
|
38
|
+
recommendedFix: "Check a machine-readable dashboard source (Grafana JSON, Datadog Terraform, OpenMetrics) into the repo. Generate the deployed dashboard from source to keep them in sync.",
|
|
39
|
+
confidence: 0.55,
|
|
40
|
+
}),
|
|
41
|
+
];
|
|
42
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Sofia (observability persona) domain-tool registry (#A20).
|
|
2
|
+
|
|
3
|
+
import { runAlertAudit } from "./alert-audit.js";
|
|
4
|
+
import { runDashboardGap } from "./dashboard-gap.js";
|
|
5
|
+
import { runLogSchemaCheck } from "./log-schema-check.js";
|
|
6
|
+
import { runSpanCoverage } from "./span-coverage.js";
|
|
7
|
+
|
|
8
|
+
export const OBSERVABILITY_TOOLS = Object.freeze({
|
|
9
|
+
"span-coverage": {
|
|
10
|
+
id: "span-coverage",
|
|
11
|
+
description: "Flag route handlers that have no OpenTelemetry / Sentry / Datadog tracing span in scope.",
|
|
12
|
+
schema: { type: "object", properties: { rootPath: { type: "string" }, files: { type: "array", items: { type: "string" } } } },
|
|
13
|
+
handler: runSpanCoverage,
|
|
14
|
+
},
|
|
15
|
+
"dashboard-gap": {
|
|
16
|
+
id: "dashboard-gap",
|
|
17
|
+
description: "Report if no dashboard configuration (Grafana JSON, Datadog TF, observability dir) is checked into the repo.",
|
|
18
|
+
schema: { type: "object", properties: { rootPath: { type: "string" } } },
|
|
19
|
+
handler: runDashboardGap,
|
|
20
|
+
},
|
|
21
|
+
"alert-audit": {
|
|
22
|
+
id: "alert-audit",
|
|
23
|
+
description: "Report if no declarative alert definitions (Prometheus rules, Datadog monitors, alertmanager) are checked in.",
|
|
24
|
+
schema: { type: "object", properties: { rootPath: { type: "string" } } },
|
|
25
|
+
handler: runAlertAudit,
|
|
26
|
+
},
|
|
27
|
+
"log-schema-check": {
|
|
28
|
+
id: "log-schema-check",
|
|
29
|
+
description: "Flag production source files (not tests / scripts / docs) that use console.log / print() instead of a structured logger.",
|
|
30
|
+
schema: { type: "object", properties: { rootPath: { type: "string" }, files: { type: "array", items: { type: "string" } } } },
|
|
31
|
+
handler: runLogSchemaCheck,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const OBSERVABILITY_TOOL_IDS = Object.freeze(Object.keys(OBSERVABILITY_TOOLS));
|
|
36
|
+
|
|
37
|
+
export async function dispatchObservabilityTool(toolId, args = {}) {
|
|
38
|
+
const tool = OBSERVABILITY_TOOLS[toolId];
|
|
39
|
+
if (!tool) {
|
|
40
|
+
throw new Error(`Unknown observability tool: ${toolId}`);
|
|
41
|
+
}
|
|
42
|
+
return tool.handler(args);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function runAllObservabilityTools({ rootPath, files = null } = {}) {
|
|
46
|
+
const findings = [];
|
|
47
|
+
for (const toolId of OBSERVABILITY_TOOL_IDS) {
|
|
48
|
+
const out = await dispatchObservabilityTool(toolId, { rootPath, files });
|
|
49
|
+
findings.push(...out);
|
|
50
|
+
}
|
|
51
|
+
return findings;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { runAlertAudit, runDashboardGap, runLogSchemaCheck, runSpanCoverage };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// log-schema-check — flag console.log in production code paths (#A20).
|
|
2
|
+
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { createFinding, findLineMatches, getLineContent, toPosix, walkRepoFiles } from "./base.js";
|
|
7
|
+
|
|
8
|
+
const CODE_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".py"]);
|
|
9
|
+
|
|
10
|
+
function isProductionSource(relPath) {
|
|
11
|
+
const p = toPosix(relPath);
|
|
12
|
+
if (/(^|\/)(tests?|__tests__|specs?)\//.test(p)) return false;
|
|
13
|
+
if (/\.(test|spec)\.(js|jsx|ts|tsx|mjs|cjs|py)$/.test(p)) return false;
|
|
14
|
+
if (/(^|\/)scripts\//.test(p)) return false;
|
|
15
|
+
if (/(^|\/)bin\//.test(p)) return false;
|
|
16
|
+
if (/(^|\/)docs?\//.test(p)) return false;
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const UNSTRUCTURED_LOG_PATTERNS_JS = /\bconsole\.(log|info|warn|error|debug)\s*\(/;
|
|
21
|
+
const UNSTRUCTURED_LOG_PATTERNS_PY = /\bprint\s*\(/;
|
|
22
|
+
|
|
23
|
+
export async function runLogSchemaCheck({ rootPath, files = null } = {}) {
|
|
24
|
+
const resolvedRoot = path.resolve(String(rootPath || "."));
|
|
25
|
+
const iterator =
|
|
26
|
+
Array.isArray(files) && files.length > 0
|
|
27
|
+
? iterateExplicitFiles(resolvedRoot, files)
|
|
28
|
+
: walkRepoFiles({ rootPath: resolvedRoot, extensions: CODE_EXTENSIONS });
|
|
29
|
+
|
|
30
|
+
const findings = [];
|
|
31
|
+
const reported = new Set();
|
|
32
|
+
for await (const { fullPath, relativePath } of iterator) {
|
|
33
|
+
if (!isProductionSource(relativePath)) continue;
|
|
34
|
+
let content;
|
|
35
|
+
try {
|
|
36
|
+
content = await fsp.readFile(fullPath, "utf-8");
|
|
37
|
+
} catch {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const ext = path.extname(fullPath).toLowerCase();
|
|
41
|
+
const pattern = ext === ".py" ? UNSTRUCTURED_LOG_PATTERNS_PY : UNSTRUCTURED_LOG_PATTERNS_JS;
|
|
42
|
+
const matches = findLineMatches(content, pattern);
|
|
43
|
+
if (matches.length === 0) continue;
|
|
44
|
+
const rel = toPosix(relativePath);
|
|
45
|
+
if (reported.has(rel)) continue;
|
|
46
|
+
reported.add(rel);
|
|
47
|
+
findings.push(
|
|
48
|
+
createFinding({
|
|
49
|
+
tool: "log-schema-check",
|
|
50
|
+
kind: "observability.unstructured-log",
|
|
51
|
+
severity: "P3",
|
|
52
|
+
file: rel,
|
|
53
|
+
line: matches[0].line,
|
|
54
|
+
evidence: getLineContent(content, matches[0].line),
|
|
55
|
+
rootCause: "Production code uses console.log / print() — unindexed output that can't be queried, correlated, or redacted at collection time.",
|
|
56
|
+
recommendedFix: "Route through a structured logger (pino, winston, structlog) with a shared schema. Configure PII fields to be automatically redacted.",
|
|
57
|
+
confidence: 0.55,
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return findings;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function* iterateExplicitFiles(resolvedRoot, files) {
|
|
65
|
+
for (const file of files) {
|
|
66
|
+
const trimmed = String(file || "").trim();
|
|
67
|
+
if (!trimmed) continue;
|
|
68
|
+
const fullPath = path.isAbsolute(trimmed) ? trimmed : path.join(resolvedRoot, trimmed);
|
|
69
|
+
const relativePath = path.relative(resolvedRoot, fullPath).replace(/\\/g, "/");
|
|
70
|
+
yield { fullPath, relativePath };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { isProductionSource };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// span-coverage — flag route handlers without a tracing span (#A20).
|
|
2
|
+
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { createFinding, findLineMatches, getLineContent, toPosix, walkRepoFiles } from "./base.js";
|
|
7
|
+
|
|
8
|
+
const CODE_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".py", ".go"]);
|
|
9
|
+
|
|
10
|
+
const HANDLER_PATTERNS = [
|
|
11
|
+
/\b(?:app|router|server|fastify|hono)\.(get|post|put|patch|delete)\s*\(/,
|
|
12
|
+
/^export\s+async\s+function\s+(GET|POST|PUT|PATCH|DELETE)\s*\(/m,
|
|
13
|
+
/@(?:app|router|api_router)\.(?:get|post|put|patch|delete)\s*\(/,
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const SPAN_SIGNALS = [
|
|
17
|
+
/tracer\.startSpan|tracer\.startActiveSpan|otel\.|opentelemetry|@tracing|withSpan|withActiveSpan/i,
|
|
18
|
+
/sentry\.(?:startTransaction|startSpan)/i,
|
|
19
|
+
/datadog|dd-trace|ddtrace/i,
|
|
20
|
+
/Sentry\.startTransaction/,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export async function runSpanCoverage({ rootPath, files = null } = {}) {
|
|
24
|
+
const resolvedRoot = path.resolve(String(rootPath || "."));
|
|
25
|
+
const iterator =
|
|
26
|
+
Array.isArray(files) && files.length > 0
|
|
27
|
+
? iterateExplicitFiles(resolvedRoot, files)
|
|
28
|
+
: walkRepoFiles({ rootPath: resolvedRoot, extensions: CODE_EXTENSIONS });
|
|
29
|
+
|
|
30
|
+
const findings = [];
|
|
31
|
+
for await (const { fullPath, relativePath } of iterator) {
|
|
32
|
+
let content;
|
|
33
|
+
try {
|
|
34
|
+
content = await fsp.readFile(fullPath, "utf-8");
|
|
35
|
+
} catch {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const hasHandler = HANDLER_PATTERNS.some((p) => p.test(content));
|
|
39
|
+
if (!hasHandler) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const hasSpan = SPAN_SIGNALS.some((p) => p.test(content));
|
|
43
|
+
if (hasSpan) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const match = findLineMatches(content, HANDLER_PATTERNS[0])[0] ||
|
|
47
|
+
findLineMatches(content, HANDLER_PATTERNS[1])[0] ||
|
|
48
|
+
findLineMatches(content, HANDLER_PATTERNS[2])[0];
|
|
49
|
+
findings.push(
|
|
50
|
+
createFinding({
|
|
51
|
+
tool: "span-coverage",
|
|
52
|
+
kind: "observability.no-span",
|
|
53
|
+
severity: "P2",
|
|
54
|
+
file: toPosix(relativePath),
|
|
55
|
+
line: match?.line || 1,
|
|
56
|
+
evidence: getLineContent(content, match?.line || 1),
|
|
57
|
+
rootCause: "Route handler declared without any tracing-span signal (OpenTelemetry, Sentry, Datadog). Request latency / error breakdowns will be opaque.",
|
|
58
|
+
recommendedFix: "Wrap the handler body in tracer.startActiveSpan('<name>', …) or use framework middleware that auto-instruments handlers.",
|
|
59
|
+
confidence: 0.55,
|
|
60
|
+
})
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return findings;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function* iterateExplicitFiles(resolvedRoot, files) {
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
const trimmed = String(file || "").trim();
|
|
69
|
+
if (!trimmed) continue;
|
|
70
|
+
const fullPath = path.isAbsolute(trimmed) ? trimmed : path.join(resolvedRoot, trimmed);
|
|
71
|
+
const relativePath = path.relative(resolvedRoot, fullPath).replace(/\\/g, "/");
|
|
72
|
+
yield { fullPath, relativePath };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -25,6 +25,44 @@ export const PERSONA_VISUALS = Object.freeze({
|
|
|
25
25
|
"ai-governance": { color: "violet", avatar: "\u{1F916}", shortName: "Amina", fullName: "Amina Chen", domain: "ai_pipeline", specialty: "Prompt injection, tool abuse, eval regressions, unsafe model routing, guardrail bypass, policy drift in agentic flows", bias: "AI autonomy requires proportional governance" },
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Visual identity for the orchestrator tier (Dr. Kai Chen / Senti).
|
|
30
|
+
*
|
|
31
|
+
* Kept SEPARATE from PERSONA_VISUALS so the dispatch-invariant test
|
|
32
|
+
* (FULL_DEPTH_PERSONAS ⊆ PERSONA_VISUALS keys) isn't polluted by
|
|
33
|
+
* non-review entries. Consumers that need the orchestrator visual
|
|
34
|
+
* import ORCHESTRATOR_VISUALS or call resolveOrchestratorVisual().
|
|
35
|
+
*/
|
|
36
|
+
export const ORCHESTRATOR_VISUALS = Object.freeze({
|
|
37
|
+
"kai-chen": {
|
|
38
|
+
color: "gold",
|
|
39
|
+
avatar: "\u{1F3AD}",
|
|
40
|
+
shortName: "Kai",
|
|
41
|
+
fullName: "Dr. Kai Chen",
|
|
42
|
+
domain: "orchestration",
|
|
43
|
+
specialty: "Global orchestrator — picks personas, deduplicates across domains, ranks by severity × confidence × blast radius, demands reproduction steps",
|
|
44
|
+
bias: "performance budgets; operational simplicity; correctness over cleverness",
|
|
45
|
+
role: "Global Orchestrator / Senti",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export function resolveOrchestratorVisual(idOrName) {
|
|
50
|
+
if (!idOrName) return null;
|
|
51
|
+
const lower = String(idOrName).toLowerCase();
|
|
52
|
+
if (ORCHESTRATOR_VISUALS[lower]) {
|
|
53
|
+
return { id: lower, ...ORCHESTRATOR_VISUALS[lower] };
|
|
54
|
+
}
|
|
55
|
+
for (const [id, visual] of Object.entries(ORCHESTRATOR_VISUALS)) {
|
|
56
|
+
if (
|
|
57
|
+
visual.shortName.toLowerCase() === lower
|
|
58
|
+
|| visual.fullName.toLowerCase() === lower
|
|
59
|
+
) {
|
|
60
|
+
return { id, ...visual };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
28
66
|
/**
|
|
29
67
|
* Resolve persona visual identity by agent ID or persona name.
|
|
30
68
|
*/
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Omar (release persona) — barrel export (#A19).
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
RELEASE_TOOLS,
|
|
5
|
+
RELEASE_TOOL_IDS,
|
|
6
|
+
dispatchReleaseTool,
|
|
7
|
+
runAllReleaseTools,
|
|
8
|
+
runChangelogDiff,
|
|
9
|
+
runFeatureFlagAudit,
|
|
10
|
+
runRollbackVerify,
|
|
11
|
+
runSemverCheck,
|
|
12
|
+
} from "./tools/index.js";
|