sentinelayer-cli 0.8.0 → 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 (153) hide show
  1. package/README.md +13 -0
  2. package/package.json +4 -4
  3. package/src/agents/ai-governance/index.js +12 -0
  4. package/src/agents/ai-governance/tools/base.js +171 -0
  5. package/src/agents/ai-governance/tools/eval-regression.js +47 -0
  6. package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
  7. package/src/agents/ai-governance/tools/index.js +52 -0
  8. package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
  9. package/src/agents/ai-governance/tools/provenance-check.js +69 -0
  10. package/src/agents/backend/index.js +12 -0
  11. package/src/agents/backend/tools/base.js +189 -0
  12. package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
  13. package/src/agents/backend/tools/idempotency-audit.js +105 -0
  14. package/src/agents/backend/tools/index.js +87 -0
  15. package/src/agents/backend/tools/retry-audit.js +132 -0
  16. package/src/agents/backend/tools/timeout-audit.js +144 -0
  17. package/src/agents/code-quality/index.js +12 -0
  18. package/src/agents/code-quality/tools/base.js +159 -0
  19. package/src/agents/code-quality/tools/complexity-measure.js +197 -0
  20. package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
  21. package/src/agents/code-quality/tools/cycle-detect.js +49 -0
  22. package/src/agents/code-quality/tools/dep-graph.js +196 -0
  23. package/src/agents/code-quality/tools/index.js +89 -0
  24. package/src/agents/data-layer/index.js +12 -0
  25. package/src/agents/data-layer/tools/base.js +181 -0
  26. package/src/agents/data-layer/tools/index-audit.js +165 -0
  27. package/src/agents/data-layer/tools/index.js +83 -0
  28. package/src/agents/data-layer/tools/migration-scan.js +135 -0
  29. package/src/agents/data-layer/tools/query-explain.js +120 -0
  30. package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
  31. package/src/agents/documentation/index.js +12 -0
  32. package/src/agents/documentation/tools/api-diff.js +91 -0
  33. package/src/agents/documentation/tools/base.js +151 -0
  34. package/src/agents/documentation/tools/dead-link-check.js +58 -0
  35. package/src/agents/documentation/tools/docstring-coverage.js +78 -0
  36. package/src/agents/documentation/tools/index.js +52 -0
  37. package/src/agents/documentation/tools/readme-freshness.js +61 -0
  38. package/src/agents/envelope/fix-cycle.js +45 -0
  39. package/src/agents/envelope/index.js +31 -0
  40. package/src/agents/envelope/loop.js +150 -0
  41. package/src/agents/envelope/pulse.js +18 -0
  42. package/src/agents/envelope/stream.js +40 -0
  43. package/src/agents/infrastructure/index.js +12 -0
  44. package/src/agents/infrastructure/tools/base.js +171 -0
  45. package/src/agents/infrastructure/tools/checkov-run.js +32 -0
  46. package/src/agents/infrastructure/tools/drift-detect.js +59 -0
  47. package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
  48. package/src/agents/infrastructure/tools/index.js +52 -0
  49. package/src/agents/infrastructure/tools/tflint-run.js +31 -0
  50. package/src/agents/jules/loop.js +7 -4
  51. package/src/agents/jules/swarm/sub-agent.js +5 -1
  52. package/src/agents/jules/tools/auth-audit.js +10 -1
  53. package/src/agents/mode.js +113 -0
  54. package/src/agents/observability/index.js +12 -0
  55. package/src/agents/observability/tools/alert-audit.js +39 -0
  56. package/src/agents/observability/tools/base.js +181 -0
  57. package/src/agents/observability/tools/dashboard-gap.js +42 -0
  58. package/src/agents/observability/tools/index.js +54 -0
  59. package/src/agents/observability/tools/log-schema-check.js +74 -0
  60. package/src/agents/observability/tools/span-coverage.js +74 -0
  61. package/src/agents/persona-visuals.js +38 -0
  62. package/src/agents/release/index.js +12 -0
  63. package/src/agents/release/tools/base.js +181 -0
  64. package/src/agents/release/tools/changelog-diff.js +86 -0
  65. package/src/agents/release/tools/feature-flag-audit.js +126 -0
  66. package/src/agents/release/tools/index.js +61 -0
  67. package/src/agents/release/tools/rollback-verify.js +129 -0
  68. package/src/agents/release/tools/semver-check.js +109 -0
  69. package/src/agents/reliability/index.js +12 -0
  70. package/src/agents/reliability/tools/backpressure-check.js +129 -0
  71. package/src/agents/reliability/tools/base.js +181 -0
  72. package/src/agents/reliability/tools/chaos-probe.js +109 -0
  73. package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
  74. package/src/agents/reliability/tools/health-check-audit.js +111 -0
  75. package/src/agents/reliability/tools/index.js +87 -0
  76. package/src/agents/run-persona.js +109 -0
  77. package/src/agents/security/index.js +12 -0
  78. package/src/agents/security/tools/authz-audit.js +134 -0
  79. package/src/agents/security/tools/base.js +190 -0
  80. package/src/agents/security/tools/crypto-review.js +175 -0
  81. package/src/agents/security/tools/index.js +97 -0
  82. package/src/agents/security/tools/sast-scan.js +175 -0
  83. package/src/agents/security/tools/secrets-scan.js +216 -0
  84. package/src/agents/supply-chain/index.js +12 -0
  85. package/src/agents/supply-chain/tools/attestation-check.js +42 -0
  86. package/src/agents/supply-chain/tools/base.js +151 -0
  87. package/src/agents/supply-chain/tools/index.js +52 -0
  88. package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
  89. package/src/agents/supply-chain/tools/package-verify.js +56 -0
  90. package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
  91. package/src/agents/testing/index.js +12 -0
  92. package/src/agents/testing/tools/base.js +202 -0
  93. package/src/agents/testing/tools/coverage-gap.js +144 -0
  94. package/src/agents/testing/tools/flake-detect.js +125 -0
  95. package/src/agents/testing/tools/index.js +85 -0
  96. package/src/agents/testing/tools/mutation-test.js +143 -0
  97. package/src/agents/testing/tools/snapshot-diff.js +103 -0
  98. package/src/auth/gate.js +65 -37
  99. package/src/cli.js +1 -1
  100. package/src/commands/chat.js +3 -10
  101. package/src/commands/legacy-args.js +10 -0
  102. package/src/commands/omargate.js +36 -2
  103. package/src/commands/persona.js +46 -1
  104. package/src/commands/scan.js +3 -10
  105. package/src/commands/session.js +654 -6
  106. package/src/commands/spec.js +3 -10
  107. package/src/coord/events-log.js +141 -0
  108. package/src/coord/handshake.js +719 -0
  109. package/src/coord/index.js +35 -0
  110. package/src/coord/paths.js +84 -0
  111. package/src/coord/priority.js +62 -0
  112. package/src/coord/tarjan.js +157 -0
  113. package/src/cost/tokenizer.js +160 -0
  114. package/src/cost/tracker.js +61 -0
  115. package/src/daemon/artifact-lineage.js +362 -0
  116. package/src/daemon/assignment-ledger.js +117 -0
  117. package/src/daemon/ast-drift.js +496 -0
  118. package/src/daemon/ingest-refresh.js +69 -2
  119. package/src/ingest/engine.js +15 -0
  120. package/src/ingest/ownership.js +380 -0
  121. package/src/legacy-cli.js +68 -1
  122. package/src/orchestrator/kai-chen.js +126 -0
  123. package/src/review/ai-review.js +3 -10
  124. package/src/review/compliance-pack.js +389 -0
  125. package/src/review/investor-dd-config.js +54 -0
  126. package/src/review/investor-dd-file-loop.js +303 -0
  127. package/src/review/investor-dd-file-router.js +406 -0
  128. package/src/review/investor-dd-html-report.js +233 -0
  129. package/src/review/investor-dd-notification.js +120 -0
  130. package/src/review/investor-dd-orchestrator.js +405 -0
  131. package/src/review/investor-dd-persona-runner.js +275 -0
  132. package/src/review/live-validator.js +253 -0
  133. package/src/review/omargate-orchestrator.js +90 -2
  134. package/src/review/persona-prompts.js +244 -56
  135. package/src/review/reconciliation-rules.js +329 -0
  136. package/src/review/reproducibility-chain.js +136 -0
  137. package/src/review/scan-modes.js +102 -3
  138. package/src/session/agent-registry.js +7 -0
  139. package/src/session/analytics.js +479 -0
  140. package/src/session/daemon.js +609 -14
  141. package/src/session/file-locks.js +666 -0
  142. package/src/session/paths.js +4 -0
  143. package/src/session/recap.js +567 -0
  144. package/src/session/redact.js +82 -0
  145. package/src/session/runtime-bridge.js +24 -1
  146. package/src/session/scoring.js +406 -0
  147. package/src/session/setup-guides.js +304 -0
  148. package/src/session/store.js +318 -2
  149. package/src/session/stream.js +9 -1
  150. package/src/session/sync.js +753 -0
  151. package/src/session/tasks.js +1054 -0
  152. package/src/session/templates.js +188 -0
  153. package/src/swarm/runtime.js +1 -8
