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.
Files changed (280) hide show
  1. package/README.md +1009 -996
  2. package/bin/create-sentinelayer.js +5 -5
  3. package/bin/sentinelayer-cli.js +4 -4
  4. package/bin/sl.js +5 -5
  5. package/package.json +64 -63
  6. package/src/agents/ai-governance/index.js +12 -0
  7. package/src/agents/ai-governance/tools/base.js +171 -0
  8. package/src/agents/ai-governance/tools/eval-regression.js +47 -0
  9. package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
  10. package/src/agents/ai-governance/tools/index.js +52 -0
  11. package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
  12. package/src/agents/ai-governance/tools/provenance-check.js +69 -0
  13. package/src/agents/backend/index.js +12 -0
  14. package/src/agents/backend/tools/base.js +189 -0
  15. package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
  16. package/src/agents/backend/tools/idempotency-audit.js +105 -0
  17. package/src/agents/backend/tools/index.js +87 -0
  18. package/src/agents/backend/tools/retry-audit.js +132 -0
  19. package/src/agents/backend/tools/timeout-audit.js +144 -0
  20. package/src/agents/code-quality/index.js +12 -0
  21. package/src/agents/code-quality/tools/base.js +159 -0
  22. package/src/agents/code-quality/tools/complexity-measure.js +197 -0
  23. package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
  24. package/src/agents/code-quality/tools/cycle-detect.js +49 -0
  25. package/src/agents/code-quality/tools/dep-graph.js +196 -0
  26. package/src/agents/code-quality/tools/index.js +89 -0
  27. package/src/agents/data-layer/index.js +12 -0
  28. package/src/agents/data-layer/tools/base.js +181 -0
  29. package/src/agents/data-layer/tools/index-audit.js +165 -0
  30. package/src/agents/data-layer/tools/index.js +83 -0
  31. package/src/agents/data-layer/tools/migration-scan.js +135 -0
  32. package/src/agents/data-layer/tools/query-explain.js +120 -0
  33. package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
  34. package/src/agents/documentation/index.js +12 -0
  35. package/src/agents/documentation/tools/api-diff.js +91 -0
  36. package/src/agents/documentation/tools/base.js +151 -0
  37. package/src/agents/documentation/tools/dead-link-check.js +58 -0
  38. package/src/agents/documentation/tools/docstring-coverage.js +78 -0
  39. package/src/agents/documentation/tools/index.js +52 -0
  40. package/src/agents/documentation/tools/readme-freshness.js +61 -0
  41. package/src/agents/envelope/fix-cycle.js +45 -0
  42. package/src/agents/envelope/index.js +31 -0
  43. package/src/agents/envelope/loop.js +150 -0
  44. package/src/agents/envelope/pulse.js +18 -0
  45. package/src/agents/envelope/stream.js +40 -0
  46. package/src/agents/infrastructure/index.js +12 -0
  47. package/src/agents/infrastructure/tools/base.js +171 -0
  48. package/src/agents/infrastructure/tools/checkov-run.js +32 -0
  49. package/src/agents/infrastructure/tools/drift-detect.js +59 -0
  50. package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
  51. package/src/agents/infrastructure/tools/index.js +52 -0
  52. package/src/agents/infrastructure/tools/tflint-run.js +31 -0
  53. package/src/agents/jules/config/definition.js +160 -160
  54. package/src/agents/jules/config/system-prompt.js +182 -182
  55. package/src/agents/jules/error-intake.js +51 -51
  56. package/src/agents/jules/fix-cycle.js +17 -17
  57. package/src/agents/jules/loop.js +460 -450
  58. package/src/agents/jules/pulse.js +10 -10
  59. package/src/agents/jules/stream.js +187 -186
  60. package/src/agents/jules/swarm/file-scanner.js +74 -74
  61. package/src/agents/jules/swarm/index.js +11 -11
  62. package/src/agents/jules/swarm/orchestrator.js +362 -362
  63. package/src/agents/jules/swarm/pattern-hunter.js +123 -123
  64. package/src/agents/jules/swarm/sub-agent.js +315 -309
  65. package/src/agents/jules/tools/aidenid-email.js +189 -189
  66. package/src/agents/jules/tools/auth-audit.js +1708 -1691
  67. package/src/agents/jules/tools/dispatch.js +340 -335
  68. package/src/agents/jules/tools/file-edit.js +2 -2
  69. package/src/agents/jules/tools/file-read.js +2 -2
  70. package/src/agents/jules/tools/frontend-analyze.js +570 -570
  71. package/src/agents/jules/tools/glob.js +2 -2
  72. package/src/agents/jules/tools/grep.js +2 -2
  73. package/src/agents/jules/tools/index.js +29 -29
  74. package/src/agents/jules/tools/path-guards.js +2 -2
  75. package/src/agents/jules/tools/runtime-audit.js +507 -507
  76. package/src/agents/jules/tools/shell.js +2 -2
  77. package/src/agents/jules/tools/url-policy.js +100 -100
  78. package/src/agents/mode.js +113 -0
  79. package/src/agents/observability/index.js +12 -0
  80. package/src/agents/observability/tools/alert-audit.js +39 -0
  81. package/src/agents/observability/tools/base.js +181 -0
  82. package/src/agents/observability/tools/dashboard-gap.js +42 -0
  83. package/src/agents/observability/tools/index.js +54 -0
  84. package/src/agents/observability/tools/log-schema-check.js +74 -0
  85. package/src/agents/observability/tools/span-coverage.js +74 -0
  86. package/src/agents/persona-visuals.js +102 -61
  87. package/src/agents/release/index.js +12 -0
  88. package/src/agents/release/tools/base.js +181 -0
  89. package/src/agents/release/tools/changelog-diff.js +86 -0
  90. package/src/agents/release/tools/feature-flag-audit.js +126 -0
  91. package/src/agents/release/tools/index.js +61 -0
  92. package/src/agents/release/tools/rollback-verify.js +129 -0
  93. package/src/agents/release/tools/semver-check.js +109 -0
  94. package/src/agents/reliability/index.js +12 -0
  95. package/src/agents/reliability/tools/backpressure-check.js +129 -0
  96. package/src/agents/reliability/tools/base.js +181 -0
  97. package/src/agents/reliability/tools/chaos-probe.js +109 -0
  98. package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
  99. package/src/agents/reliability/tools/health-check-audit.js +111 -0
  100. package/src/agents/reliability/tools/index.js +87 -0
  101. package/src/agents/run-persona.js +109 -0
  102. package/src/agents/security/index.js +12 -0
  103. package/src/agents/security/tools/authz-audit.js +134 -0
  104. package/src/agents/security/tools/base.js +190 -0
  105. package/src/agents/security/tools/crypto-review.js +175 -0
  106. package/src/agents/security/tools/index.js +97 -0
  107. package/src/agents/security/tools/sast-scan.js +175 -0
  108. package/src/agents/security/tools/secrets-scan.js +216 -0
  109. package/src/agents/shared-tools/dispatch-core.js +320 -315
  110. package/src/agents/shared-tools/file-edit.js +180 -180
  111. package/src/agents/shared-tools/file-read.js +100 -100
  112. package/src/agents/shared-tools/glob.js +168 -168
  113. package/src/agents/shared-tools/grep.js +228 -228
  114. package/src/agents/shared-tools/index.js +46 -46
  115. package/src/agents/shared-tools/path-guards.js +161 -161
  116. package/src/agents/shared-tools/shell.js +383 -383
  117. package/src/agents/supply-chain/index.js +12 -0
  118. package/src/agents/supply-chain/tools/attestation-check.js +42 -0
  119. package/src/agents/supply-chain/tools/base.js +151 -0
  120. package/src/agents/supply-chain/tools/index.js +52 -0
  121. package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
  122. package/src/agents/supply-chain/tools/package-verify.js +56 -0
  123. package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
  124. package/src/agents/testing/index.js +12 -0
  125. package/src/agents/testing/tools/base.js +202 -0
  126. package/src/agents/testing/tools/coverage-gap.js +144 -0
  127. package/src/agents/testing/tools/flake-detect.js +125 -0
  128. package/src/agents/testing/tools/index.js +85 -0
  129. package/src/agents/testing/tools/mutation-test.js +143 -0
  130. package/src/agents/testing/tools/snapshot-diff.js +103 -0
  131. package/src/ai/aidenid.js +1021 -1009
  132. package/src/ai/client.js +553 -553
  133. package/src/ai/domain-target-store.js +268 -268
  134. package/src/ai/identity-store.js +270 -270
  135. package/src/ai/proxy.js +137 -137
  136. package/src/ai/site-store.js +145 -145
  137. package/src/audit/agents/architecture.js +180 -180
  138. package/src/audit/agents/compliance.js +179 -179
  139. package/src/audit/agents/documentation.js +165 -165
  140. package/src/audit/agents/performance.js +145 -145
  141. package/src/audit/agents/security.js +215 -215
  142. package/src/audit/agents/testing.js +172 -172
  143. package/src/audit/orchestrator.js +557 -557
  144. package/src/audit/package.js +204 -204
  145. package/src/audit/registry.js +284 -284
  146. package/src/audit/replay.js +103 -103
  147. package/src/auth/gate.js +428 -371
  148. package/src/auth/http.js +681 -611
  149. package/src/auth/service.js +1106 -1106
  150. package/src/auth/session-store.js +813 -813
  151. package/src/cli.js +257 -252
  152. package/src/commands/ai/identity-lifecycle.js +1338 -1338
  153. package/src/commands/ai/provision-governance.js +1272 -1272
  154. package/src/commands/ai/shared.js +147 -147
  155. package/src/commands/ai.js +11 -11
  156. package/src/commands/apply.js +12 -12
  157. package/src/commands/audit.js +1171 -1166
  158. package/src/commands/auth.js +419 -419
  159. package/src/commands/chat.js +184 -191
  160. package/src/commands/config.js +184 -184
  161. package/src/commands/cost.js +311 -311
  162. package/src/commands/daemon/core.js +850 -850
  163. package/src/commands/daemon/extended.js +1048 -1048
  164. package/src/commands/daemon/shared.js +213 -213
  165. package/src/commands/daemon.js +11 -11
  166. package/src/commands/guide.js +174 -174
  167. package/src/commands/ingest.js +58 -58
  168. package/src/commands/init.js +55 -55
  169. package/src/commands/legacy-args.js +20 -10
  170. package/src/commands/mcp.js +461 -461
  171. package/src/commands/omargate.js +63 -29
  172. package/src/commands/persona.js +65 -20
  173. package/src/commands/plugin.js +260 -260
  174. package/src/commands/policy.js +132 -132
  175. package/src/commands/prompt.js +238 -238
  176. package/src/commands/review.js +704 -704
  177. package/src/commands/scan.js +865 -872
  178. package/src/commands/session.js +1238 -0
  179. package/src/commands/spec.js +771 -716
  180. package/src/commands/swarm.js +651 -651
  181. package/src/commands/telemetry.js +202 -202
  182. package/src/commands/watch.js +511 -511
  183. package/src/config/agent-dictionary.js +182 -182
  184. package/src/config/io.js +56 -56
  185. package/src/config/paths.js +18 -18
  186. package/src/config/schema.js +55 -55
  187. package/src/config/service.js +184 -184
  188. package/src/coord/events-log.js +141 -0
  189. package/src/coord/handshake.js +719 -0
  190. package/src/coord/index.js +35 -0
  191. package/src/coord/paths.js +84 -0
  192. package/src/coord/priority.js +62 -0
  193. package/src/coord/tarjan.js +157 -0
  194. package/src/cost/budget.js +235 -235
  195. package/src/cost/history.js +188 -188
  196. package/src/cost/tokenizer.js +160 -0
  197. package/src/cost/tracker.js +232 -171
  198. package/src/daemon/artifact-lineage.js +896 -534
  199. package/src/daemon/assignment-ledger.js +1083 -770
  200. package/src/daemon/ast-drift.js +496 -0
  201. package/src/daemon/ast-parser-layer.js +258 -258
  202. package/src/daemon/budget-governor.js +633 -633
  203. package/src/daemon/callgraph-overlay.js +646 -646
  204. package/src/daemon/error-worker.js +1209 -626
  205. package/src/daemon/fix-cycle.js +384 -377
  206. package/src/daemon/hybrid-mapper.js +929 -929
  207. package/src/daemon/ingest-refresh.js +79 -11
  208. package/src/daemon/jira-lifecycle.js +767 -632
  209. package/src/daemon/operator-control.js +657 -657
  210. package/src/daemon/pulse.js +327 -327
  211. package/src/daemon/reliability-lane.js +471 -471
  212. package/src/daemon/scope-engine.js +1068 -0
  213. package/src/daemon/watchdog.js +971 -971
  214. package/src/events/schema.js +190 -0
  215. package/src/guide/generator.js +316 -316
  216. package/src/ingest/engine.js +933 -918
  217. package/src/ingest/ownership.js +380 -0
  218. package/src/interactive/index.js +97 -97
  219. package/src/legacy-cli.js +3228 -2994
  220. package/src/mcp/registry.js +695 -695
  221. package/src/memory/blackboard.js +301 -301
  222. package/src/memory/retrieval.js +581 -581
  223. package/src/orchestrator/kai-chen.js +126 -0
  224. package/src/plugin/manifest.js +553 -553
  225. package/src/policy/packs.js +144 -144
  226. package/src/prompt/generator.js +136 -118
  227. package/src/review/ai-review.js +672 -679
  228. package/src/review/compliance-pack.js +389 -0
  229. package/src/review/investor-dd-config.js +54 -0
  230. package/src/review/investor-dd-file-loop.js +303 -0
  231. package/src/review/investor-dd-file-router.js +406 -0
  232. package/src/review/investor-dd-html-report.js +233 -0
  233. package/src/review/investor-dd-notification.js +120 -0
  234. package/src/review/investor-dd-orchestrator.js +405 -0
  235. package/src/review/investor-dd-persona-runner.js +275 -0
  236. package/src/review/live-validator.js +253 -0
  237. package/src/review/local-review.js +1351 -1305
  238. package/src/review/omargate-interactive.js +68 -68
  239. package/src/review/omargate-orchestrator.js +492 -300
  240. package/src/review/persona-prompts.js +484 -296
  241. package/src/review/reconciliation-rules.js +329 -0
  242. package/src/review/replay.js +235 -235
  243. package/src/review/report.js +664 -664
  244. package/src/review/reproducibility-chain.js +136 -0
  245. package/src/review/scan-modes.js +147 -42
  246. package/src/review/spec-binding.js +487 -487
  247. package/src/scaffold/generator.js +67 -67
  248. package/src/scaffold/templates.js +150 -150
  249. package/src/scan/generator.js +418 -418
  250. package/src/scan/gh-secrets.js +107 -107
  251. package/src/session/agent-registry.js +359 -0
  252. package/src/session/analytics.js +479 -0
  253. package/src/session/daemon.js +1396 -0
  254. package/src/session/file-locks.js +666 -0
  255. package/src/session/paths.js +37 -0
  256. package/src/session/recap.js +567 -0
  257. package/src/session/redact.js +82 -0
  258. package/src/session/runtime-bridge.js +762 -0
  259. package/src/session/scoring.js +406 -0
  260. package/src/session/setup-guides.js +304 -0
  261. package/src/session/store.js +704 -0
  262. package/src/session/stream.js +333 -0
  263. package/src/session/sync.js +753 -0
  264. package/src/session/tasks.js +1054 -0
  265. package/src/session/templates.js +188 -0
  266. package/src/spec/generator.js +619 -519
  267. package/src/spec/regenerate.js +237 -237
  268. package/src/spec/templates.js +91 -91
  269. package/src/swarm/dashboard.js +247 -247
  270. package/src/swarm/factory.js +363 -363
  271. package/src/swarm/pentest.js +934 -934
  272. package/src/swarm/registry.js +419 -419
  273. package/src/swarm/report.js +158 -158
  274. package/src/swarm/runtime.js +569 -576
  275. package/src/swarm/scenario-dsl.js +272 -272
  276. package/src/telemetry/ledger.js +302 -302
  277. package/src/telemetry/session-tracker.js +234 -234
  278. package/src/telemetry/sync.js +203 -203
  279. package/src/ui/command-hints.js +13 -13
  280. 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 };
@@ -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
+ }