sentinelayer-cli 0.8.11 → 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 +10 -5
- 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/agents/jules/stream.js +2 -12
- package/src/audit/orchestrator.js +471 -114
- package/src/audit/persona-loop.js +1342 -0
- package/src/audit/registry.js +58 -2
- package/src/commands/audit.js +42 -1
- package/src/commands/legacy-args.js +32 -1
- package/src/commands/omargate.js +4 -0
- package/src/commands/session.js +417 -89
- package/src/commands/swarm.js +11 -2
- package/src/cost/history.js +41 -21
- package/src/events/schema.js +27 -1
- package/src/guide/generator.js +14 -0
- package/src/legacy-cli.js +110 -18
- 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-cache.js +285 -0
- package/src/review/omargate-orchestrator.js +605 -4
- package/src/review/persona-prompts.js +34 -1
- package/src/review/report.js +189 -4
- 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/senti-naming.js +36 -0
- package/src/session/setup-guides.js +3 -15
- package/src/session/store.js +54 -5
- package/src/session/sync.js +23 -0
- package/src/spec/generator.js +8 -10
- package/src/swarm/registry.js +20 -0
- package/src/swarm/runtime.js +139 -1
|
@@ -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
|
@@ -62,6 +62,75 @@ function formatConfidence(value) {
|
|
|
62
62
|
return Math.max(0, Math.min(1, normalized));
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function normalizeConfidenceFloor(value) {
|
|
66
|
+
const normalized = Number(value);
|
|
67
|
+
if (!Number.isFinite(normalized)) {
|
|
68
|
+
return 0.7;
|
|
69
|
+
}
|
|
70
|
+
return Math.max(0, Math.min(1, normalized));
|
|
71
|
+
}
|
|
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
|
+
|
|
95
|
+
function confidenceFloorForFinding(finding = {}, {
|
|
96
|
+
source = "ai",
|
|
97
|
+
confidenceFloors = {},
|
|
98
|
+
defaultConfidenceFloor = 0.7,
|
|
99
|
+
} = {}) {
|
|
100
|
+
const persona = normalizeString(finding.persona || finding.personaId || finding.agentId);
|
|
101
|
+
const layer = normalizeString(finding.layer);
|
|
102
|
+
const identity = sourceIdentityForFinding(finding, source);
|
|
103
|
+
const floor =
|
|
104
|
+
finding.confidenceFloor ??
|
|
105
|
+
finding.personaConfidenceFloor ??
|
|
106
|
+
confidenceFloors[identity] ??
|
|
107
|
+
confidenceFloors[persona] ??
|
|
108
|
+
confidenceFloors[layer] ??
|
|
109
|
+
confidenceFloors[source] ??
|
|
110
|
+
defaultConfidenceFloor;
|
|
111
|
+
return normalizeConfidenceFloor(floor);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function sourceIdentityForFinding(finding = {}, source = "ai") {
|
|
115
|
+
if (source === "deterministic") {
|
|
116
|
+
return "deterministic";
|
|
117
|
+
}
|
|
118
|
+
const persona = normalizeString(
|
|
119
|
+
finding.persona || finding.personaId || finding.agentId || finding.layer
|
|
120
|
+
);
|
|
121
|
+
return `ai:${persona || "generic"}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function hasMultiSourceConfirmation(finding = {}) {
|
|
125
|
+
const confirmationSources = Array.isArray(finding.confirmationSources)
|
|
126
|
+
? finding.confirmationSources
|
|
127
|
+
: [];
|
|
128
|
+
const sourceIdentities = confirmationSources.length > 0
|
|
129
|
+
? confirmationSources
|
|
130
|
+
: (Array.isArray(finding.sources) ? finding.sources : []);
|
|
131
|
+
return new Set(sourceIdentities.filter(Boolean)).size >= 2;
|
|
132
|
+
}
|
|
133
|
+
|
|
65
134
|
function dedupeKeyForFinding(finding = {}) {
|
|
66
135
|
const file = toPosixPath(normalizeString(finding.file) || "unknown");
|
|
67
136
|
const line = Number(finding.line || 1);
|
|
@@ -104,25 +173,85 @@ function summarizeFindings(findings = []) {
|
|
|
104
173
|
};
|
|
105
174
|
}
|
|
106
175
|
|
|
176
|
+
export function dropBelowConfidence(findings = [], { threshold = 0.7 } = {}) {
|
|
177
|
+
const defaultThreshold = normalizeConfidenceFloor(threshold);
|
|
178
|
+
const kept = [];
|
|
179
|
+
const dropped = [];
|
|
180
|
+
|
|
181
|
+
for (const finding of findings || []) {
|
|
182
|
+
const confidence = formatConfidence(finding.confidence);
|
|
183
|
+
const confidenceFloor = normalizeConfidenceFloor(
|
|
184
|
+
finding.confidenceFloor ?? finding.personaConfidenceFloor ?? defaultThreshold
|
|
185
|
+
);
|
|
186
|
+
if (!hasMultiSourceConfirmation(finding) && confidence < confidenceFloor) {
|
|
187
|
+
dropped.push({
|
|
188
|
+
...finding,
|
|
189
|
+
confidence,
|
|
190
|
+
confidenceFloor,
|
|
191
|
+
droppedReason: "below_confidence_floor_single_source",
|
|
192
|
+
});
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
kept.push({
|
|
196
|
+
...finding,
|
|
197
|
+
confidence,
|
|
198
|
+
confidenceFloor,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
findings: kept,
|
|
204
|
+
dropped,
|
|
205
|
+
droppedCount: dropped.length,
|
|
206
|
+
threshold: defaultThreshold,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
107
210
|
export function reconcileReviewFindings({
|
|
108
211
|
deterministicFindings = [],
|
|
109
212
|
aiFindings = [],
|
|
213
|
+
confidenceFloor = 0.7,
|
|
214
|
+
defaultConfidenceFloor = confidenceFloor,
|
|
215
|
+
confidenceFloors = {},
|
|
110
216
|
} = {}) {
|
|
111
217
|
const merged = new Map();
|
|
218
|
+
const normalizedDefaultConfidenceFloor = normalizeConfidenceFloor(defaultConfidenceFloor);
|
|
112
219
|
|
|
113
220
|
const addFinding = (finding, source) => {
|
|
221
|
+
const persona = normalizeString(finding.persona || finding.personaId || finding.agentId);
|
|
222
|
+
const confidenceFloorForSource = confidenceFloorForFinding(finding, {
|
|
223
|
+
source,
|
|
224
|
+
confidenceFloors,
|
|
225
|
+
defaultConfidenceFloor: normalizedDefaultConfidenceFloor,
|
|
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);
|
|
114
233
|
const normalized = {
|
|
115
234
|
findingId: "",
|
|
116
235
|
severity: normalizeSeverity(finding.severity),
|
|
117
236
|
file: toPosixPath(normalizeString(finding.file) || "unknown"),
|
|
118
237
|
line: Math.max(1, Math.floor(Number(finding.line || 1))),
|
|
119
238
|
message: normalizeString(finding.message) || "Unnamed finding",
|
|
120
|
-
excerpt: normalizeString(finding.excerpt),
|
|
239
|
+
excerpt: normalizeString(finding.excerpt || evidence || rootCause),
|
|
121
240
|
ruleId: normalizeString(finding.ruleId),
|
|
122
|
-
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,
|
|
249
|
+
persona,
|
|
123
250
|
layer: normalizeString(finding.layer),
|
|
124
251
|
confidence: source === "deterministic" ? 1 : formatConfidence(finding.confidence),
|
|
252
|
+
confidenceFloor: confidenceFloorForSource,
|
|
125
253
|
sources: [source],
|
|
254
|
+
confirmationSources: [sourceIdentityForFinding(finding, source)],
|
|
126
255
|
adjudication: {
|
|
127
256
|
verdict: "pending",
|
|
128
257
|
note: "",
|
|
@@ -138,8 +267,25 @@ export function reconcileReviewFindings({
|
|
|
138
267
|
}
|
|
139
268
|
|
|
140
269
|
const nextSources = new Set([...(existing.sources || []), source]);
|
|
270
|
+
const nextConfirmationSources = new Set([
|
|
271
|
+
...(existing.confirmationSources || []),
|
|
272
|
+
...(normalized.confirmationSources || []),
|
|
273
|
+
]);
|
|
141
274
|
const preferred = compareFindingPriority(existing, normalized) <= 0 ? existing : normalized;
|
|
142
275
|
preferred.sources = [...nextSources].sort((left, right) => left.localeCompare(right));
|
|
276
|
+
preferred.confirmationSources = [...nextConfirmationSources].sort((left, right) =>
|
|
277
|
+
left.localeCompare(right)
|
|
278
|
+
);
|
|
279
|
+
preferred.confidenceFloor = Math.max(
|
|
280
|
+
normalizeConfidenceFloor(existing.confidenceFloor),
|
|
281
|
+
normalizeConfidenceFloor(normalized.confidenceFloor)
|
|
282
|
+
);
|
|
283
|
+
if (!preferred.persona) {
|
|
284
|
+
preferred.persona = existing.persona || normalized.persona;
|
|
285
|
+
}
|
|
286
|
+
if (!preferred.layer) {
|
|
287
|
+
preferred.layer = existing.layer || normalized.layer;
|
|
288
|
+
}
|
|
143
289
|
if (!preferred.excerpt) {
|
|
144
290
|
preferred.excerpt = existing.excerpt || normalized.excerpt;
|
|
145
291
|
}
|
|
@@ -149,6 +295,27 @@ export function reconcileReviewFindings({
|
|
|
149
295
|
if (!preferred.suggestedFix) {
|
|
150
296
|
preferred.suggestedFix = existing.suggestedFix || normalized.suggestedFix;
|
|
151
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
|
+
}
|
|
152
319
|
merged.set(key, preferred);
|
|
153
320
|
};
|
|
154
321
|
|
|
@@ -159,7 +326,10 @@ export function reconcileReviewFindings({
|
|
|
159
326
|
addFinding(finding, "ai");
|
|
160
327
|
}
|
|
161
328
|
|
|
162
|
-
const
|
|
329
|
+
const confidenceFilter = dropBelowConfidence([...merged.values()], {
|
|
330
|
+
threshold: normalizedDefaultConfidenceFloor,
|
|
331
|
+
});
|
|
332
|
+
const findings = confidenceFilter.findings.sort((left, right) => {
|
|
163
333
|
const severityDelta = SEVERITY_RANK[left.severity] - SEVERITY_RANK[right.severity];
|
|
164
334
|
if (severityDelta !== 0) {
|
|
165
335
|
return severityDelta;
|
|
@@ -177,7 +347,13 @@ export function reconcileReviewFindings({
|
|
|
177
347
|
|
|
178
348
|
return {
|
|
179
349
|
findings,
|
|
180
|
-
|
|
350
|
+
droppedFindings: confidenceFilter.dropped,
|
|
351
|
+
summary: {
|
|
352
|
+
...summarizeFindings(findings),
|
|
353
|
+
confidenceFloor: confidenceFilter.threshold,
|
|
354
|
+
droppedBelowConfidence: confidenceFilter.droppedCount,
|
|
355
|
+
droppedBelowConfidenceSingleSource: confidenceFilter.droppedCount,
|
|
356
|
+
},
|
|
181
357
|
};
|
|
182
358
|
}
|
|
183
359
|
|
|
@@ -234,6 +410,9 @@ function composeReportMarkdown(report = {}) {
|
|
|
234
410
|
` confidence: ${(formatConfidence(finding.confidence) * 100).toFixed(0)}%\n` +
|
|
235
411
|
` sources: ${(finding.sources || []).join(", ") || "none"}\n` +
|
|
236
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` : "") +
|
|
237
416
|
` suggested_fix: ${finding.suggestedFix || "Review and remediate as needed."}`
|
|
238
417
|
)
|
|
239
418
|
.join("\n")
|
|
@@ -250,6 +429,7 @@ function composeReportMarkdown(report = {}) {
|
|
|
250
429
|
`- Findings: P0=${report.summary.P0} P1=${report.summary.P1} P2=${report.summary.P2} P3=${report.summary.P3}`,
|
|
251
430
|
`- Blocking: ${report.summary.blocking ? "yes" : "no"}`,
|
|
252
431
|
`- Total findings: ${report.findings.length}`,
|
|
432
|
+
`- Dropped below confidence floor (single-source): ${report.summary.droppedBelowConfidence || 0}`,
|
|
253
433
|
"",
|
|
254
434
|
"Metadata:",
|
|
255
435
|
`- commit_sha: ${report.metadata.git.commitSha || "unknown"}`,
|
|
@@ -280,6 +460,8 @@ export async function buildUnifiedReviewReport({
|
|
|
280
460
|
deterministic,
|
|
281
461
|
aiLayer = null,
|
|
282
462
|
specFile = "",
|
|
463
|
+
defaultConfidenceFloor = 0.7,
|
|
464
|
+
confidenceFloors = {},
|
|
283
465
|
} = {}) {
|
|
284
466
|
const normalizedTargetPath = path.resolve(String(targetPath || "."));
|
|
285
467
|
const normalizedMode = normalizeString(mode) || "full";
|
|
@@ -289,6 +471,8 @@ export async function buildUnifiedReviewReport({
|
|
|
289
471
|
const reconciliation = reconcileReviewFindings({
|
|
290
472
|
deterministicFindings: deterministic?.findings || [],
|
|
291
473
|
aiFindings: aiLayer?.findings || [],
|
|
474
|
+
defaultConfidenceFloor,
|
|
475
|
+
confidenceFloors,
|
|
292
476
|
});
|
|
293
477
|
const spec = await resolveSpecMetadata(normalizedTargetPath, specFile);
|
|
294
478
|
const commitSha = runGit(normalizedTargetPath, ["rev-parse", "HEAD"]);
|
|
@@ -303,6 +487,7 @@ export async function buildUnifiedReviewReport({
|
|
|
303
487
|
mode: normalizedMode,
|
|
304
488
|
summary: reconciliation.summary,
|
|
305
489
|
findings: reconciliation.findings,
|
|
490
|
+
droppedFindings: reconciliation.droppedFindings,
|
|
306
491
|
severityMatrix: buildSeverityMatrix(),
|
|
307
492
|
metadata: {
|
|
308
493
|
git: {
|
|
@@ -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) {
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
|
|
3
|
+
import { pollSessionEvents } from "./sync.js";
|
|
4
|
+
import { readSyncCursor, writeSyncCursor } from "./sync-cursor.js";
|
|
5
|
+
|
|
6
|
+
const BROADCAST_RECIPIENTS = new Set([
|
|
7
|
+
"*",
|
|
8
|
+
"all",
|
|
9
|
+
"broadcast",
|
|
10
|
+
"everyone",
|
|
11
|
+
"anyone",
|
|
12
|
+
"agents",
|
|
13
|
+
"all-agents",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function normalizeString(value) {
|
|
17
|
+
return String(value || "").trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeComparableId(value) {
|
|
21
|
+
return normalizeString(value)
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
24
|
+
.replace(/^-+|-+$/g, "");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizePositiveInteger(value, fallbackValue) {
|
|
28
|
+
if (value === undefined || value === null || String(value).trim() === "") {
|
|
29
|
+
return fallbackValue;
|
|
30
|
+
}
|
|
31
|
+
const normalized = Number(value);
|
|
32
|
+
if (!Number.isFinite(normalized) || normalized <= 0) return fallbackValue;
|
|
33
|
+
return Math.floor(normalized);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isPlainObject(value) {
|
|
37
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function addRecipientValue(values, value) {
|
|
41
|
+
if (value === undefined || value === null) return;
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
for (const item of value) addRecipientValue(values, item);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (isPlainObject(value)) {
|
|
47
|
+
addRecipientValue(values, value.id || value.agentId || value.name);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const raw = normalizeString(value);
|
|
51
|
+
if (!raw) return;
|
|
52
|
+
for (const token of raw.split(/[\s,;]+/g)) {
|
|
53
|
+
const normalized = normalizeString(token);
|
|
54
|
+
if (normalized) values.push(normalized);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function collectSessionEventRecipients(event = {}) {
|
|
59
|
+
const values = [];
|
|
60
|
+
if (!isPlainObject(event)) return values;
|
|
61
|
+
const payload = isPlainObject(event.payload) ? event.payload : {};
|
|
62
|
+
for (const source of [
|
|
63
|
+
event.to,
|
|
64
|
+
event.recipient,
|
|
65
|
+
event.recipients,
|
|
66
|
+
event.targetAgent,
|
|
67
|
+
event.targetAgentId,
|
|
68
|
+
payload.to,
|
|
69
|
+
payload.recipient,
|
|
70
|
+
payload.recipients,
|
|
71
|
+
payload.targetAgent,
|
|
72
|
+
payload.targetAgentId,
|
|
73
|
+
]) {
|
|
74
|
+
addRecipientValue(values, source);
|
|
75
|
+
}
|
|
76
|
+
return values;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function eventMatchesAgent(event = {}, agentId = "") {
|
|
80
|
+
if (!isPlainObject(event)) return false;
|
|
81
|
+
const normalizedAgentId = normalizeComparableId(agentId);
|
|
82
|
+
if (!normalizedAgentId) return false;
|
|
83
|
+
|
|
84
|
+
const payload = isPlainObject(event.payload) ? event.payload : {};
|
|
85
|
+
if (event.broadcast === true || payload.broadcast === true) return true;
|
|
86
|
+
|
|
87
|
+
const recipients = collectSessionEventRecipients(event);
|
|
88
|
+
if (recipients.length === 0) return true;
|
|
89
|
+
|
|
90
|
+
for (const recipient of recipients) {
|
|
91
|
+
const rawRecipient = normalizeString(recipient).toLowerCase();
|
|
92
|
+
if (BROADCAST_RECIPIENTS.has(rawRecipient)) return true;
|
|
93
|
+
const normalizedRecipient = normalizeComparableId(recipient);
|
|
94
|
+
if (!normalizedRecipient) continue;
|
|
95
|
+
if (BROADCAST_RECIPIENTS.has(normalizedRecipient)) return true;
|
|
96
|
+
if (normalizedRecipient === normalizedAgentId) return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function listenCursorSuffix(agentId = "") {
|
|
102
|
+
return `listen-${normalizeComparableId(agentId) || "agent"}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function defaultSleep(ms, { signal } = {}) {
|
|
106
|
+
await delay(ms, undefined, { signal });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function shouldAbort(error, signal) {
|
|
110
|
+
return Boolean(signal?.aborted || error?.name === "AbortError" || error?.code === "ABORT_ERR");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function cursorFromEvents(events = [], fallbackCursor = null) {
|
|
114
|
+
let cursor = normalizeString(fallbackCursor) || null;
|
|
115
|
+
for (const event of events) {
|
|
116
|
+
const candidate = normalizeString(event?.cursor);
|
|
117
|
+
if (candidate) cursor = candidate;
|
|
118
|
+
}
|
|
119
|
+
return cursor;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function eventTimestampMs(event = {}) {
|
|
123
|
+
for (const key of ["ts", "timestamp", "createdAt", "at"]) {
|
|
124
|
+
const epoch = Date.parse(normalizeString(event?.[key]));
|
|
125
|
+
if (Number.isFinite(epoch)) return epoch;
|
|
126
|
+
}
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Poll session events in the background and emit only events addressed to
|
|
132
|
+
* the current agent or broadcast to everyone. The loop advances its cursor
|
|
133
|
+
* across non-matching events so direct listeners do not replay unrelated
|
|
134
|
+
* traffic forever.
|
|
135
|
+
*/
|
|
136
|
+
export async function listenSessionEvents({
|
|
137
|
+
sessionId,
|
|
138
|
+
targetPath = process.cwd(),
|
|
139
|
+
agentId = "cli-user",
|
|
140
|
+
intervalSeconds = 60,
|
|
141
|
+
limit = 200,
|
|
142
|
+
since = undefined,
|
|
143
|
+
replay = false,
|
|
144
|
+
maxPolls = null,
|
|
145
|
+
signal,
|
|
146
|
+
onEvent = async () => {},
|
|
147
|
+
onError = async () => {},
|
|
148
|
+
_poll = pollSessionEvents,
|
|
149
|
+
_readCursor = readSyncCursor,
|
|
150
|
+
_writeCursor = writeSyncCursor,
|
|
151
|
+
_sleep = defaultSleep,
|
|
152
|
+
_nowMs = Date.now,
|
|
153
|
+
} = {}) {
|
|
154
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
155
|
+
const normalizedAgentId = normalizeComparableId(agentId) || "cli-user";
|
|
156
|
+
if (!normalizedSessionId) {
|
|
157
|
+
throw new Error("session id is required.");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const cursorSuffix = listenCursorSuffix(normalizedAgentId);
|
|
161
|
+
let cursor =
|
|
162
|
+
typeof since === "string" || since === null
|
|
163
|
+
? normalizeString(since) || null
|
|
164
|
+
: await _readCursor(normalizedSessionId, { targetPath, suffix: cursorSuffix });
|
|
165
|
+
let primed = Boolean(cursor) || Boolean(replay);
|
|
166
|
+
let pollCount = 0;
|
|
167
|
+
let emitted = 0;
|
|
168
|
+
let matched = 0;
|
|
169
|
+
let persistedCursor = false;
|
|
170
|
+
let lastReason = "";
|
|
171
|
+
const maxPollCount = normalizePositiveInteger(maxPolls, 0);
|
|
172
|
+
const pollLimit = normalizePositiveInteger(limit, 200);
|
|
173
|
+
const sleepMs = Math.max(1, normalizePositiveInteger(intervalSeconds, 60)) * 1000;
|
|
174
|
+
const startedAtMs = Number(_nowMs()) || Date.now();
|
|
175
|
+
|
|
176
|
+
while (!signal?.aborted) {
|
|
177
|
+
pollCount += 1;
|
|
178
|
+
const result = await _poll(normalizedSessionId, {
|
|
179
|
+
targetPath,
|
|
180
|
+
since: cursor,
|
|
181
|
+
limit: pollLimit,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (result?.ok) {
|
|
185
|
+
lastReason = "";
|
|
186
|
+
const events = Array.isArray(result.events) ? result.events : [];
|
|
187
|
+
const shouldEmitBatch = primed || Boolean(replay);
|
|
188
|
+
for (const event of events) {
|
|
189
|
+
if (!eventMatchesAgent(event, normalizedAgentId)) continue;
|
|
190
|
+
matched += 1;
|
|
191
|
+
if (!shouldEmitBatch && eventTimestampMs(event) < startedAtMs) continue;
|
|
192
|
+
await onEvent(event);
|
|
193
|
+
emitted += 1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const nextCursor = normalizeString(result.cursor) || cursorFromEvents(events, cursor);
|
|
197
|
+
if (nextCursor && nextCursor !== cursor) {
|
|
198
|
+
const writeResult = await _writeCursor(normalizedSessionId, nextCursor, {
|
|
199
|
+
targetPath,
|
|
200
|
+
suffix: cursorSuffix,
|
|
201
|
+
}).catch(() => null);
|
|
202
|
+
persistedCursor = Boolean(writeResult?.written) || persistedCursor;
|
|
203
|
+
cursor = nextCursor;
|
|
204
|
+
}
|
|
205
|
+
primed = true;
|
|
206
|
+
} else {
|
|
207
|
+
lastReason = normalizeString(result?.reason) || "poll_failed";
|
|
208
|
+
await onError({
|
|
209
|
+
ok: false,
|
|
210
|
+
reason: lastReason,
|
|
211
|
+
cursor: result?.cursor || cursor || null,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (maxPollCount > 0 && pollCount >= maxPollCount) break;
|
|
216
|
+
try {
|
|
217
|
+
await _sleep(sleepMs, { signal });
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (shouldAbort(error, signal)) break;
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
ok: true,
|
|
226
|
+
sessionId: normalizedSessionId,
|
|
227
|
+
agentId: normalizedAgentId,
|
|
228
|
+
cursor,
|
|
229
|
+
cursorSuffix,
|
|
230
|
+
pollCount,
|
|
231
|
+
matched,
|
|
232
|
+
emitted,
|
|
233
|
+
persistedCursor,
|
|
234
|
+
reason: lastReason,
|
|
235
|
+
};
|
|
236
|
+
}
|