sentinelayer-cli 0.8.12 → 0.9.0
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/package.json +7 -2
- package/src/agents/devtestbot/config/definition.js +100 -0
- package/src/agents/devtestbot/config/system-prompt.js +92 -0
- package/src/agents/devtestbot/index.js +9 -0
- package/src/agents/devtestbot/runner.js +769 -0
- package/src/agents/devtestbot/tool.js +707 -0
- package/src/commands/legacy-args.js +4 -0
- package/src/commands/omargate.js +4 -0
- package/src/commands/session.js +415 -147
- package/src/commands/swarm.js +11 -2
- package/src/guide/generator.js +14 -0
- package/src/legacy-cli.js +34 -17
- package/src/prompt/generator.js +4 -16
- package/src/review/ai-review.js +95 -6
- package/src/review/dd-report-email-client.js +148 -0
- package/src/review/investor-dd-devtestbot.js +599 -0
- package/src/review/investor-dd-orchestrator.js +135 -3
- package/src/review/omargate-orchestrator.js +20 -2
- package/src/review/persona-prompts.js +34 -1
- package/src/review/report.js +61 -2
- package/src/session/coordination-guidance.js +48 -0
- package/src/session/daemon.js +3 -2
- package/src/session/listener.js +236 -0
- package/src/session/setup-guides.js +3 -15
- package/src/session/store.js +54 -5
- package/src/spec/generator.js +8 -10
- package/src/swarm/registry.js +20 -0
- package/src/swarm/runtime.js +139 -1
|
@@ -37,6 +37,8 @@ import {
|
|
|
37
37
|
import { notifyRunCompleted } from "./investor-dd-notification.js";
|
|
38
38
|
import { attachReproducibilityChain } from "./reproducibility-chain.js";
|
|
39
39
|
import { renderInvestorDdHtml } from "./investor-dd-html-report.js";
|
|
40
|
+
import { runDevTestBotPhase } from "./investor-dd-devtestbot.js";
|
|
41
|
+
import { redactDdEmailError } from "./dd-report-email-client.js";
|
|
40
42
|
|
|
41
43
|
const INVESTOR_DD_PERSONAS = Object.freeze([
|
|
42
44
|
"security",
|
|
@@ -164,10 +166,95 @@ function buildSummaryMarkdown({ runId, summary, routing, byPersona }) {
|
|
|
164
166
|
lines.push(`- **${sev}**: ${count}`);
|
|
165
167
|
}
|
|
166
168
|
lines.push("");
|
|
169
|
+
if (summary.devTestBot) {
|
|
170
|
+
lines.push("## devTestBot");
|
|
171
|
+
lines.push("");
|
|
172
|
+
lines.push(`- Skipped: ${summary.devTestBot.skipped ? "yes" : "no"}`);
|
|
173
|
+
lines.push(`- Subagents: ${summary.devTestBot.swarmCount || 0}`);
|
|
174
|
+
lines.push(`- Identities: ${summary.devTestBot.identityCount || 0}`);
|
|
175
|
+
lines.push(`- Findings: ${summary.devTestBot.findingCount || 0}`);
|
|
176
|
+
lines.push(`- Artifacts: ${summary.devTestBot.artifactRoot || "n/a"}`);
|
|
177
|
+
lines.push("");
|
|
178
|
+
}
|
|
167
179
|
lines.push(`Total: ${allFindings.length}`);
|
|
168
180
|
return lines.join("\n");
|
|
169
181
|
}
|
|
170
182
|
|
|
183
|
+
async function triggerReportEmail({ reportEmail, runResult, dryRun, emit }) {
|
|
184
|
+
const to = String(reportEmail?.to || "").trim();
|
|
185
|
+
if (!to) return null;
|
|
186
|
+
|
|
187
|
+
if (dryRun && reportEmail.skipWhenDryRun !== false) {
|
|
188
|
+
const result = { queued: false, skipped: true, runId: runResult.runId, to, code: "DD_EMAIL_DRY_RUN" };
|
|
189
|
+
emit({
|
|
190
|
+
type: "dd_email_skipped",
|
|
191
|
+
event: "dd_email_skipped",
|
|
192
|
+
runId: runResult.runId,
|
|
193
|
+
to,
|
|
194
|
+
reason: "dry_run",
|
|
195
|
+
});
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const client = reportEmail.client;
|
|
200
|
+
if (!client || typeof client.send !== "function") {
|
|
201
|
+
const result = { queued: false, runId: runResult.runId, to, code: "DD_EMAIL_CLIENT_MISSING" };
|
|
202
|
+
emit({
|
|
203
|
+
type: "dd_email_error",
|
|
204
|
+
event: "dd_email_error",
|
|
205
|
+
runId: runResult.runId,
|
|
206
|
+
to,
|
|
207
|
+
code: result.code,
|
|
208
|
+
error: "DD report email client is not configured.",
|
|
209
|
+
});
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const result = await client.send({ runId: runResult.runId, to, run: runResult });
|
|
215
|
+
if (result?.queued) {
|
|
216
|
+
emit({
|
|
217
|
+
type: "dd_email_queued",
|
|
218
|
+
event: "dd_email_queued",
|
|
219
|
+
runId: String(result.runId || runResult.runId),
|
|
220
|
+
to: String(result.to || to),
|
|
221
|
+
messageId: result.messageId || "",
|
|
222
|
+
replay: Boolean(result.replay),
|
|
223
|
+
sent: result.sent !== false,
|
|
224
|
+
});
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
emit({
|
|
229
|
+
type: "dd_email_error",
|
|
230
|
+
event: "dd_email_error",
|
|
231
|
+
runId: runResult.runId,
|
|
232
|
+
to,
|
|
233
|
+
code: String(result?.code || "DD_EMAIL_FAILED"),
|
|
234
|
+
status: Number(result?.status || 0),
|
|
235
|
+
error: redactDdEmailError(result?.error || "DD report email request failed."),
|
|
236
|
+
});
|
|
237
|
+
return result || { queued: false, runId: runResult.runId, to };
|
|
238
|
+
} catch (err) {
|
|
239
|
+
const result = {
|
|
240
|
+
queued: false,
|
|
241
|
+
runId: runResult.runId,
|
|
242
|
+
to,
|
|
243
|
+
code: "DD_EMAIL_EXCEPTION",
|
|
244
|
+
error: redactDdEmailError(err instanceof Error ? err.message : String(err)),
|
|
245
|
+
};
|
|
246
|
+
emit({
|
|
247
|
+
type: "dd_email_error",
|
|
248
|
+
event: "dd_email_error",
|
|
249
|
+
runId: runResult.runId,
|
|
250
|
+
to,
|
|
251
|
+
code: result.code,
|
|
252
|
+
error: result.error,
|
|
253
|
+
});
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
171
258
|
/**
|
|
172
259
|
* Run the investor-DD orchestration end to end.
|
|
173
260
|
*
|
|
@@ -183,6 +270,10 @@ function buildSummaryMarkdown({ runId, summary, routing, byPersona }) {
|
|
|
183
270
|
* @param {object} [params.liveValidator.devTestBot] - DevTestBot client.
|
|
184
271
|
* @param {object} [params.liveValidator.aidenid] - AIdenID client.
|
|
185
272
|
* @param {number} [params.liveValidator.maxInteractions]
|
|
273
|
+
* @param {object|false} [params.devTestBot] - Automated devTestBot phase config.
|
|
274
|
+
* @param {object|null} [params.reportEmail] - Optional API-side report email trigger.
|
|
275
|
+
* @param {string} [params.reportEmail.to]
|
|
276
|
+
* @param {object} [params.reportEmail.client] - { send({ runId, to, run }) }.
|
|
186
277
|
* @param {object} [params.notification] - Optional notification config.
|
|
187
278
|
* @param {string} [params.notification.notifyEmail]
|
|
188
279
|
* @param {object} [params.notification.emailClient]
|
|
@@ -198,6 +289,8 @@ export async function runInvestorDd({
|
|
|
198
289
|
dryRun = false,
|
|
199
290
|
compliancePacks = COMPLIANCE_PACK_CATALOG,
|
|
200
291
|
liveValidator = null,
|
|
292
|
+
devTestBot = {},
|
|
293
|
+
reportEmail = null,
|
|
201
294
|
notification = null,
|
|
202
295
|
} = {}) {
|
|
203
296
|
if (!rootPath) throw new TypeError("runInvestorDd requires rootPath");
|
|
@@ -207,6 +300,10 @@ export async function runInvestorDd({
|
|
|
207
300
|
const artifactBase = outputDir
|
|
208
301
|
? path.resolve(outputDir, runId, INVESTOR_DD_ARTIFACT_SUBDIR)
|
|
209
302
|
: path.resolve(rootPath, ".sentinelayer", "runs", runId, INVESTOR_DD_ARTIFACT_SUBDIR);
|
|
303
|
+
const runRoot = path.dirname(artifactBase);
|
|
304
|
+
const outputRoot = outputDir
|
|
305
|
+
? path.resolve(outputDir)
|
|
306
|
+
: path.resolve(rootPath, ".sentinelayer");
|
|
210
307
|
await fsp.mkdir(artifactBase, { recursive: true });
|
|
211
308
|
|
|
212
309
|
const streamPath = path.join(artifactBase, "stream.ndjson");
|
|
@@ -244,9 +341,11 @@ export async function runInvestorDd({
|
|
|
244
341
|
let terminationReason = "ok";
|
|
245
342
|
let reconciliationAvailable = false;
|
|
246
343
|
let compliance = null;
|
|
344
|
+
let devTestBotPhase = null;
|
|
345
|
+
let budgetState = null;
|
|
247
346
|
|
|
248
347
|
if (!dryRun) {
|
|
249
|
-
|
|
348
|
+
budgetState = createBudgetState({
|
|
250
349
|
maxUsd: resolvedBudget.maxCostUsd,
|
|
251
350
|
maxRuntimeMs: resolvedBudget.maxRuntimeMinutes * 60_000,
|
|
252
351
|
});
|
|
@@ -274,6 +373,20 @@ export async function runInvestorDd({
|
|
|
274
373
|
totalGaps: compliance.totalGaps,
|
|
275
374
|
});
|
|
276
375
|
|
|
376
|
+
devTestBotPhase = await runDevTestBotPhase({
|
|
377
|
+
runId,
|
|
378
|
+
rootPath,
|
|
379
|
+
outputRoot,
|
|
380
|
+
runRoot,
|
|
381
|
+
artifactDir: artifactBase,
|
|
382
|
+
files,
|
|
383
|
+
findings,
|
|
384
|
+
budget: budgetState,
|
|
385
|
+
options: devTestBot === false ? { enabled: false } : devTestBot || {},
|
|
386
|
+
onEvent: emit,
|
|
387
|
+
});
|
|
388
|
+
findings.push(...(devTestBotPhase.findings || []));
|
|
389
|
+
|
|
277
390
|
// Live-web validation (Jules): optional; only runs when both
|
|
278
391
|
// devTestBot + aidenid clients are supplied (pluggable contracts).
|
|
279
392
|
if (
|
|
@@ -346,6 +459,16 @@ export async function runInvestorDd({
|
|
|
346
459
|
? { totalCovered: compliance.totalCovered, totalGaps: compliance.totalGaps }
|
|
347
460
|
: null,
|
|
348
461
|
reconciliation: reconciliationAvailable,
|
|
462
|
+
devTestBot: devTestBotPhase
|
|
463
|
+
? {
|
|
464
|
+
skipped: Boolean(devTestBotPhase.skipped),
|
|
465
|
+
reason: devTestBotPhase.reason || "",
|
|
466
|
+
identityCount: devTestBotPhase.plan?.identityCount || devTestBotPhase.identities?.length || 0,
|
|
467
|
+
swarmCount: devTestBotPhase.plan?.swarmCount || devTestBotPhase.subagents?.length || 0,
|
|
468
|
+
findingCount: devTestBotPhase.findingCount || 0,
|
|
469
|
+
artifactRoot: devTestBotPhase.artifactRoot || "",
|
|
470
|
+
}
|
|
471
|
+
: null,
|
|
349
472
|
};
|
|
350
473
|
await writeJson(path.join(artifactBase, "summary.json"), summary);
|
|
351
474
|
|
|
@@ -369,6 +492,17 @@ export async function runInvestorDd({
|
|
|
369
492
|
durationSeconds,
|
|
370
493
|
terminationReason,
|
|
371
494
|
});
|
|
495
|
+
|
|
496
|
+
const runResult = { runId, artifactDir: artifactBase, summary, findings, devTestBot: devTestBotPhase };
|
|
497
|
+
if (reportEmail) {
|
|
498
|
+
runResult.reportEmail = await triggerReportEmail({
|
|
499
|
+
reportEmail,
|
|
500
|
+
runResult,
|
|
501
|
+
dryRun,
|
|
502
|
+
emit,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
372
506
|
await streamHandle.close();
|
|
373
507
|
|
|
374
508
|
const artifactFiles = await fsp.readdir(artifactBase);
|
|
@@ -385,8 +519,6 @@ export async function runInvestorDd({
|
|
|
385
519
|
}
|
|
386
520
|
await writeJson(path.join(artifactBase, "manifest.json"), manifest);
|
|
387
521
|
|
|
388
|
-
const runResult = { runId, artifactDir: artifactBase, summary, findings };
|
|
389
|
-
|
|
390
522
|
// Fire-and-forget notification dispatch (email + dashboard). Failures
|
|
391
523
|
// are non-fatal — the report is already persisted to disk + manifest.
|
|
392
524
|
if (notification && (notification.emailClient || notification.dashboardClient)) {
|
|
@@ -321,7 +321,13 @@ async function runOmarPersonaSwarm({
|
|
|
321
321
|
}));
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
-
const parentRunDirectory =
|
|
324
|
+
const parentRunDirectory =
|
|
325
|
+
deterministic?.artifacts?.runDirectory || path.join(targetPath, ".sentinelayer", "reviews", runId);
|
|
326
|
+
const systemPrompt = buildPersonaReviewPrompt({
|
|
327
|
+
personaId,
|
|
328
|
+
targetPath,
|
|
329
|
+
deterministicSummary: deterministic?.summary || {},
|
|
330
|
+
});
|
|
325
331
|
const subagentResults = await runWithConcurrency(
|
|
326
332
|
partitions.map((files, index) => ({ files, subagentIndex: index + 1 })),
|
|
327
333
|
maxConcurrent,
|
|
@@ -380,6 +386,7 @@ async function runOmarPersonaSwarm({
|
|
|
380
386
|
model: model || undefined,
|
|
381
387
|
sessionId: `${subagentRunId}-ai`,
|
|
382
388
|
maxCostUsd: budget.maxCostUsd,
|
|
389
|
+
systemPrompt,
|
|
383
390
|
dryRun,
|
|
384
391
|
env: process.env,
|
|
385
392
|
});
|
|
@@ -450,6 +457,7 @@ async function runOmarPersonaSwarm({
|
|
|
450
457
|
summary: result?.summary || summarizeFindings(findings),
|
|
451
458
|
costUsd: result?.usage?.costUsd || 0,
|
|
452
459
|
model: result?.model || model || null,
|
|
460
|
+
artifacts: result?.artifacts || null,
|
|
453
461
|
durationMs: Date.now() - subagentStart,
|
|
454
462
|
};
|
|
455
463
|
} catch (err) {
|
|
@@ -564,6 +572,7 @@ async function runOmarPersonaSwarm({
|
|
|
564
572
|
costUsd: result.costUsd || 0,
|
|
565
573
|
durationMs: result.durationMs || 0,
|
|
566
574
|
error: result.error || null,
|
|
575
|
+
artifacts: result.artifacts || null,
|
|
567
576
|
})),
|
|
568
577
|
},
|
|
569
578
|
};
|
|
@@ -786,16 +795,23 @@ export async function runOmarGateOrchestrator({
|
|
|
786
795
|
targetPath,
|
|
787
796
|
mode: "full",
|
|
788
797
|
runId: `${runId}-${personaId}`,
|
|
789
|
-
runDirectory:
|
|
798
|
+
runDirectory: path.join(
|
|
799
|
+
deterministic?.artifacts?.runDirectory || path.join(targetPath, ".sentinelayer", "reviews", runId),
|
|
800
|
+
"personas",
|
|
801
|
+
personaId
|
|
802
|
+
),
|
|
790
803
|
deterministic: {
|
|
791
804
|
summary: detSummary,
|
|
792
805
|
findings: detFindings,
|
|
806
|
+
scope: deterministic?.scope || {},
|
|
807
|
+
layers: deterministic?.layers || {},
|
|
793
808
|
metadata: deterministic?.metadata || {},
|
|
794
809
|
},
|
|
795
810
|
outputDir,
|
|
796
811
|
provider: provider || undefined,
|
|
797
812
|
model: model || undefined,
|
|
798
813
|
maxCostUsd: perPersonaCost,
|
|
814
|
+
systemPrompt,
|
|
799
815
|
dryRun,
|
|
800
816
|
env: process.env,
|
|
801
817
|
});
|
|
@@ -857,6 +873,7 @@ export async function runOmarGateOrchestrator({
|
|
|
857
873
|
summary: result?.summary || { P0: 0, P1: 0, P2: 0, P3: 0 },
|
|
858
874
|
costUsd: personaCost,
|
|
859
875
|
model: result?.model || model || null,
|
|
876
|
+
artifacts: result?.artifacts || null,
|
|
860
877
|
durationMs: Date.now() - personaStart,
|
|
861
878
|
};
|
|
862
879
|
} catch (err) {
|
|
@@ -977,6 +994,7 @@ export async function runOmarGateOrchestrator({
|
|
|
977
994
|
model: r.model || null,
|
|
978
995
|
error: r.error || null,
|
|
979
996
|
swarm: r.swarm || null,
|
|
997
|
+
artifacts: r.artifacts || null,
|
|
980
998
|
})),
|
|
981
999
|
personaHealth,
|
|
982
1000
|
findings: reconciledFindings,
|
|
@@ -27,6 +27,29 @@ Non-negotiables for your review:
|
|
|
27
27
|
|
|
28
28
|
Your output must help an acquirer decide whether to buy this codebase. Be FOUND-violations accurate, not speculation-padded.`;
|
|
29
29
|
|
|
30
|
+
export const ELEVEN_LENS_EVIDENCE_APPENDIX = `## 11-lens evidence contract
|
|
31
|
+
Evaluate every confirmed finding through these lenses before returning it:
|
|
32
|
+
|
|
33
|
+
A. Route/runtime boundary integrity
|
|
34
|
+
B. State, lifecycle, and hook correctness
|
|
35
|
+
C. Render cost, re-render, and scalability mechanics
|
|
36
|
+
D. Hydration, SSR, streaming, and environment divergence
|
|
37
|
+
E. Data fetching, caching, timeout, and freshness behavior
|
|
38
|
+
F. Bundle/dependency footprint and code-splitting risk
|
|
39
|
+
G. Assets, scripts, layout stability, and resource loading
|
|
40
|
+
H. Accessibility, keyboard, focus, and trust-critical UX
|
|
41
|
+
I. Mobile/responsive reliability across 360px, 768px, and desktop
|
|
42
|
+
J. Verification, rollback, and QA readiness
|
|
43
|
+
K. AI governance, provenance, HITL, and agent/tool permission surfaces
|
|
44
|
+
|
|
45
|
+
For each finding include:
|
|
46
|
+
- lensEvidence: object keyed by lens letter with "passed", "failed", or "not_applicable" plus one short evidence sentence
|
|
47
|
+
- reproduction: object with type (manual_step | shell | runtime_probe | static_trace) and steps array; required for P0/P1
|
|
48
|
+
- user_impact: one sentence describing what a user/operator/system experiences
|
|
49
|
+
- trafficLight: green | yellow | red for automation safety
|
|
50
|
+
- rootCause: why the issue exists
|
|
51
|
+
- recommendedFix: concrete fix path`;
|
|
52
|
+
|
|
30
53
|
const PERSONA_PROMPTS = {
|
|
31
54
|
security: {
|
|
32
55
|
role: "Nina Patel — Security Specialist",
|
|
@@ -291,6 +314,8 @@ ${FAANG_GRADE_PREAMBLE}
|
|
|
291
314
|
|
|
292
315
|
${persona.focus}
|
|
293
316
|
|
|
317
|
+
${ELEVEN_LENS_EVIDENCE_APPENDIX}
|
|
318
|
+
|
|
294
319
|
${checklistBlock}
|
|
295
320
|
## Context
|
|
296
321
|
Target: ${targetPath || "(not provided)"}
|
|
@@ -313,6 +338,10 @@ Return a JSON OBJECT (not array) with this shape — return ONLY the JSON, no ot
|
|
|
313
338
|
"line": 42,
|
|
314
339
|
"title": "Brief description",
|
|
315
340
|
"evidence": "Concrete code excerpt at file:line (min 1 line)",
|
|
341
|
+
"lensEvidence": { "A": "not_applicable: no route/runtime boundary impact", "K": "passed: no AI governance surface involved" },
|
|
342
|
+
"reproduction": { "type": "static_trace", "steps": ["Inspect path/to/file.ext:42", "Trace the value/control flow to the failing behavior"] },
|
|
343
|
+
"user_impact": "One sentence describing the user/operator/system failure mode",
|
|
344
|
+
"trafficLight": "green|yellow|red",
|
|
316
345
|
"rootCause": "Why this is a problem",
|
|
317
346
|
"recommendedFix": "Specific code change to apply",
|
|
318
347
|
"confidence": 0.85,
|
|
@@ -326,6 +355,8 @@ Rules:
|
|
|
326
355
|
- Maximum ${maxFindings} findings.
|
|
327
356
|
- Only report findings you have HIGH confidence in (>= 0.7).
|
|
328
357
|
- Every finding MUST have concrete file:line evidence AND a non-empty \`evidence\` code excerpt.
|
|
358
|
+
- Every finding MUST include \`lensEvidence\`, \`user_impact\`, \`trafficLight\`, \`rootCause\`, and \`recommendedFix\`.
|
|
359
|
+
- P0/P1 findings MUST include \`reproduction\` steps.
|
|
329
360
|
- Do NOT repeat findings already in the deterministic scan.
|
|
330
361
|
- Do NOT report hypothetical/speculative issues.
|
|
331
362
|
- Focus on REAL, EXPLOITABLE, IMPACTFUL problems in your domain.
|
|
@@ -337,10 +368,12 @@ Rules:
|
|
|
337
368
|
function buildGenericPrompt({ targetPath, deterministicSummary, maxFindings }) {
|
|
338
369
|
return `You are a senior code reviewer. Analyze the code for security, quality, and reliability issues.
|
|
339
370
|
|
|
371
|
+
${ELEVEN_LENS_EVIDENCE_APPENDIX}
|
|
372
|
+
|
|
340
373
|
Target: ${targetPath || "(not provided)"}
|
|
341
374
|
Deterministic scan: P0=${deterministicSummary.P0 || 0} P1=${deterministicSummary.P1 || 0} P2=${deterministicSummary.P2 || 0}
|
|
342
375
|
|
|
343
|
-
Return a JSON
|
|
376
|
+
Return a JSON object with inspectedFiles, coverage, and up to ${maxFindings} findings. Each finding needs: severity, file, line, title, evidence, lensEvidence, reproduction for P0/P1, user_impact, trafficLight, rootCause, recommendedFix, confidence.
|
|
344
377
|
Only report findings with concrete evidence. Do NOT repeat deterministic findings.`;
|
|
345
378
|
}
|
|
346
379
|
|
package/src/review/report.js
CHANGED
|
@@ -70,6 +70,28 @@ function normalizeConfidenceFloor(value) {
|
|
|
70
70
|
return Math.max(0, Math.min(1, normalized));
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
function normalizeTrafficLight(value) {
|
|
74
|
+
const normalized = normalizeString(value).toLowerCase();
|
|
75
|
+
if (["green", "yellow", "red"].includes(normalized)) {
|
|
76
|
+
return normalized;
|
|
77
|
+
}
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function cloneJsonCompatible(value) {
|
|
82
|
+
if (value === undefined || value === null || value === "") {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
if (typeof value === "string") {
|
|
86
|
+
return normalizeString(value) || null;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(JSON.stringify(value));
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
73
95
|
function confidenceFloorForFinding(finding = {}, {
|
|
74
96
|
source = "ai",
|
|
75
97
|
confidenceFloors = {},
|
|
@@ -202,15 +224,28 @@ export function reconcileReviewFindings({
|
|
|
202
224
|
confidenceFloors,
|
|
203
225
|
defaultConfidenceFloor: normalizedDefaultConfidenceFloor,
|
|
204
226
|
});
|
|
227
|
+
const evidence = normalizeString(finding.evidence || finding.excerpt);
|
|
228
|
+
const rootCause = normalizeString(finding.rootCause || finding.root_cause);
|
|
229
|
+
const recommendedFix = normalizeString(
|
|
230
|
+
finding.recommendedFix || finding.recommended_fix || finding.suggestedFix
|
|
231
|
+
);
|
|
232
|
+
const suggestedFix = normalizeString(finding.suggestedFix || recommendedFix);
|
|
205
233
|
const normalized = {
|
|
206
234
|
findingId: "",
|
|
207
235
|
severity: normalizeSeverity(finding.severity),
|
|
208
236
|
file: toPosixPath(normalizeString(finding.file) || "unknown"),
|
|
209
237
|
line: Math.max(1, Math.floor(Number(finding.line || 1))),
|
|
210
238
|
message: normalizeString(finding.message) || "Unnamed finding",
|
|
211
|
-
excerpt: normalizeString(finding.excerpt),
|
|
239
|
+
excerpt: normalizeString(finding.excerpt || evidence || rootCause),
|
|
212
240
|
ruleId: normalizeString(finding.ruleId),
|
|
213
|
-
suggestedFix
|
|
241
|
+
suggestedFix,
|
|
242
|
+
evidence,
|
|
243
|
+
lensEvidence: cloneJsonCompatible(finding.lensEvidence || finding.lens_evidence),
|
|
244
|
+
reproduction: cloneJsonCompatible(finding.reproduction),
|
|
245
|
+
userImpact: normalizeString(finding.userImpact || finding.user_impact),
|
|
246
|
+
trafficLight: normalizeTrafficLight(finding.trafficLight || finding.traffic_light),
|
|
247
|
+
rootCause,
|
|
248
|
+
recommendedFix: recommendedFix || suggestedFix,
|
|
214
249
|
persona,
|
|
215
250
|
layer: normalizeString(finding.layer),
|
|
216
251
|
confidence: source === "deterministic" ? 1 : formatConfidence(finding.confidence),
|
|
@@ -260,6 +295,27 @@ export function reconcileReviewFindings({
|
|
|
260
295
|
if (!preferred.suggestedFix) {
|
|
261
296
|
preferred.suggestedFix = existing.suggestedFix || normalized.suggestedFix;
|
|
262
297
|
}
|
|
298
|
+
if (!preferred.evidence) {
|
|
299
|
+
preferred.evidence = existing.evidence || normalized.evidence;
|
|
300
|
+
}
|
|
301
|
+
if (!preferred.lensEvidence) {
|
|
302
|
+
preferred.lensEvidence = existing.lensEvidence || normalized.lensEvidence;
|
|
303
|
+
}
|
|
304
|
+
if (!preferred.reproduction) {
|
|
305
|
+
preferred.reproduction = existing.reproduction || normalized.reproduction;
|
|
306
|
+
}
|
|
307
|
+
if (!preferred.userImpact) {
|
|
308
|
+
preferred.userImpact = existing.userImpact || normalized.userImpact;
|
|
309
|
+
}
|
|
310
|
+
if (!preferred.trafficLight) {
|
|
311
|
+
preferred.trafficLight = existing.trafficLight || normalized.trafficLight;
|
|
312
|
+
}
|
|
313
|
+
if (!preferred.rootCause) {
|
|
314
|
+
preferred.rootCause = existing.rootCause || normalized.rootCause;
|
|
315
|
+
}
|
|
316
|
+
if (!preferred.recommendedFix) {
|
|
317
|
+
preferred.recommendedFix = existing.recommendedFix || normalized.recommendedFix;
|
|
318
|
+
}
|
|
263
319
|
merged.set(key, preferred);
|
|
264
320
|
};
|
|
265
321
|
|
|
@@ -354,6 +410,9 @@ function composeReportMarkdown(report = {}) {
|
|
|
354
410
|
` confidence: ${(formatConfidence(finding.confidence) * 100).toFixed(0)}%\n` +
|
|
355
411
|
` sources: ${(finding.sources || []).join(", ") || "none"}\n` +
|
|
356
412
|
` verdict: ${finding.adjudication?.verdict || "pending"}\n` +
|
|
413
|
+
(finding.trafficLight ? ` traffic_light: ${finding.trafficLight}\n` : "") +
|
|
414
|
+
(finding.userImpact ? ` user_impact: ${finding.userImpact}\n` : "") +
|
|
415
|
+
(finding.rootCause ? ` root_cause: ${finding.rootCause}\n` : "") +
|
|
357
416
|
` suggested_fix: ${finding.suggestedFix || "Review and remediate as needed."}`
|
|
358
417
|
)
|
|
359
418
|
.join("\n")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const COORDINATION_GUIDANCE_TITLE = "Multi-Agent Coordination Protocol";
|
|
2
|
+
|
|
3
|
+
export const COORDINATION_ETIQUETTE_ITEMS = Object.freeze([
|
|
4
|
+
"Find the recent Senti session for this codebase: run `sl session list --path .` and `sl session list --remote --path .`; join the right room with `sl session join <id> --name <your-name> --role coder`.",
|
|
5
|
+
"Before implementation, post a short plan and file claims with `sl session say <id> \"plan: <scope>; files: <paths>\"`.",
|
|
6
|
+
"Claim shared files before editing with `lock: <file> - <intent>` and release them with `unlock: <file> - done`.",
|
|
7
|
+
"Run a background listener for replies: `sl session listen --session <id> --agent <your-name> --interval 60 --emit ndjson`; if background polling is unavailable, fall back to `sl session sync <id> --json` then `sl session read <id> --tail 20 --json` every 5 minutes.",
|
|
8
|
+
"Run `sl review --diff` after each finished file or PR-ready diff and post the result summary back to the session.",
|
|
9
|
+
"Post findings through `sl session say <id> \"finding: [P2] <title> in <file>:<line>\"` with enough context for a peer to act.",
|
|
10
|
+
"Ask for help in-session instead of stopping on unexpected file changes, blocked context, or ambiguous ownership.",
|
|
11
|
+
"Offer non-conflicting follow-up work to peers when you finish your claimed scope or discover separable tasks.",
|
|
12
|
+
"Run `sl --help` when you hit an unfamiliar workflow before guessing at command syntax.",
|
|
13
|
+
"Leave the session when done with `sl session leave <id>` after posting the final status and verification evidence.",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export function getCoordinationEtiquetteItems() {
|
|
17
|
+
return [...COORDINATION_ETIQUETTE_ITEMS];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function renderCoordinationNumberedList({
|
|
21
|
+
items = COORDINATION_ETIQUETTE_ITEMS,
|
|
22
|
+
indent = "",
|
|
23
|
+
} = {}) {
|
|
24
|
+
return items.map((item, index) => `${indent}${index + 1}. ${item}`).join("\n");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function renderCoordinationBulletList({
|
|
28
|
+
items = COORDINATION_ETIQUETTE_ITEMS,
|
|
29
|
+
indent = "",
|
|
30
|
+
} = {}) {
|
|
31
|
+
return items.map((item) => `${indent}- ${item}`).join("\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function renderCoordinationMarkdownSection({
|
|
35
|
+
headingLevel = 2,
|
|
36
|
+
title = COORDINATION_GUIDANCE_TITLE,
|
|
37
|
+
} = {}) {
|
|
38
|
+
const level = Math.max(1, Math.min(6, Number.parseInt(String(headingLevel || 2), 10) || 2));
|
|
39
|
+
return `${"#".repeat(level)} ${title}
|
|
40
|
+
${renderCoordinationNumberedList()}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function renderCoordinationTicketBlock() {
|
|
44
|
+
return [
|
|
45
|
+
"Coordination rules:",
|
|
46
|
+
renderCoordinationNumberedList(),
|
|
47
|
+
].join("\n");
|
|
48
|
+
}
|
package/src/session/daemon.js
CHANGED
|
@@ -45,6 +45,7 @@ const HELP_MODEL_TIMEOUT_MS = 3_000;
|
|
|
45
45
|
const HELP_CONTEXT_EVENT_TAIL = 50;
|
|
46
46
|
const HELP_CONTEXT_RESULT_LIMIT = 6;
|
|
47
47
|
const HELP_BLACKBOARD_ENTRY_LIMIT = 40;
|
|
48
|
+
const WATCHER_STARTUP_REPLAY_TAIL = 100;
|
|
48
49
|
const FILE_CONFLICT_WINDOW_MS = 60_000;
|
|
49
50
|
const RENEWAL_WINDOW_MS = 60 * 60 * 1000;
|
|
50
51
|
const RENEWAL_THRESHOLD_EVENTS = 10;
|
|
@@ -608,7 +609,7 @@ async function runHelpWatcher(daemonState) {
|
|
|
608
609
|
targetPath: daemonState.targetPath,
|
|
609
610
|
signal,
|
|
610
611
|
since: daemonState.startedAt,
|
|
611
|
-
replayTail:
|
|
612
|
+
replayTail: WATCHER_STARTUP_REPLAY_TAIL,
|
|
612
613
|
pollMs: Math.max(25, Math.min(250, Math.floor(daemonState.helpRequestTimeoutMs / 4))),
|
|
613
614
|
})) {
|
|
614
615
|
if (!daemonState.running) {
|
|
@@ -781,7 +782,7 @@ async function runSessionDirectiveWatcher(daemonState) {
|
|
|
781
782
|
targetPath: daemonState.targetPath,
|
|
782
783
|
signal,
|
|
783
784
|
since: daemonState.startedAt,
|
|
784
|
-
replayTail:
|
|
785
|
+
replayTail: WATCHER_STARTUP_REPLAY_TAIL,
|
|
785
786
|
pollMs: 100,
|
|
786
787
|
})) {
|
|
787
788
|
if (!daemonState.running) {
|