@@ -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/legacy-cli.js CHANGED
@@ -206,12 +206,23 @@ function printUsage() {
206
206
  console.log("");
207
207
  console.log("Session Coordination:");
208
208
  console.log(" sl session start --json Create an agent coordination session");
209
+ console.log(" sl session start --template code-review Start from quick-start preset + launch plan");
210
+ console.log(" sl session templates --json List available session quick-start templates");
209
211
  console.log(" sl session join <id> --name <n> Join a session as an agent");
210
212
  console.log(" sl session say <id> \"msg\" --json Append a message event to session stream");
213
+ console.log(" sl session say <id> \"lock: <file> - <intent>\" Request an exclusive file lock via Senti");
214
+ console.log(" sl session say <id> \"assign: @agent <task>\" Create task assignment + lease");
215
+ console.log(" sl session say <id> \"assign: @*:reviewer <task>\" Wildcard route to least-busy role");
216
+ console.log(" sl session say <id> \"accepted: task <task-id>\" / \"done: task <task-id>\" Task transitions");
211
217
  console.log(" sl session read <id> --tail 20 Read session stream events");
212
218
  console.log(" sl session status <id> --json Show session health, agents, runs, leases");
213
219
  console.log(" sl session leave <id> Leave a session");
214
220
  console.log(" sl session list --json List active sessions");
221
+ console.log(" sl session setup-guides <id> --json Upsert AGENTS.md/CLAUDE.md coordination section");
222
+ console.log(" sl session inject-guide <id> --json Inject section into existing AGENTS.md/CLAUDE.md files");
223
+ console.log(" sl session provision-emails <id> --count 5 Provision AIdenID emails for swarm testing");
224
+ console.log(" sl session admin-kill <id> --reason <reason> Admin kill one remote session");
225
+ console.log(" sl session admin-kill-all --confirm Admin kill ALL remote sessions");
215
226
  console.log(" sl session kill --session <id> --agent <id> Kill agent + revoke active leases");
216
227
  console.log("");
217
228
  console.log("Security & Review:");
@@ -1099,8 +1110,54 @@ function buildOmarTerminalHandler({ startedAt = Date.now() } = {}) {
1099
1110
  async function runLocalOmarGateCommand(args) {
1100
1111
  const commandStartedAt = Date.now();
1101
1112
  const mode = String(args[0] || "").trim().toLowerCase();
1113
+ if (mode === "investor-dd") {
1114
+ const pathArg = getCommandOptionValue(args, "--path") || ".";
1115
+ const outputDirArg = getCommandOptionValue(args, "--output-dir") || "";
1116
+ const asJson = hasCommandOption(args, "--json");
1117
+ const dryRun = hasCommandOption(args, "--dry-run");
1118
+ const maxCostUsd = parseFloat(getCommandOptionValue(args, "--max-cost") || "25.0") || 25.0;
1119
+ const maxRuntimeMinutes =
1120
+ parseInt(getCommandOptionValue(args, "--max-runtime-minutes") || "45", 10) || 45;
1121
+ const maxParallel =
1122
+ parseInt(getCommandOptionValue(args, "--max-parallel") || "3", 10) || 3;
1123
+ const streamEnabled = hasCommandOption(args, "--stream");
1124
+
1125
+ const targetPath = path.resolve(process.cwd(), pathArg);
1126
+ if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
1127
+ throw new Error(`Invalid --path target: ${targetPath}`);
1128
+ }
1129
+
1130
+ const { runInvestorDd } = await import("./review/investor-dd-orchestrator.js");
1131
+ if (!asJson) {
1132
+ printSection("Investor-DD Audit");
1133
+ printInfo(`Target: ${targetPath}`);
1134
+ printInfo(
1135
+ `Budget: $${maxCostUsd.toFixed(2)} / ${maxRuntimeMinutes}min / ${maxParallel} parallel`,
1136
+ );
1137
+ if (dryRun) printInfo("Mode: dry-run (plan + stub report only)");
1138
+ }
1139
+ const result = await runInvestorDd({
1140
+ rootPath: targetPath,
1141
+ outputDir: outputDirArg,
1142
+ budgetOptions: { maxCostUsd, maxRuntimeMinutes, maxParallel },
1143
+ dryRun,
1144
+ onEvent: streamEnabled
1145
+ ? (event) => process.stdout.write(`${JSON.stringify(event)}\n`)
1146
+ : () => {},
1147
+ });
1148
+ if (asJson) {
1149
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
1150
+ } else {
1151
+ printInfo(`Report: ${path.join(result.artifactDir, "report.md")}`);
1152
+ printInfo(`Artifacts: ${result.artifactDir}`);
1153
+ printInfo(`Status: ${result.summary.terminationReason}`);
1154
+ printInfo(`Findings: ${result.summary.totalFindings}`);
1155
+ printInfo(`Elapsed: ${result.summary.durationSeconds.toFixed(1)}s`);
1156
+ }
1157
+ return;
1158
+ }
1102
1159
  if (mode && mode !== "deep") {
1103
- throw new Error(`Unsupported /omargate mode '${mode}'. Use: /omargate deep`);
1160
+ throw new Error(`Unsupported /omargate mode '${mode}'. Use: /omargate deep | /omargate investor-dd`);
1104
1161
  }
1105
1162
  const asJson = hasCommandOption(args, "--json");
1106
1163
  const pathArg = getCommandOptionValue(args, "--path") || ".";
@@ -1113,6 +1170,14 @@ async function runLocalOmarGateCommand(args) {
1113
1170
  const scanMode = getCommandOptionValue(args, "--scan-mode") || "deep";
1114
1171
  const maxParallel = parseInt(getCommandOptionValue(args, "--max-parallel") || "4", 10) || 4;
1115
1172
  const streamEnabled = hasCommandOption(args, "--stream");
1173
+ // Per-persona filter flags (A-CLI-1). --persona <csv> narrows the dispatch
1174
+ // roster to the listed IDs; --skip-persona <csv> removes listed IDs from
1175
+ // whatever the mode's baseline roster is. Both can be combined.
1176
+ const personaCsvFlag = getCommandOptionValue(args, "--persona") || "";
1177
+ const skipPersonaCsvFlag = getCommandOptionValue(args, "--skip-persona") || "";
1178
+ const { parsePersonaCsv } = await import("./review/scan-modes.js");
1179
+ const includeOnly = parsePersonaCsv(personaCsvFlag);
1180
+ const skipPersonas = parsePersonaCsv(skipPersonaCsvFlag);
1116
1181
  const targetPath = path.resolve(process.cwd(), pathArg);
1117
1182
  if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
1118
1183
  throw new Error(`Invalid --path target: ${targetPath}`);
@@ -1175,6 +1240,8 @@ async function runLocalOmarGateCommand(args) {
1175
1240
  metadata: deterministic.metadata || {},
1176
1241
  },
1177
1242
  onEvent: streamHandler,
1243
+ includeOnly: includeOnly.length > 0 ? includeOnly : null,
1244
+ skipPersonas: skipPersonas.length > 0 ? skipPersonas : null,
1178
1245
  });
1179
1246
 
1180
1247
  // Use orchestrator results as the AI layer. aiResult represents ONLY
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Dr. Kai Chen — Global Orchestrator (a.k.a. "Senti" / Telegram coordinator).
3
+ *
4
+ * Kai Chen is NOT a review persona; he is the orchestration tier that sits
5
+ * above the 13 domain reviewers (Nina, Maya, Ethan, Priya, Linh, Jules,
6
+ * Samir, Noah, Omar, Sofia, Kat, Nora, Amina). He picks which personas run,
7
+ * routes high-signal findings up to the user, and emits the final report.
8
+ *
9
+ * Background (Carter's canon):
10
+ * - Ex-Google Staff; Chrome V8 performance lead
11
+ * - Bias: performance budgets, operational simplicity, correctness over
12
+ * cleverness
13
+ * - Tone: crisp, evidence-first; hates vague claims; demands reproduction
14
+ * steps
15
+ * - Output signature: "Here's what breaks, where, why, and what to do next."
16
+ *
17
+ * Model routing:
18
+ * - Primary: Opus 4.6 (reasoning-heavy orchestration; called ~1-3 times
19
+ * per scan or build)
20
+ * - NEVER OpenAI gpt-5.3-codex (code-gen workers only)
21
+ * - NEVER Gemini (dropped from provider fallback order)
22
+ *
23
+ * This module exports the orchestrator DEFINITION + a prompt-assembly helper.
24
+ * Wiring Kai into actual review/build flows happens in subsequent PRs (the
25
+ * gate dispatcher, the Telegram entry-point, and the build-pathway planner
26
+ * all consume this definition).
27
+ */
28
+
29
+ const KAI_CHEN_BIAS = Object.freeze([
30
+ "performance budgets over premature optimization",
31
+ "operational simplicity over cleverness",
32
+ "correctness over features",
33
+ "evidence over vague claims",
34
+ "reproduction steps for every issue",
35
+ ]);
36
+
37
+ const KAI_CHEN_TONE_RULES = Object.freeze([
38
+ "crisp sentences; no hedging",
39
+ "evidence-first; cite file:line or metric name on every claim",
40
+ "demand reproduction steps before accepting any finding as actionable",
41
+ "reject reviewer output that is vague, speculative, or missing coverage proof",
42
+ "call out 'looks fine' conclusions that aren't backed by enumerated checklist coverage",
43
+ ]);
44
+
45
+ const KAI_CHEN_OUTPUT_SIGNATURE = "Here's what breaks, where, why, and what to do next.";
46
+
47
+ const KAI_CHEN_SYSTEM_PROMPT = [
48
+ "You are Dr. Kai Chen, global orchestrator for the Sentinelayer review platform.",
49
+ "",
50
+ "Your job is NOT to review code directly. Your job is to:",
51
+ " 1. Pick which specialist personas should run against this target and why.",
52
+ " 2. Receive the specialists' findings + coverage enumerations.",
53
+ " 3. Deduplicate across personas (same file:line across domains boosts confidence, not noise).",
54
+ " 4. Rank by severity × confidence × blast radius.",
55
+ " 5. Emit a single consolidated report using your output signature.",
56
+ "",
57
+ "Non-negotiables:",
58
+ " - Every finding you surface to the user MUST have an enumerated reproduction path.",
59
+ " - If a specialist returned zero findings, the specialist MUST have enumerated their checklist coverage; if they did not, you reject their output and re-dispatch.",
60
+ " - You do not pad reports with speculative or 'theoretical' concerns. Cut them at the orchestrator tier.",
61
+ " - You are a performance-focused reviewer by training. Favor operational simplicity over cleverness in your recommendations.",
62
+ "",
63
+ "Output signature (end every summary with this exact phrasing, populated):",
64
+ ` "${KAI_CHEN_OUTPUT_SIGNATURE}"`,
65
+ "",
66
+ "Your tone rules:",
67
+ ...KAI_CHEN_TONE_RULES.map((rule) => ` - ${rule}`),
68
+ ].join("\n");
69
+
70
+ export const ORCHESTRATOR_DEFINITION = Object.freeze({
71
+ id: "orchestrator-kai-chen",
72
+ name: "Dr. Kai Chen",
73
+ shortName: "Kai",
74
+ role: "Global Orchestrator / Senti",
75
+ background: "Ex-Google Staff; Chrome V8 performance lead",
76
+ model: "claude-opus-4-6",
77
+ modelProvider: "anthropic",
78
+ bias: KAI_CHEN_BIAS,
79
+ toneRules: KAI_CHEN_TONE_RULES,
80
+ outputSignature: KAI_CHEN_OUTPUT_SIGNATURE,
81
+ systemPrompt: KAI_CHEN_SYSTEM_PROMPT,
82
+ });
83
+
84
+ /**
85
+ * Build a context-enriched orchestrator prompt for a specific scan/build run.
86
+ *
87
+ * @param {object} [options]
88
+ * @param {string} [options.targetPath] - Repository path under review.
89
+ * @param {string} [options.mode] - e.g. "baseline" | "deep" | "full-depth" | "build".
90
+ * @param {string[]} [options.dispatchedPersonas] - Persona IDs dispatched for this run.
91
+ * @param {object} [options.deterministicSummary] - Pre-LLM deterministic scan summary.
92
+ * @returns {string} Assembled orchestrator system prompt.
93
+ */
94
+ export function buildOrchestratorPrompt({
95
+ targetPath = "",
96
+ mode = "deep",
97
+ dispatchedPersonas = [],
98
+ deterministicSummary = {},
99
+ } = {}) {
100
+ const personaList = dispatchedPersonas.length > 0
101
+ ? dispatchedPersonas.map((id) => ` - ${id}`).join("\n")
102
+ : " (none specified)";
103
+
104
+ const detSummary = [
105
+ `P0=${deterministicSummary.P0 || 0}`,
106
+ `P1=${deterministicSummary.P1 || 0}`,
107
+ `P2=${deterministicSummary.P2 || 0}`,
108
+ `P3=${deterministicSummary.P3 || 0}`,
109
+ ].join(" ");
110
+
111
+ return [
112
+ ORCHESTRATOR_DEFINITION.systemPrompt,
113
+ "",
114
+ "## Run context",
115
+ `Target: ${targetPath || "(not provided)"}`,
116
+ `Mode: ${mode}`,
117
+ `Deterministic-scan summary (already surfaced, do NOT re-report): ${detSummary}`,
118
+ "",
119
+ "## Specialists dispatched for this run",
120
+ personaList,
121
+ "",
122
+ "Begin.",
123
+ ].join("\n");
124
+ }
125
+
126
+ export const KAI_CHEN_OUTPUT_SIGNATURE_VALUE = KAI_CHEN_OUTPUT_SIGNATURE;
@@ -10,6 +10,7 @@ import { loadConfig } from "../config/service.js";
10
10
  import { evaluateBudget } from "../cost/budget.js";
11
11
  import { appendCostEntry, summarizeCostHistory } from "../cost/history.js";
12
12
  import { estimateModelCost } from "../cost/tracker.js";
13
+ import { estimateTokens } from "../cost/tokenizer.js";
13
14
  import { appendRunEvent, deriveStopClassFromBudget } from "../telemetry/ledger.js";
14
15
 
15
16
  const AI_SEVERITIES = new Set(["P0", "P1", "P2", "P3"]);
@@ -36,14 +37,6 @@ function parsePercent(rawValue, field) {
36
37
  return normalized;
37
38
  }
38
39
 
39
- function estimateTokenCount(text) {
40
- const normalized = String(text || "");
41
- if (!normalized) {
42
- return 0;
43
- }
44
- return Math.max(1, Math.ceil(normalized.length / 4));
45
- }
46
-
47
40
  function resolveConfiguredApiKey(provider, resolvedConfig = {}) {
48
41
  const normalizedProvider = normalizeString(provider).toLowerCase();
49
42
  if (normalizedProvider === "openai") {
@@ -479,8 +472,8 @@ export async function runAiReviewLayer({
479
472
  };
480
473
  combinedSummary.blocking = combinedSummary.P0 > 0 || combinedSummary.P1 > 0;
481
474
 
482
- const inputTokens = estimateTokenCount(prompt);
483
- const outputTokens = estimateTokenCount(responseText);
475
+ const inputTokens = estimateTokens(prompt, { model: resolvedModel });
476
+ const outputTokens = estimateTokens(responseText, { model: resolvedModel });
484
477
  const modelCost = maybeEstimateModelCost({
485
478
  modelId: resolvedModel,
486
479
  inputTokens,