sentinelayer-cli 0.6.2 → 0.8.1
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 +1009 -996
- package/bin/create-sentinelayer.js +5 -5
- package/bin/sentinelayer-cli.js +4 -4
- package/bin/sl.js +5 -5
- package/package.json +64 -63
- 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/config/definition.js +160 -160
- package/src/agents/jules/config/system-prompt.js +182 -182
- package/src/agents/jules/error-intake.js +51 -51
- package/src/agents/jules/fix-cycle.js +17 -17
- package/src/agents/jules/loop.js +460 -450
- package/src/agents/jules/pulse.js +10 -10
- package/src/agents/jules/stream.js +187 -186
- package/src/agents/jules/swarm/file-scanner.js +74 -74
- package/src/agents/jules/swarm/index.js +11 -11
- package/src/agents/jules/swarm/orchestrator.js +362 -362
- package/src/agents/jules/swarm/pattern-hunter.js +123 -123
- package/src/agents/jules/swarm/sub-agent.js +315 -309
- package/src/agents/jules/tools/aidenid-email.js +189 -189
- package/src/agents/jules/tools/auth-audit.js +1708 -1691
- package/src/agents/jules/tools/dispatch.js +340 -335
- package/src/agents/jules/tools/file-edit.js +2 -2
- package/src/agents/jules/tools/file-read.js +2 -2
- package/src/agents/jules/tools/frontend-analyze.js +570 -570
- package/src/agents/jules/tools/glob.js +2 -2
- package/src/agents/jules/tools/grep.js +2 -2
- package/src/agents/jules/tools/index.js +29 -29
- package/src/agents/jules/tools/path-guards.js +2 -2
- package/src/agents/jules/tools/runtime-audit.js +507 -507
- package/src/agents/jules/tools/shell.js +2 -2
- package/src/agents/jules/tools/url-policy.js +100 -100
- 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 +102 -61
- 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/shared-tools/dispatch-core.js +320 -315
- package/src/agents/shared-tools/file-edit.js +180 -180
- package/src/agents/shared-tools/file-read.js +100 -100
- package/src/agents/shared-tools/glob.js +168 -168
- package/src/agents/shared-tools/grep.js +228 -228
- package/src/agents/shared-tools/index.js +46 -46
- package/src/agents/shared-tools/path-guards.js +161 -161
- package/src/agents/shared-tools/shell.js +383 -383
- 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/ai/aidenid.js +1021 -1009
- package/src/ai/client.js +553 -553
- package/src/ai/domain-target-store.js +268 -268
- package/src/ai/identity-store.js +270 -270
- package/src/ai/proxy.js +137 -137
- package/src/ai/site-store.js +145 -145
- package/src/audit/agents/architecture.js +180 -180
- package/src/audit/agents/compliance.js +179 -179
- package/src/audit/agents/documentation.js +165 -165
- package/src/audit/agents/performance.js +145 -145
- package/src/audit/agents/security.js +215 -215
- package/src/audit/agents/testing.js +172 -172
- package/src/audit/orchestrator.js +557 -557
- package/src/audit/package.js +204 -204
- package/src/audit/registry.js +284 -284
- package/src/audit/replay.js +103 -103
- package/src/auth/gate.js +428 -371
- package/src/auth/http.js +681 -611
- package/src/auth/service.js +1106 -1106
- package/src/auth/session-store.js +813 -813
- package/src/cli.js +257 -252
- package/src/commands/ai/identity-lifecycle.js +1338 -1338
- package/src/commands/ai/provision-governance.js +1272 -1272
- package/src/commands/ai/shared.js +147 -147
- package/src/commands/ai.js +11 -11
- package/src/commands/apply.js +12 -12
- package/src/commands/audit.js +1171 -1166
- package/src/commands/auth.js +419 -419
- package/src/commands/chat.js +184 -191
- package/src/commands/config.js +184 -184
- package/src/commands/cost.js +311 -311
- package/src/commands/daemon/core.js +850 -850
- package/src/commands/daemon/extended.js +1048 -1048
- package/src/commands/daemon/shared.js +213 -213
- package/src/commands/daemon.js +11 -11
- package/src/commands/guide.js +174 -174
- package/src/commands/ingest.js +58 -58
- package/src/commands/init.js +55 -55
- package/src/commands/legacy-args.js +20 -10
- package/src/commands/mcp.js +461 -461
- package/src/commands/omargate.js +63 -29
- package/src/commands/persona.js +65 -20
- package/src/commands/plugin.js +260 -260
- package/src/commands/policy.js +132 -132
- package/src/commands/prompt.js +238 -238
- package/src/commands/review.js +704 -704
- package/src/commands/scan.js +865 -872
- package/src/commands/session.js +1238 -0
- package/src/commands/spec.js +771 -716
- package/src/commands/swarm.js +651 -651
- package/src/commands/telemetry.js +202 -202
- package/src/commands/watch.js +511 -511
- package/src/config/agent-dictionary.js +182 -182
- package/src/config/io.js +56 -56
- package/src/config/paths.js +18 -18
- package/src/config/schema.js +55 -55
- package/src/config/service.js +184 -184
- 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/budget.js +235 -235
- package/src/cost/history.js +188 -188
- package/src/cost/tokenizer.js +160 -0
- package/src/cost/tracker.js +232 -171
- package/src/daemon/artifact-lineage.js +896 -534
- package/src/daemon/assignment-ledger.js +1083 -770
- package/src/daemon/ast-drift.js +496 -0
- package/src/daemon/ast-parser-layer.js +258 -258
- package/src/daemon/budget-governor.js +633 -633
- package/src/daemon/callgraph-overlay.js +646 -646
- package/src/daemon/error-worker.js +1209 -626
- package/src/daemon/fix-cycle.js +384 -377
- package/src/daemon/hybrid-mapper.js +929 -929
- package/src/daemon/ingest-refresh.js +79 -11
- package/src/daemon/jira-lifecycle.js +767 -632
- package/src/daemon/operator-control.js +657 -657
- package/src/daemon/pulse.js +327 -327
- package/src/daemon/reliability-lane.js +471 -471
- package/src/daemon/scope-engine.js +1068 -0
- package/src/daemon/watchdog.js +971 -971
- package/src/events/schema.js +190 -0
- package/src/guide/generator.js +316 -316
- package/src/ingest/engine.js +933 -918
- package/src/ingest/ownership.js +380 -0
- package/src/interactive/index.js +97 -97
- package/src/legacy-cli.js +3228 -2994
- package/src/mcp/registry.js +695 -695
- package/src/memory/blackboard.js +301 -301
- package/src/memory/retrieval.js +581 -581
- package/src/orchestrator/kai-chen.js +126 -0
- package/src/plugin/manifest.js +553 -553
- package/src/policy/packs.js +144 -144
- package/src/prompt/generator.js +136 -118
- package/src/review/ai-review.js +672 -679
- 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/local-review.js +1351 -1305
- package/src/review/omargate-interactive.js +68 -68
- package/src/review/omargate-orchestrator.js +492 -300
- package/src/review/persona-prompts.js +484 -296
- package/src/review/reconciliation-rules.js +329 -0
- package/src/review/replay.js +235 -235
- package/src/review/report.js +664 -664
- package/src/review/reproducibility-chain.js +136 -0
- package/src/review/scan-modes.js +147 -42
- package/src/review/spec-binding.js +487 -487
- package/src/scaffold/generator.js +67 -67
- package/src/scaffold/templates.js +150 -150
- package/src/scan/generator.js +418 -418
- package/src/scan/gh-secrets.js +107 -107
- package/src/session/agent-registry.js +359 -0
- package/src/session/analytics.js +479 -0
- package/src/session/daemon.js +1396 -0
- package/src/session/file-locks.js +666 -0
- package/src/session/paths.js +37 -0
- package/src/session/recap.js +567 -0
- package/src/session/redact.js +82 -0
- package/src/session/runtime-bridge.js +762 -0
- package/src/session/scoring.js +406 -0
- package/src/session/setup-guides.js +304 -0
- package/src/session/store.js +704 -0
- package/src/session/stream.js +333 -0
- package/src/session/sync.js +753 -0
- package/src/session/tasks.js +1054 -0
- package/src/session/templates.js +188 -0
- package/src/spec/generator.js +619 -519
- package/src/spec/regenerate.js +237 -237
- package/src/spec/templates.js +91 -91
- package/src/swarm/dashboard.js +247 -247
- package/src/swarm/factory.js +363 -363
- package/src/swarm/pentest.js +934 -934
- package/src/swarm/registry.js +419 -419
- package/src/swarm/report.js +158 -158
- package/src/swarm/runtime.js +569 -576
- package/src/swarm/scenario-dsl.js +272 -272
- package/src/telemetry/ledger.js +302 -302
- package/src/telemetry/session-tracker.js +234 -234
- package/src/telemetry/sync.js +203 -203
- package/src/ui/command-hints.js +13 -13
- package/src/ui/markdown.js +220 -220
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// File-to-persona ownership routing (#A10, spec §5.7).
|
|
2
|
+
//
|
|
3
|
+
// Personas are expensive: every persona runs an LLM call over whatever files
|
|
4
|
+
// it thinks are in-scope. When 13 personas each scan the whole codebase the
|
|
5
|
+
// token usage compounds. This module lets the orchestrator send each finding
|
|
6
|
+
// (or each file) to only the persona that owns the code, which the spec
|
|
7
|
+
// measures at >40% token savings on multi-persona runs.
|
|
8
|
+
//
|
|
9
|
+
// Two routing modes:
|
|
10
|
+
// 1. Explicit — read `.sentinelayer/scaffold.yaml`, walk `ownership_rules`
|
|
11
|
+
// as a last-match-wins glob → persona list.
|
|
12
|
+
// 2. Heuristic — no scaffold.yaml, fall back to keyword / extension rules
|
|
13
|
+
// derived from the 13-persona canon (security, backend, frontend,
|
|
14
|
+
// testing, code-quality, data-layer, documentation, reliability,
|
|
15
|
+
// release, observability, infrastructure, supply-chain, ai-governance).
|
|
16
|
+
//
|
|
17
|
+
// All exports are pure functions: no filesystem work except
|
|
18
|
+
// `loadScaffoldConfig` which reads a single YAML file. The rest operate on
|
|
19
|
+
// in-memory inputs so they compose cleanly with existing ingest callers.
|
|
20
|
+
|
|
21
|
+
import fsp from "node:fs/promises";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import process from "node:process";
|
|
24
|
+
|
|
25
|
+
import YAML from "yaml";
|
|
26
|
+
|
|
27
|
+
import { PERSONA_IDS } from "../review/persona-prompts.js";
|
|
28
|
+
|
|
29
|
+
const DEFAULT_HEURISTIC_FALLBACK = "backend";
|
|
30
|
+
const SCAFFOLD_RELATIVE_PATH = ".sentinelayer/scaffold.yaml";
|
|
31
|
+
|
|
32
|
+
// --- Glob matching -------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
// Translate a shell-style glob into a RegExp. Supports `*` (single segment),
|
|
35
|
+
// `**` (cross-segment), `?` (single char), and character classes passed
|
|
36
|
+
// through. Not a full fnmatch — but enough for ownership routing and good
|
|
37
|
+
// enough that a pattern like `lib/auth/**/*.{ts,tsx}` could be rewritten as
|
|
38
|
+
// two entries: `lib/auth/**/*.ts` and `lib/auth/**/*.tsx`.
|
|
39
|
+
function globToRegExp(glob) {
|
|
40
|
+
const raw = String(glob || "").trim();
|
|
41
|
+
if (!raw) {
|
|
42
|
+
throw new Error("ownership_rules.pattern is required.");
|
|
43
|
+
}
|
|
44
|
+
const normalized = raw.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
45
|
+
|
|
46
|
+
let escaped = "";
|
|
47
|
+
for (let idx = 0; idx < normalized.length; idx += 1) {
|
|
48
|
+
const ch = normalized[idx];
|
|
49
|
+
const next = normalized[idx + 1];
|
|
50
|
+
if (ch === "*") {
|
|
51
|
+
if (next === "*") {
|
|
52
|
+
// `**/` matches zero or more path segments, `/** ` matches any tail
|
|
53
|
+
if (normalized[idx + 2] === "/") {
|
|
54
|
+
escaped += "(?:.*/)?";
|
|
55
|
+
idx += 2;
|
|
56
|
+
} else {
|
|
57
|
+
escaped += ".*";
|
|
58
|
+
idx += 1;
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
escaped += "[^/]*";
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (ch === "?") {
|
|
66
|
+
escaped += "[^/]";
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if ("\\.+^$(){}|".includes(ch)) {
|
|
70
|
+
escaped += `\\${ch}`;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
escaped += ch;
|
|
74
|
+
}
|
|
75
|
+
return new RegExp(`^${escaped}$`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function matchGlob(pattern, filePath) {
|
|
79
|
+
return globToRegExp(pattern).test(filePath);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizePathForMatch(filePath) {
|
|
83
|
+
return String(filePath || "")
|
|
84
|
+
.trim()
|
|
85
|
+
.replace(/\\/g, "/")
|
|
86
|
+
.replace(/^\.\//, "");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizePersonaId(value) {
|
|
90
|
+
return String(value || "").trim().toLowerCase();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function assertKnownPersona(value) {
|
|
94
|
+
const normalized = normalizePersonaId(value);
|
|
95
|
+
if (!normalized) {
|
|
96
|
+
throw new Error("ownership_rules.persona is required.");
|
|
97
|
+
}
|
|
98
|
+
if (!PERSONA_IDS.includes(normalized)) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`ownership_rules.persona must be one of ${PERSONA_IDS.join(", ")} (got "${value}").`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return normalized;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- Scaffold YAML -------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
export function parseScaffoldYaml(raw) {
|
|
109
|
+
const text = String(raw || "");
|
|
110
|
+
const trimmed = text.trim();
|
|
111
|
+
if (!trimmed) {
|
|
112
|
+
return { ownershipRules: [] };
|
|
113
|
+
}
|
|
114
|
+
let parsed;
|
|
115
|
+
try {
|
|
116
|
+
parsed = YAML.parse(text);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
throw new Error(`scaffold.yaml is not valid YAML: ${err.message}`);
|
|
119
|
+
}
|
|
120
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
121
|
+
throw new Error("scaffold.yaml must be a mapping at the top level.");
|
|
122
|
+
}
|
|
123
|
+
const rawRules = parsed.ownership_rules;
|
|
124
|
+
if (rawRules === undefined || rawRules === null) {
|
|
125
|
+
return { ownershipRules: [] };
|
|
126
|
+
}
|
|
127
|
+
if (!Array.isArray(rawRules)) {
|
|
128
|
+
throw new Error("scaffold.yaml ownership_rules must be a list.");
|
|
129
|
+
}
|
|
130
|
+
const ownershipRules = rawRules.map((rule, idx) => {
|
|
131
|
+
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
|
|
132
|
+
throw new Error(`scaffold.yaml ownership_rules[${idx}] must be a mapping.`);
|
|
133
|
+
}
|
|
134
|
+
const pattern = String(rule.pattern || "").trim();
|
|
135
|
+
if (!pattern) {
|
|
136
|
+
throw new Error(`scaffold.yaml ownership_rules[${idx}].pattern is required.`);
|
|
137
|
+
}
|
|
138
|
+
const persona = assertKnownPersona(rule.persona);
|
|
139
|
+
return { pattern, persona };
|
|
140
|
+
});
|
|
141
|
+
return { ownershipRules };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function loadScaffoldConfig({
|
|
145
|
+
targetPath = process.cwd(),
|
|
146
|
+
relativePath = SCAFFOLD_RELATIVE_PATH,
|
|
147
|
+
} = {}) {
|
|
148
|
+
const absolutePath = path.join(
|
|
149
|
+
path.resolve(String(targetPath || ".")),
|
|
150
|
+
String(relativePath || SCAFFOLD_RELATIVE_PATH)
|
|
151
|
+
);
|
|
152
|
+
try {
|
|
153
|
+
const raw = await fsp.readFile(absolutePath, "utf-8");
|
|
154
|
+
return { found: true, path: absolutePath, ...parseScaffoldYaml(raw) };
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if (err && typeof err === "object" && err.code === "ENOENT") {
|
|
157
|
+
return { found: false, path: absolutePath, ownershipRules: [] };
|
|
158
|
+
}
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// --- Heuristic routing ---------------------------------------------------
|
|
164
|
+
|
|
165
|
+
// The heuristic table is explicit rather than a big switch: earlier entries
|
|
166
|
+
// are more specific, later entries are broader catch-alls. We iterate in
|
|
167
|
+
// order and take the first match so "docs/api.md" sorts as documentation
|
|
168
|
+
// rather than getting routed to backend by the ".md" extension catch-all.
|
|
169
|
+
const HEURISTIC_RULES = [
|
|
170
|
+
{
|
|
171
|
+
persona: "testing",
|
|
172
|
+
match: (p) =>
|
|
173
|
+
/(^|\/)(tests?|__tests__|specs?)\//.test(p) ||
|
|
174
|
+
/\.(test|spec)\.(js|jsx|ts|tsx|mjs|cjs|py|rb|go|rs)$/.test(p),
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
persona: "documentation",
|
|
178
|
+
match: (p) =>
|
|
179
|
+
/(^|\/)docs?\//.test(p) ||
|
|
180
|
+
/(^|\/)(README|CHANGELOG|CONTRIBUTING|ADR)(\.md)?$/i.test(p) ||
|
|
181
|
+
/(^|\/)adr[-_]/i.test(p),
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
persona: "supply-chain",
|
|
185
|
+
match: (p) =>
|
|
186
|
+
/(^|\/)package(-lock)?\.json$/.test(p) ||
|
|
187
|
+
/(^|\/)yarn\.lock$/.test(p) ||
|
|
188
|
+
/(^|\/)pnpm-lock\.yaml$/.test(p) ||
|
|
189
|
+
/(^|\/)requirements([-.]\w+)?\.txt$/.test(p) ||
|
|
190
|
+
/(^|\/)pyproject\.toml$/.test(p) ||
|
|
191
|
+
/(^|\/)Pipfile(\.lock)?$/.test(p) ||
|
|
192
|
+
/(^|\/)Gemfile(\.lock)?$/.test(p) ||
|
|
193
|
+
/(^|\/)go\.(mod|sum)$/.test(p) ||
|
|
194
|
+
/(^|\/)cargo\.toml$/i.test(p) ||
|
|
195
|
+
/(^|\/)renovate\.json$/.test(p),
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
persona: "release",
|
|
199
|
+
match: (p) =>
|
|
200
|
+
/(^|\/)\.github\/workflows\//.test(p) ||
|
|
201
|
+
/(^|\/)release-please/.test(p) ||
|
|
202
|
+
/(^|\/)action\.yml$/.test(p) ||
|
|
203
|
+
/(^|\/)\.releaserc/.test(p) ||
|
|
204
|
+
/(^|\/)(scripts|bin)\/release/.test(p),
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
persona: "infrastructure",
|
|
208
|
+
match: (p) =>
|
|
209
|
+
/(^|\/)(infra|terraform|k8s|kubernetes|helm)\//.test(p) ||
|
|
210
|
+
/\.(tf|tfvars|hcl)$/.test(p) ||
|
|
211
|
+
/(^|\/)Dockerfile(\.\w+)?$/.test(p) ||
|
|
212
|
+
/(^|\/)docker-compose(\.[-\w]+)?\.ya?ml$/.test(p) ||
|
|
213
|
+
/(^|\/)\.dockerignore$/.test(p),
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
persona: "observability",
|
|
217
|
+
match: (p) =>
|
|
218
|
+
/(^|\/)(observability|telemetry|metrics|tracing|logging|monitoring)\//i.test(p) ||
|
|
219
|
+
/(^|\/)sentry\.(client|server)\./.test(p),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
persona: "ai-governance",
|
|
223
|
+
match: (p) =>
|
|
224
|
+
/(^|\/)(prompts?|llm|ai|agents?)\//i.test(p) ||
|
|
225
|
+
/(^|\/)prompt[-_]/.test(p) ||
|
|
226
|
+
/\.prompt(\.md)?$/.test(p),
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
persona: "data-layer",
|
|
230
|
+
match: (p) =>
|
|
231
|
+
/(^|\/)(migrations?|alembic|prisma|db|database|schema)\//i.test(p) ||
|
|
232
|
+
/\.sql$/.test(p) ||
|
|
233
|
+
/(^|\/)models?\//i.test(p),
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
persona: "security",
|
|
237
|
+
match: (p) =>
|
|
238
|
+
/(^|\/)(auth|authn|authz|security)\//i.test(p) ||
|
|
239
|
+
/(^|\/)(middleware|guards?)\/(auth|security)/i.test(p) ||
|
|
240
|
+
/(^|\/)\.env(\.\w+)?$/.test(p),
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
persona: "frontend",
|
|
244
|
+
match: (p) =>
|
|
245
|
+
/(^|\/)(components?|pages?|app|views?|ui|styles?)\//i.test(p) ||
|
|
246
|
+
/\.(tsx|jsx|vue|svelte|css|scss|sass)$/.test(p),
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
persona: "reliability",
|
|
250
|
+
match: (p) =>
|
|
251
|
+
/(^|\/)(health|liveness|readiness|circuit[-_]?breaker)\//i.test(p) ||
|
|
252
|
+
/(^|\/)retries?\//i.test(p),
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
persona: "code-quality",
|
|
256
|
+
match: (p) =>
|
|
257
|
+
/(^|\/)\.?(eslintrc|prettierrc|biome|stylelintrc)(\.[-\w]+)?$/.test(p) ||
|
|
258
|
+
/(^|\/)\.editorconfig$/.test(p),
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
persona: "backend",
|
|
262
|
+
match: (p) =>
|
|
263
|
+
/(^|\/)(api|server|backend|routes?|services?|handlers?|controllers?)\//i.test(
|
|
264
|
+
p
|
|
265
|
+
) ||
|
|
266
|
+
/\.(py|rb|go|rs)$/.test(p) ||
|
|
267
|
+
/\.(ts|js|mts|mjs|cts|cjs)$/.test(p),
|
|
268
|
+
},
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
export function routeFileHeuristic(
|
|
272
|
+
filePath,
|
|
273
|
+
{ fallback = DEFAULT_HEURISTIC_FALLBACK } = {}
|
|
274
|
+
) {
|
|
275
|
+
const normalized = normalizePathForMatch(filePath);
|
|
276
|
+
if (!normalized) {
|
|
277
|
+
return fallback;
|
|
278
|
+
}
|
|
279
|
+
for (const rule of HEURISTIC_RULES) {
|
|
280
|
+
if (rule.match(normalized)) {
|
|
281
|
+
return rule.persona;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return fallback;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// --- Public API ---------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
// Given the file list plus (optional) scaffold config, produce a Map of
|
|
290
|
+
// posix-style relative path → persona id. Rules are last-match-wins: the
|
|
291
|
+
// scaffold ordering lets authors put a broad default first, then override
|
|
292
|
+
// subtrees below.
|
|
293
|
+
export function buildOwnershipMap(files, scaffoldConfig = null) {
|
|
294
|
+
const rules = Array.isArray(scaffoldConfig?.ownershipRules)
|
|
295
|
+
? scaffoldConfig.ownershipRules
|
|
296
|
+
: [];
|
|
297
|
+
const map = new Map();
|
|
298
|
+
const fileList = Array.isArray(files) ? files : [];
|
|
299
|
+
for (const rawFile of fileList) {
|
|
300
|
+
const file = normalizePathForMatch(rawFile);
|
|
301
|
+
if (!file) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (rules.length > 0) {
|
|
305
|
+
let owner = null;
|
|
306
|
+
for (const rule of rules) {
|
|
307
|
+
if (matchGlob(rule.pattern, file)) {
|
|
308
|
+
owner = rule.persona;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (owner) {
|
|
312
|
+
map.set(file, owner);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
map.set(file, routeFileHeuristic(file));
|
|
317
|
+
}
|
|
318
|
+
return map;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Bin findings by persona using the ownership map. Findings whose file is
|
|
322
|
+
// not in the map (e.g. a scanner reported on a path outside the ingest)
|
|
323
|
+
// fall back to the heuristic router so they don't get silently dropped.
|
|
324
|
+
export function routeFindingsToPersonas(findings, ownershipMap) {
|
|
325
|
+
const source = Array.isArray(findings) ? findings : [];
|
|
326
|
+
const map = ownershipMap instanceof Map ? ownershipMap : new Map();
|
|
327
|
+
const perPersona = {};
|
|
328
|
+
for (const finding of source) {
|
|
329
|
+
if (!finding || typeof finding !== "object") {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const filePath = normalizePathForMatch(
|
|
333
|
+
finding.file || finding.path || finding.location || ""
|
|
334
|
+
);
|
|
335
|
+
let persona = normalizePersonaId(map.get(filePath) || "");
|
|
336
|
+
if (!persona) {
|
|
337
|
+
persona = routeFileHeuristic(filePath);
|
|
338
|
+
}
|
|
339
|
+
if (!perPersona[persona]) {
|
|
340
|
+
perPersona[persona] = [];
|
|
341
|
+
}
|
|
342
|
+
perPersona[persona].push(finding);
|
|
343
|
+
}
|
|
344
|
+
return perPersona;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Lightweight metric for the spec's ≥40% token-reduction target. Given an
|
|
348
|
+
// ownership map + pre-routing cost assumption (every persona sees every
|
|
349
|
+
// file), report how many files each persona would actually need to scan.
|
|
350
|
+
export function computeRoutingStats(ownershipMap) {
|
|
351
|
+
const map = ownershipMap instanceof Map ? ownershipMap : new Map();
|
|
352
|
+
const totalFiles = map.size;
|
|
353
|
+
if (totalFiles === 0) {
|
|
354
|
+
return {
|
|
355
|
+
totalFiles: 0,
|
|
356
|
+
personaCoverage: {},
|
|
357
|
+
totalScansUnrouted: 0,
|
|
358
|
+
totalScansRouted: 0,
|
|
359
|
+
tokenReductionEstimatePct: 0,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const personaCoverage = {};
|
|
363
|
+
for (const persona of map.values()) {
|
|
364
|
+
personaCoverage[persona] = (personaCoverage[persona] || 0) + 1;
|
|
365
|
+
}
|
|
366
|
+
const totalScansUnrouted = totalFiles * PERSONA_IDS.length;
|
|
367
|
+
const totalScansRouted = totalFiles; // 1 persona per file with last-match-wins routing
|
|
368
|
+
const tokenReductionEstimatePct = Math.round(
|
|
369
|
+
(1 - totalScansRouted / totalScansUnrouted) * 100
|
|
370
|
+
);
|
|
371
|
+
return {
|
|
372
|
+
totalFiles,
|
|
373
|
+
personaCoverage,
|
|
374
|
+
totalScansUnrouted,
|
|
375
|
+
totalScansRouted,
|
|
376
|
+
tokenReductionEstimatePct,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export { DEFAULT_HEURISTIC_FALLBACK, SCAFFOLD_RELATIVE_PATH };
|
package/src/interactive/index.js
CHANGED
|
@@ -1,97 +1,97 @@
|
|
|
1
|
-
import pc from "picocolors";
|
|
2
|
-
import { selectRepo } from "./workspace.js";
|
|
3
|
-
import { autoIngestWithProgress } from "./auto-ingest.js";
|
|
4
|
-
import { showActionMenu } from "./action-menu.js";
|
|
5
|
-
import { preferredCliCommand } from "../ui/command-hints.js";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Interactive CLI mode — the "sl" experience with no args.
|
|
9
|
-
*
|
|
10
|
-
* Flow:
|
|
11
|
-
* 1. Detect repos in workspace → select if multiple
|
|
12
|
-
* 2. Auto-ingest with live progress
|
|
13
|
-
* 3. Present action menu
|
|
14
|
-
* 4. Route to the selected command
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Run the interactive flow.
|
|
19
|
-
*
|
|
20
|
-
* @param {object} [options]
|
|
21
|
-
* @param {function} [options.executeCommand] - Command executor (receives action + args)
|
|
22
|
-
* @returns {Promise<void>}
|
|
23
|
-
*/
|
|
24
|
-
export async function runInteractiveMode(options = {}) {
|
|
25
|
-
console.error("");
|
|
26
|
-
console.error(pc.bold(" SentinelLayer CLI") + pc.gray(" — security-first development platform"));
|
|
27
|
-
console.error("");
|
|
28
|
-
|
|
29
|
-
// Step 1: Repo selection
|
|
30
|
-
const repo = await selectRepo();
|
|
31
|
-
if (!repo) {
|
|
32
|
-
console.error(pc.yellow(`No repository selected. Run ${preferredCliCommand()} --help for available commands.`));
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Step 2: Auto-ingest
|
|
37
|
-
const ingest = await autoIngestWithProgress(repo.path);
|
|
38
|
-
|
|
39
|
-
// Step 3: Action menu
|
|
40
|
-
const choice = await showActionMenu();
|
|
41
|
-
if (choice.action === "exit") {
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Step 4: Route to command
|
|
46
|
-
console.error("");
|
|
47
|
-
if (options.executeCommand) {
|
|
48
|
-
await options.executeCommand(choice, repo, ingest);
|
|
49
|
-
} else {
|
|
50
|
-
// Print the equivalent CLI command for the user
|
|
51
|
-
const cmd = buildEquivalentCommand(choice, repo);
|
|
52
|
-
if (cmd) {
|
|
53
|
-
console.error(pc.gray(" Equivalent command: ") + pc.cyan(cmd));
|
|
54
|
-
console.error("");
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Build the equivalent CLI command string for a menu choice.
|
|
61
|
-
*/
|
|
62
|
-
function buildEquivalentCommand(choice, repo) {
|
|
63
|
-
const cli = preferredCliCommand();
|
|
64
|
-
const pathFlag = " --path " + repo.path;
|
|
65
|
-
|
|
66
|
-
switch (choice.action) {
|
|
67
|
-
case "audit":
|
|
68
|
-
if (choice.subAction === "deep") return `${cli} audit${pathFlag} --json`;
|
|
69
|
-
return `${cli} audit ${choice.subAction}${pathFlag} --stream`;
|
|
70
|
-
case "review":
|
|
71
|
-
if (choice.subAction === "diff") return `${cli} review scan --mode diff${pathFlag} --json`;
|
|
72
|
-
if (choice.subAction === "staged") return `${cli} review scan --mode staged${pathFlag} --json`;
|
|
73
|
-
return `${cli} review scan --mode full${pathFlag} --json`;
|
|
74
|
-
case "feature":
|
|
75
|
-
return `${cli} spec generate --description "${(choice.input || "").slice(0, 50)}..."${pathFlag}`;
|
|
76
|
-
case "create":
|
|
77
|
-
return `${cli} init`;
|
|
78
|
-
case "cost":
|
|
79
|
-
return `${cli} cost show${pathFlag} --json`;
|
|
80
|
-
case "telemetry":
|
|
81
|
-
return `${cli} telemetry show${pathFlag} --json`;
|
|
82
|
-
case "config":
|
|
83
|
-
return `${cli} config list --json`;
|
|
84
|
-
case "auth-status":
|
|
85
|
-
return `${cli} auth status --json`;
|
|
86
|
-
case "plugins":
|
|
87
|
-
return `${cli} plugin list --json`;
|
|
88
|
-
case "watch":
|
|
89
|
-
return `${cli} watch history${pathFlag} --json`;
|
|
90
|
-
case "ai":
|
|
91
|
-
return `${cli} ai provision-email --json`;
|
|
92
|
-
case "daemon":
|
|
93
|
-
return `${cli} daemon budget status${pathFlag} --json`;
|
|
94
|
-
default:
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { selectRepo } from "./workspace.js";
|
|
3
|
+
import { autoIngestWithProgress } from "./auto-ingest.js";
|
|
4
|
+
import { showActionMenu } from "./action-menu.js";
|
|
5
|
+
import { preferredCliCommand } from "../ui/command-hints.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Interactive CLI mode — the "sl" experience with no args.
|
|
9
|
+
*
|
|
10
|
+
* Flow:
|
|
11
|
+
* 1. Detect repos in workspace → select if multiple
|
|
12
|
+
* 2. Auto-ingest with live progress
|
|
13
|
+
* 3. Present action menu
|
|
14
|
+
* 4. Route to the selected command
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Run the interactive flow.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} [options]
|
|
21
|
+
* @param {function} [options.executeCommand] - Command executor (receives action + args)
|
|
22
|
+
* @returns {Promise<void>}
|
|
23
|
+
*/
|
|
24
|
+
export async function runInteractiveMode(options = {}) {
|
|
25
|
+
console.error("");
|
|
26
|
+
console.error(pc.bold(" SentinelLayer CLI") + pc.gray(" — security-first development platform"));
|
|
27
|
+
console.error("");
|
|
28
|
+
|
|
29
|
+
// Step 1: Repo selection
|
|
30
|
+
const repo = await selectRepo();
|
|
31
|
+
if (!repo) {
|
|
32
|
+
console.error(pc.yellow(`No repository selected. Run ${preferredCliCommand()} --help for available commands.`));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Step 2: Auto-ingest
|
|
37
|
+
const ingest = await autoIngestWithProgress(repo.path);
|
|
38
|
+
|
|
39
|
+
// Step 3: Action menu
|
|
40
|
+
const choice = await showActionMenu();
|
|
41
|
+
if (choice.action === "exit") {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Step 4: Route to command
|
|
46
|
+
console.error("");
|
|
47
|
+
if (options.executeCommand) {
|
|
48
|
+
await options.executeCommand(choice, repo, ingest);
|
|
49
|
+
} else {
|
|
50
|
+
// Print the equivalent CLI command for the user
|
|
51
|
+
const cmd = buildEquivalentCommand(choice, repo);
|
|
52
|
+
if (cmd) {
|
|
53
|
+
console.error(pc.gray(" Equivalent command: ") + pc.cyan(cmd));
|
|
54
|
+
console.error("");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build the equivalent CLI command string for a menu choice.
|
|
61
|
+
*/
|
|
62
|
+
function buildEquivalentCommand(choice, repo) {
|
|
63
|
+
const cli = preferredCliCommand();
|
|
64
|
+
const pathFlag = " --path " + repo.path;
|
|
65
|
+
|
|
66
|
+
switch (choice.action) {
|
|
67
|
+
case "audit":
|
|
68
|
+
if (choice.subAction === "deep") return `${cli} audit${pathFlag} --json`;
|
|
69
|
+
return `${cli} audit ${choice.subAction}${pathFlag} --stream`;
|
|
70
|
+
case "review":
|
|
71
|
+
if (choice.subAction === "diff") return `${cli} review scan --mode diff${pathFlag} --json`;
|
|
72
|
+
if (choice.subAction === "staged") return `${cli} review scan --mode staged${pathFlag} --json`;
|
|
73
|
+
return `${cli} review scan --mode full${pathFlag} --json`;
|
|
74
|
+
case "feature":
|
|
75
|
+
return `${cli} spec generate --description "${(choice.input || "").slice(0, 50)}..."${pathFlag}`;
|
|
76
|
+
case "create":
|
|
77
|
+
return `${cli} init`;
|
|
78
|
+
case "cost":
|
|
79
|
+
return `${cli} cost show${pathFlag} --json`;
|
|
80
|
+
case "telemetry":
|
|
81
|
+
return `${cli} telemetry show${pathFlag} --json`;
|
|
82
|
+
case "config":
|
|
83
|
+
return `${cli} config list --json`;
|
|
84
|
+
case "auth-status":
|
|
85
|
+
return `${cli} auth status --json`;
|
|
86
|
+
case "plugins":
|
|
87
|
+
return `${cli} plugin list --json`;
|
|
88
|
+
case "watch":
|
|
89
|
+
return `${cli} watch history${pathFlag} --json`;
|
|
90
|
+
case "ai":
|
|
91
|
+
return `${cli} ai provision-email --json`;
|
|
92
|
+
case "daemon":
|
|
93
|
+
return `${cli} daemon budget status${pathFlag} --json`;
|
|
94
|
+
default:
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|