sentinelayer-cli 0.25.0 → 0.25.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,21 +41,9 @@ import { renderInvestorDdHtml } from "./investor-dd-html-report.js";
41
41
  import { runDevTestBotPhase } from "./investor-dd-devtestbot.js";
42
42
  import { redactDdEmailError } from "./dd-report-email-client.js";
43
43
  import { buildInvestorDdProgress } from "./investor-dd-progress.js";
44
+ import { FULL_DEPTH_PERSONAS } from "./scan-modes.js";
44
45
 
45
- const INVESTOR_DD_PERSONAS = Object.freeze([
46
- "security",
47
- "backend",
48
- "code-quality",
49
- "testing",
50
- "data-layer",
51
- "reliability",
52
- "release",
53
- "observability",
54
- "infrastructure",
55
- "supply-chain",
56
- "documentation",
57
- "ai-governance",
58
- ]);
46
+ const INVESTOR_DD_PERSONAS = Object.freeze([...FULL_DEPTH_PERSONAS]);
59
47
 
60
48
  /**
61
49
  * Walk the target repo and return a list of relative POSIX file paths.
@@ -148,7 +136,10 @@ function buildSummaryMarkdown({ runId, summary, routing, byPersona }) {
148
136
  lines.push("");
149
137
  lines.push("| Persona | Files routed | Files visited | Findings |");
150
138
  lines.push("|---|---:|---:|---:|");
151
- for (const personaId of INVESTOR_DD_PERSONAS) {
139
+ const reportPersonas = Array.isArray(summary.personas) && summary.personas.length > 0
140
+ ? summary.personas
141
+ : INVESTOR_DD_PERSONAS;
142
+ for (const personaId of reportPersonas) {
152
143
  const record = byPersona[personaId] || {};
153
144
  const routed = (routing[personaId] || []).length;
154
145
  const visited = Array.isArray(record.visited) ? record.visited.length : 0;
@@ -264,7 +255,7 @@ async function triggerReportEmail({ reportEmail, runResult, dryRun, emit }) {
264
255
  * @param {string} params.rootPath
265
256
  * @param {string} [params.outputDir] - Defaults to `<rootPath>/.sentinelayer/runs/<runId>`.
266
257
  * @param {object} [params.budgetOptions] - Overrides from CLI: { maxUsd, maxRuntimeMinutes, maxParallel }.
267
- * @param {string[]} [params.personas] - Override persona list; defaults to all 12.
258
+ * @param {string[]} [params.personas] - Override persona list; defaults to all 13.
268
259
  * @param {Function} [params.onEvent] - Extra event sink (NDJSON stream is always written).
269
260
  * @param {boolean} [params.dryRun] - If true, skip tool execution, emit plan.json + stub report only.
270
261
  * @param {string[]|null} [params.compliancePacks] - Compliance pack IDs to run (default: all seven).
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Unified per-persona runner for investor-DD (#investor-dd-4..15).
3
3
  *
4
- * Wires the 12 domain personas (security, backend, code-quality, testing,
4
+ * Wires the 13 domain personas (security, backend, code-quality, testing,
5
5
  * data-layer, reliability, release, observability, infrastructure,
6
- * supply-chain, documentation, ai-governance) into the per-file review
6
+ * supply-chain, frontend, documentation, ai-governance) into the review
7
7
  * loop. Each persona dispatches its declared domain tools against every
8
8
  * file the router assigns to it, collects findings, and returns a
9
9
  * per-persona coverage proof.
@@ -12,9 +12,8 @@
12
12
  * (LLM-driven) layer sits on top in a later PR. Determinism is the
13
13
  * investor-DD ground-truth floor; the LLM layer is additive.
14
14
  *
15
- * Frontend (Jules) is excluded here because Jules has its own bespoke
16
- * envelope with live-web validation; PR-25 routes Jules into investor-DD
17
- * via a dedicated live-validator dispatcher.
15
+ * Repo-scoped tools such as Jules' frontend analyzer run once per persona
16
+ * with the full routed file set; file-scoped tools retain the per-file loop.
18
17
  */
19
18
 
20
19
  import { AI_GOVERNANCE_TOOLS } from "../agents/ai-governance/tools/index.js";
@@ -29,11 +28,12 @@ import { RELIABILITY_TOOLS } from "../agents/reliability/tools/index.js";
29
28
  import { SECURITY_TOOLS } from "../agents/security/tools/index.js";
30
29
  import { SUPPLY_CHAIN_TOOLS } from "../agents/supply-chain/tools/index.js";
31
30
  import { TESTING_TOOLS } from "../agents/testing/tools/index.js";
31
+ import { FRONTEND_TOOLS } from "../agents/jules/tools/investor-dd.js";
32
32
 
33
33
  import { checkBudget, createBudgetState } from "./investor-dd-file-loop.js";
34
34
 
35
35
  /**
36
- * Registry of persona → tool map. Frontend handled separately via Jules.
36
+ * Registry of persona → tool map.
37
37
  */
38
38
  export const INVESTOR_DD_PERSONA_TOOL_REGISTRY = Object.freeze({
39
39
  security: SECURITY_TOOLS,
@@ -46,6 +46,7 @@ export const INVESTOR_DD_PERSONA_TOOL_REGISTRY = Object.freeze({
46
46
  observability: OBSERVABILITY_TOOLS,
47
47
  infrastructure: INFRASTRUCTURE_TOOLS,
48
48
  "supply-chain": SUPPLY_CHAIN_TOOLS,
49
+ frontend: FRONTEND_TOOLS,
49
50
  documentation: DOCUMENTATION_TOOLS,
50
51
  "ai-governance": AI_GOVERNANCE_TOOLS,
51
52
  });
@@ -66,31 +67,15 @@ export function getPersonaTools(personaId) {
66
67
  return Object.values(map);
67
68
  }
68
69
 
69
- /**
70
- * Dispatch every tool for the persona against a single file scope. Each
71
- * tool handler is invoked with `{ rootPath, files: [file] }` because
72
- * every persona's tool contract accepts this shape (see #A13-#A22).
73
- *
74
- * @param {object} params
75
- * @param {string} params.personaId
76
- * @param {string} params.file - Single file (relative to rootPath).
77
- * @param {string} params.rootPath - Repo root.
78
- * @param {object} params.budget - Shared budget state.
79
- * @param {Function} [params.onEvent]
80
- * @returns {Promise<{findings: Array, toolInvocations: Array, stoppedEarly: boolean}>}
81
- */
82
- export async function runPersonaOnFile({
70
+ async function runTools({
83
71
  personaId,
84
72
  file,
73
+ files = null,
85
74
  rootPath,
86
75
  budget,
76
+ tools,
87
77
  onEvent = () => {},
88
78
  } = {}) {
89
- if (!personaId) throw new TypeError("runPersonaOnFile requires personaId");
90
- if (!file) throw new TypeError("runPersonaOnFile requires file");
91
- if (!rootPath) throw new TypeError("runPersonaOnFile requires rootPath");
92
-
93
- const tools = getPersonaTools(personaId);
94
79
  const findings = [];
95
80
  const toolInvocations = [];
96
81
  let stoppedEarly = false;
@@ -112,7 +97,7 @@ export async function runPersonaOnFile({
112
97
  onEvent({ type: "persona_file_tool_call", personaId, file, tool: tool.id });
113
98
 
114
99
  try {
115
- const results = await tool.handler({ rootPath, files: [file] });
100
+ const results = await tool.handler({ rootPath, files: Array.isArray(files) ? files : [file] });
116
101
  if (budget) budget.toolCalls = (budget.toolCalls || 0) + 1;
117
102
  const normalized = Array.isArray(results) ? results : [];
118
103
  for (const f of normalized) {
@@ -142,6 +127,40 @@ export async function runPersonaOnFile({
142
127
  return { findings, toolInvocations, stoppedEarly };
143
128
  }
144
129
 
130
+ /**
131
+ * Dispatch every tool for the persona against a single file scope. Each
132
+ * tool handler is invoked with `{ rootPath, files: [file] }` because
133
+ * every persona's tool contract accepts this shape (see #A13-#A22).
134
+ *
135
+ * @param {object} params
136
+ * @param {string} params.personaId
137
+ * @param {string} params.file - Single file (relative to rootPath).
138
+ * @param {string} params.rootPath - Repo root.
139
+ * @param {object} params.budget - Shared budget state.
140
+ * @param {Function} [params.onEvent]
141
+ * @returns {Promise<{findings: Array, toolInvocations: Array, stoppedEarly: boolean}>}
142
+ */
143
+ export async function runPersonaOnFile({
144
+ personaId,
145
+ file,
146
+ rootPath,
147
+ budget,
148
+ onEvent = () => {},
149
+ } = {}) {
150
+ if (!personaId) throw new TypeError("runPersonaOnFile requires personaId");
151
+ if (!file) throw new TypeError("runPersonaOnFile requires file");
152
+ if (!rootPath) throw new TypeError("runPersonaOnFile requires rootPath");
153
+
154
+ return runTools({
155
+ personaId,
156
+ file,
157
+ rootPath,
158
+ budget,
159
+ tools: getPersonaTools(personaId),
160
+ onEvent,
161
+ });
162
+ }
163
+
145
164
  /**
146
165
  * Run a single persona across its assigned files in the router output.
147
166
  *
@@ -167,6 +186,50 @@ export async function runPersonaAcrossFiles({
167
186
  const visited = [];
168
187
  const skipped = [];
169
188
  let terminationReason = "ok";
189
+ const tools = getPersonaTools(personaId);
190
+ const repoScopedTools = tools.filter((tool) => tool.scope === "repo");
191
+ const fileScopedTools = tools.filter((tool) => tool.scope !== "repo");
192
+
193
+ if (repoScopedTools.length > 0) {
194
+ const budgetCheck = checkBudget(safeBudget);
195
+ if (!budgetCheck.ok) {
196
+ terminationReason = budgetCheck.reason;
197
+ skipped.push(...files);
198
+ onEvent({ type: "persona_file_skipped", personaId, file: "__repo__", stopReason: budgetCheck.reason });
199
+ } else {
200
+ onEvent({ type: "persona_file_start", personaId, file: "__repo__", fileCount: files.length });
201
+ const { findings, toolInvocations, stoppedEarly } = await runTools({
202
+ personaId,
203
+ file: "__repo__",
204
+ files,
205
+ rootPath,
206
+ budget: safeBudget,
207
+ tools: repoScopedTools,
208
+ onEvent,
209
+ });
210
+ perFile.push({ file: "__repo__", scope: "repo", files, findings, toolInvocations, stoppedEarly });
211
+ allFindings.push(...findings);
212
+ visited.push(...files);
213
+ onEvent({
214
+ type: "persona_file_complete",
215
+ personaId,
216
+ file: "__repo__",
217
+ findingCount: findings.length,
218
+ toolCount: toolInvocations.length,
219
+ });
220
+ }
221
+ }
222
+
223
+ if (fileScopedTools.length === 0) {
224
+ return {
225
+ personaId,
226
+ perFile,
227
+ findings: allFindings,
228
+ visited,
229
+ skipped,
230
+ terminationReason,
231
+ };
232
+ }
170
233
 
171
234
  for (const file of files) {
172
235
  const budgetCheck = checkBudget(safeBudget);
@@ -178,11 +241,12 @@ export async function runPersonaAcrossFiles({
178
241
  }
179
242
 
180
243
  onEvent({ type: "persona_file_start", personaId, file });
181
- const { findings, toolInvocations, stoppedEarly } = await runPersonaOnFile({
244
+ const { findings, toolInvocations, stoppedEarly } = await runTools({
182
245
  personaId,
183
246
  file,
184
247
  rootPath,
185
248
  budget: safeBudget,
249
+ tools: fileScopedTools,
186
250
  onEvent,
187
251
  });
188
252
  perFile.push({ file, findings, toolInvocations, stoppedEarly });
@@ -1,20 +1,8 @@
1
+ import { FULL_DEPTH_PERSONAS } from "./scan-modes.js";
2
+
1
3
  export const INVESTOR_DD_PROGRESS_VERSION = "investor_dd_progress_v1";
2
4
 
3
- export const INVESTOR_DD_EXPECTED_PERSONAS = Object.freeze([
4
- "security",
5
- "backend",
6
- "code-quality",
7
- "testing",
8
- "data-layer",
9
- "reliability",
10
- "release",
11
- "observability",
12
- "infrastructure",
13
- "supply-chain",
14
- "documentation",
15
- "ai-governance",
16
- "frontend",
17
- ]);
5
+ export const INVESTOR_DD_EXPECTED_PERSONAS = Object.freeze([...FULL_DEPTH_PERSONAS]);
18
6
 
19
7
  const REQUIRED_FOR_SELLABLE = Object.freeze([
20
8
  "persona_roster",
@@ -335,17 +323,15 @@ export function buildInvestorDdProgress({
335
323
  ),
336
324
  },
337
325
  findingCount: Array.isArray(findings) ? findings.length : 0,
338
- nextRecommendedSlices: byId(capabilities, "persona_roster")?.status === "complete"
339
- ? [
340
- "wire Senti session id and live usage counters into Investor-DD runs",
341
- "add per-agent token/time/LOC/customer-price/margin telemetry",
342
- "require live reconciliation and report-email proof for sellable DD closeout",
343
- ]
344
- : [
345
- "add the missing frontend/Jules persona to the default Investor-DD roster",
346
- "wire Senti session id and live usage counters into Investor-DD runs",
347
- "add per-agent token/time/LOC/customer-price/margin telemetry",
348
- "require live reconciliation and report-email proof for sellable DD closeout",
349
- ],
326
+ nextRecommendedSlices: [
327
+ ...(byId(capabilities, "persona_roster")?.status === "complete"
328
+ ? []
329
+ : missingPersonas.includes("frontend")
330
+ ? ["add the missing frontend/Jules persona to the default Investor-DD roster"]
331
+ : ["run the full 13-persona roster before claiming sellable DD coverage"]),
332
+ "wire Senti session id and live usage counters into Investor-DD runs",
333
+ "add per-agent token/time/LOC/customer-price/margin telemetry",
334
+ "require live reconciliation and report-email proof for sellable DD closeout",
335
+ ],
350
336
  };
351
337
  }
@@ -34,7 +34,7 @@
34
34
  // frontend / observability / reliability.
35
35
  // - "compliance" — cross-cutting; distributed across security / ai-governance /
36
36
  // supply-chain / infrastructure / documentation via subagent dispatch.
37
- const FULL_DEPTH_PERSONAS = [
37
+ export const FULL_DEPTH_PERSONAS = Object.freeze([
38
38
  "security",
39
39
  "backend",
40
40
  "code-quality",
@@ -48,7 +48,7 @@ const FULL_DEPTH_PERSONAS = [
48
48
  "frontend",
49
49
  "documentation",
50
50
  "ai-governance",
51
- ];
51
+ ]);
52
52
 
53
53
  const SCAN_MODES = {
54
54
  baseline: ["security"],