sentinelayer-cli 0.19.0 → 0.20.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 +1 -1
- package/src/commands/ai/identity-lifecycle.js +14 -2
- package/src/commands/mcp.js +60 -0
- package/src/commands/session.js +1255 -25
- package/src/legacy-cli.js +16 -11
- package/src/mcp/registry.js +151 -0
- package/src/mcp/session-stdio-server.js +977 -0
- package/src/scan/generator.js +3 -2
- package/src/session/agent-registry.js +118 -0
- package/src/session/checkpoints.js +71 -1
- package/src/session/coordination-guidance.js +3 -2
- package/src/session/listener.js +302 -68
- package/src/session/pricing-ledger.js +34 -4
- package/src/session/recap.js +4 -2
- package/src/session/sync.js +278 -0
- package/src/session/transcript.js +86 -36
- package/src/session/usage.js +5 -5
- package/src/session/wake/claude.js +175 -0
- package/src/session/wake/codex.js +394 -0
- package/src/session/wake/cursor-store.js +69 -0
- package/src/session/wake/dispatcher.js +184 -0
- package/src/session/wake/pump.js +135 -0
- package/src/session/wake/registry.js +80 -0
- package/src/session/wake/resolve-target.js +146 -0
- package/src/session/wake/sentid.js +103 -0
package/src/scan/generator.js
CHANGED
|
@@ -2,7 +2,7 @@ import YAML from "yaml";
|
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_SCAN_WORKFLOW_PATH = ".github/workflows/omar-gate.yml";
|
|
4
4
|
export const DEFAULT_SCAN_SECRET_NAME = "SENTINELAYER_TOKEN";
|
|
5
|
-
export const SENTINELAYER_ACTION_REF = "mrrCarter/sentinelayer-v1-action@
|
|
5
|
+
export const SENTINELAYER_ACTION_REF = "mrrCarter/sentinelayer-v1-action@03d7369cba7de2e9f15b959275c982111f0ee493";
|
|
6
6
|
export const SUPPORTED_E2E_HINTS = Object.freeze(["auto", "yes", "no"]);
|
|
7
7
|
export const SUPPORTED_PLAYWRIGHT_MODES = Object.freeze(["auto", "off", "baseline", "audit"]);
|
|
8
8
|
|
|
@@ -228,11 +228,12 @@ export function buildSecurityReviewWorkflow({ secretName = DEFAULT_SCAN_SECRET_N
|
|
|
228
228
|
sentinelayer_token: `\${{ secrets.${normalizedSecret} }}`,
|
|
229
229
|
sentinelayer_managed_llm: "false",
|
|
230
230
|
openai_api_key: "${{ secrets.OPENAI_API_KEY }}",
|
|
231
|
+
google_api_key: "${{ secrets.GOOGLE_API_KEY }}",
|
|
231
232
|
scan_mode: profile.scanMode || "deep",
|
|
232
233
|
severity_gate: profile.severityGate || "P1",
|
|
233
234
|
model: "gpt-5.3-codex",
|
|
234
235
|
codex_model: "gpt-5.3-codex",
|
|
235
|
-
model_fallback: "
|
|
236
|
+
model_fallback: "gemini-2.5-flash",
|
|
236
237
|
llm_failure_policy: "block",
|
|
237
238
|
playwright_mode: profile.playwrightMode || "off",
|
|
238
239
|
sbom_mode: profile.sbomMode || "off",
|
|
@@ -185,6 +185,43 @@ export function generateAgentId(modelName) {
|
|
|
185
185
|
return `${prefix}-${suffix}`;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
function isActiveAgentSnapshot(snapshot) {
|
|
189
|
+
if (!snapshot || typeof snapshot !== "object") {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
if (snapshot.active === false) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
if (normalizeString(snapshot.leftAt)) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function chooseAgentRefreshModel(value, existingModel) {
|
|
202
|
+
const candidate = normalizeString(value);
|
|
203
|
+
const existing = normalizeString(existingModel);
|
|
204
|
+
if (!candidate) {
|
|
205
|
+
return existing || "unknown";
|
|
206
|
+
}
|
|
207
|
+
if (existing && ["cli", "unknown"].includes(candidate.toLowerCase())) {
|
|
208
|
+
return existing;
|
|
209
|
+
}
|
|
210
|
+
return candidate;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function chooseAgentRefreshRole(value, existingRole) {
|
|
214
|
+
const candidate = normalizeString(value);
|
|
215
|
+
const existing = normalizeString(existingRole);
|
|
216
|
+
if (!candidate) {
|
|
217
|
+
return existing || "observer";
|
|
218
|
+
}
|
|
219
|
+
if (existing && candidate.toLowerCase() === "observer") {
|
|
220
|
+
return existing;
|
|
221
|
+
}
|
|
222
|
+
return candidate;
|
|
223
|
+
}
|
|
224
|
+
|
|
188
225
|
// In-process registry of agents registered by *this* CLI process. The
|
|
189
226
|
// dashboard treats any participant without a terminal agent_leave /
|
|
190
227
|
// agent_killed / session_killed event as "active". When a CLI exits via
|
|
@@ -275,6 +312,39 @@ export async function registerAgent(
|
|
|
275
312
|
}
|
|
276
313
|
|
|
277
314
|
const snapshotPath = buildAgentSnapshotPath(paths, resolvedAgentId);
|
|
315
|
+
const existing = await readAgentSnapshot(snapshotPath);
|
|
316
|
+
|
|
317
|
+
if (isActiveAgentSnapshot(existing)) {
|
|
318
|
+
const snapshot = normalizeAgentSnapshot(
|
|
319
|
+
{
|
|
320
|
+
...existing,
|
|
321
|
+
sessionId: paths.sessionId,
|
|
322
|
+
agentId: resolvedAgentId,
|
|
323
|
+
model: chooseAgentRefreshModel(model, existing.model),
|
|
324
|
+
role: chooseAgentRefreshRole(role, existing.role),
|
|
325
|
+
status: normalizeString(existing.status) || "idle",
|
|
326
|
+
detail: normalizeString(existing.detail) || "",
|
|
327
|
+
file: normalizeString(existing.file) || null,
|
|
328
|
+
lastActivityAt: nowIso,
|
|
329
|
+
leftAt: null,
|
|
330
|
+
leaveReason: null,
|
|
331
|
+
active: true,
|
|
332
|
+
updatedAt: nowIso,
|
|
333
|
+
},
|
|
334
|
+
nowIso
|
|
335
|
+
);
|
|
336
|
+
await writeAgentSnapshot(snapshotPath, snapshot);
|
|
337
|
+
if (trackProcessExit) {
|
|
338
|
+
_trackLocalAgent(paths.sessionId, snapshot.agentId, targetPath);
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
...snapshot,
|
|
342
|
+
snapshotPath,
|
|
343
|
+
emittedJoinEvent: false,
|
|
344
|
+
emittedContextBriefing: false,
|
|
345
|
+
refreshedExistingAgent: true,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
278
348
|
|
|
279
349
|
const snapshot = normalizeAgentSnapshot(
|
|
280
350
|
{
|
|
@@ -328,6 +398,54 @@ export async function registerAgent(
|
|
|
328
398
|
}).catch(() => {});
|
|
329
399
|
}
|
|
330
400
|
|
|
401
|
+
return {
|
|
402
|
+
...snapshot,
|
|
403
|
+
snapshotPath,
|
|
404
|
+
emittedJoinEvent: true,
|
|
405
|
+
emittedContextBriefing: normalizeString(snapshot.agentId).toLowerCase() !== "senti",
|
|
406
|
+
refreshedExistingAgent: false,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export async function rememberAgentIdentity(
|
|
411
|
+
sessionId,
|
|
412
|
+
{
|
|
413
|
+
agentId = "",
|
|
414
|
+
model = "",
|
|
415
|
+
role = "observer",
|
|
416
|
+
targetPath = process.cwd(),
|
|
417
|
+
} = {}
|
|
418
|
+
) {
|
|
419
|
+
const paths = resolveSessionPaths(sessionId, { targetPath });
|
|
420
|
+
const nowIso = new Date().toISOString();
|
|
421
|
+
const resolvedAgentId = normalizeString(agentId);
|
|
422
|
+
if (!resolvedAgentId) {
|
|
423
|
+
throw new Error("agentId is required.");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const snapshotPath = buildAgentSnapshotPath(paths, resolvedAgentId);
|
|
427
|
+
const existing = await readAgentSnapshot(snapshotPath);
|
|
428
|
+
const snapshot = normalizeAgentSnapshot(
|
|
429
|
+
{
|
|
430
|
+
...(existing || {}),
|
|
431
|
+
sessionId: paths.sessionId,
|
|
432
|
+
agentId: resolvedAgentId,
|
|
433
|
+
model: normalizeString(model) || normalizeString(existing?.model) || "unknown",
|
|
434
|
+
role: normalizeString(role) || normalizeString(existing?.role) || "observer",
|
|
435
|
+
status: normalizeString(existing?.status) || "idle",
|
|
436
|
+
detail: normalizeString(existing?.detail) || "",
|
|
437
|
+
file: normalizeString(existing?.file) || null,
|
|
438
|
+
joinedAt: normalizeString(existing?.joinedAt) || nowIso,
|
|
439
|
+
lastActivityAt: nowIso,
|
|
440
|
+
leftAt: null,
|
|
441
|
+
leaveReason: null,
|
|
442
|
+
active: true,
|
|
443
|
+
updatedAt: nowIso,
|
|
444
|
+
},
|
|
445
|
+
nowIso
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
await writeAgentSnapshot(snapshotPath, snapshot);
|
|
331
449
|
return {
|
|
332
450
|
...snapshot,
|
|
333
451
|
snapshotPath,
|
|
@@ -9,10 +9,12 @@ const DEFAULT_CHECKPOINT_LIMIT = 100;
|
|
|
9
9
|
const MAX_CHECKPOINT_LIMIT = 200;
|
|
10
10
|
const DEFAULT_MIN_EVENTS = 20;
|
|
11
11
|
const DEFAULT_MAX_EVENTS = 80;
|
|
12
|
+
const DEFAULT_BATCH_MAX_CHECKPOINTS = 5;
|
|
13
|
+
const MAX_BATCH_MAX_CHECKPOINTS = 50;
|
|
12
14
|
const DEFAULT_CREATED_BY_AGENT_ID = "senti";
|
|
13
15
|
|
|
14
16
|
function normalizeString(value) {
|
|
15
|
-
return String(value
|
|
17
|
+
return String(value ?? "").trim();
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
function normalizeApiUrl(value) {
|
|
@@ -36,6 +38,15 @@ function normalizeLimit(value) {
|
|
|
36
38
|
return Math.max(1, Math.min(MAX_CHECKPOINT_LIMIT, parsed));
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
function normalizeBatchMaxCheckpoints(value) {
|
|
42
|
+
const parsed = parsePositiveInteger(
|
|
43
|
+
value,
|
|
44
|
+
"max-checkpoints",
|
|
45
|
+
DEFAULT_BATCH_MAX_CHECKPOINTS,
|
|
46
|
+
);
|
|
47
|
+
return Math.max(1, Math.min(MAX_BATCH_MAX_CHECKPOINTS, parsed));
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
function stableHash(value) {
|
|
40
51
|
return crypto
|
|
41
52
|
.createHash("sha256")
|
|
@@ -97,6 +108,12 @@ function buildInvocationIdempotencyKey(operation) {
|
|
|
97
108
|
return `sl_cli_session_checkpoint_${normalizeString(operation) || "mutation"}_${suffix}`;
|
|
98
109
|
}
|
|
99
110
|
|
|
111
|
+
function buildBatchIdempotencyKey(baseKey, index) {
|
|
112
|
+
const normalizedBase = normalizeString(baseKey);
|
|
113
|
+
if (!normalizedBase) return "";
|
|
114
|
+
return `${normalizedBase}:${Number(index) + 1}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
100
117
|
function normalizeReason(value, fallbackValue = "checkpoint_generate_failed") {
|
|
101
118
|
return (
|
|
102
119
|
normalizeString(value)
|
|
@@ -345,6 +362,57 @@ export async function generateSessionCheckpoint(sessionId, options = {}) {
|
|
|
345
362
|
};
|
|
346
363
|
}
|
|
347
364
|
|
|
365
|
+
export async function generateSessionCheckpointBatch(sessionId, options = {}) {
|
|
366
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
367
|
+
if (!normalizedSessionId) {
|
|
368
|
+
throw new Error("session id is required.");
|
|
369
|
+
}
|
|
370
|
+
const maxCheckpoints = normalizeBatchMaxCheckpoints(options.maxCheckpoints);
|
|
371
|
+
const results = [];
|
|
372
|
+
const seenCheckpointIds = new Set();
|
|
373
|
+
let stoppedReason = "max_checkpoints";
|
|
374
|
+
|
|
375
|
+
for (let index = 0; index < maxCheckpoints; index += 1) {
|
|
376
|
+
const result = await generateSessionCheckpoint(normalizedSessionId, {
|
|
377
|
+
...options,
|
|
378
|
+
idempotencyKey: buildBatchIdempotencyKey(options.idempotencyKey, index),
|
|
379
|
+
});
|
|
380
|
+
const normalized = normalizeCheckpointGenerationResult(result);
|
|
381
|
+
results.push(normalized);
|
|
382
|
+
|
|
383
|
+
if (!normalized.checkpoint || normalized.duplicate || !normalized.created) {
|
|
384
|
+
stoppedReason = normalized.reason || (normalized.duplicate ? "duplicate_checkpoint" : "not_created");
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (normalized.checkpointId) {
|
|
389
|
+
if (seenCheckpointIds.has(normalized.checkpointId)) {
|
|
390
|
+
stoppedReason = "repeated_checkpoint";
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
seenCheckpointIds.add(normalized.checkpointId);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const created = results.filter((result) => result.created && !result.duplicate && result.checkpoint);
|
|
398
|
+
const duplicates = results.filter((result) => result.duplicate);
|
|
399
|
+
const lastResult = results.at(-1) || null;
|
|
400
|
+
return {
|
|
401
|
+
ok: results.every((result) => result.ok !== false),
|
|
402
|
+
sessionId: normalizedSessionId,
|
|
403
|
+
apiUrl: results.find((result) => result.apiUrl)?.apiUrl || null,
|
|
404
|
+
catchUp: true,
|
|
405
|
+
maxCheckpoints,
|
|
406
|
+
attemptedCount: results.length,
|
|
407
|
+
createdCount: created.length,
|
|
408
|
+
duplicateCount: duplicates.length,
|
|
409
|
+
checkpointIds: created.map((result) => result.checkpointId).filter(Boolean),
|
|
410
|
+
stoppedReason,
|
|
411
|
+
lastResult,
|
|
412
|
+
results,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
348
416
|
export async function generateSessionCheckpointBestEffort(sessionId, options = {}) {
|
|
349
417
|
const normalizedSessionId = normalizeString(sessionId);
|
|
350
418
|
if (!normalizedSessionId) {
|
|
@@ -375,6 +443,8 @@ export async function generateSessionCheckpointBestEffort(sessionId, options = {
|
|
|
375
443
|
|
|
376
444
|
export {
|
|
377
445
|
DEFAULT_CREATED_BY_AGENT_ID,
|
|
446
|
+
DEFAULT_BATCH_MAX_CHECKPOINTS,
|
|
378
447
|
DEFAULT_MAX_EVENTS,
|
|
379
448
|
DEFAULT_MIN_EVENTS,
|
|
449
|
+
MAX_BATCH_MAX_CHECKPOINTS,
|
|
380
450
|
};
|
|
@@ -5,8 +5,9 @@ export const COORDINATION_ETIQUETTE_ITEMS = Object.freeze([
|
|
|
5
5
|
"When you have an agent grant, post agent updates with `sl session post-agent <id> \"status: <update>\" --agent <your-agent-id>` so they render as the agent, not the human relay.",
|
|
6
6
|
"Before implementation, post a short plan and file claims with `sl session say <id> \"plan: <scope>; files: <paths>\"`.",
|
|
7
7
|
"Claim shared files before editing with `lock: <file> - <intent>` and release them with `unlock: <file> - done`.",
|
|
8
|
-
"Run a background listener for replies
|
|
9
|
-
"
|
|
8
|
+
"Run a background/secondary listener for replies with `sl session listen --session <id> --agent <your-name> --interval 60 --active-interval 5 --emit ndjson --no-presence`; this idles at 60s, switches to 5s after human activity, and avoids durable listener heartbeat noise. `session listen` is only a delivery cursor, not a grounding command; join or recap before acting. For your primary interactive listener, omit `--no-presence` so other participants can see you online. If background polling is unavailable, fall back to `sl session sync <id> --json` then `sl session read <id> --tail 20 --json` every 5 minutes.",
|
|
9
|
+
"For long-lived rooms, make sure exactly one visible participant owns the Senti daemon: `sl session daemon --session <id> --recap-interval 300 --checkpoint-interval 60`. If no durable recap/checkpoint is appearing, run `sl session recap now <id> --remote --agent <your-name> --json` before posting a long plan.",
|
|
10
|
+
"Use message actions for low-noise coordination before posting a new top-level message: `sl session react <id> ack --target-sequence <n>` only when an explicit ACK is useful, `sl session action <id> working_on --target-sequence <n>` for ownership, and `sl session reply <id> <sequence> \"<message>\"` / `sl session comment <id> <sequence> \"<message>\"` for threaded responses. Read receipts are automatic when you use `sl session read <id> --remote --agent <your-name>`; reserve `sl session view <id> <sequence>` for repair/backfill. Run `sl session actions` for the full list.",
|
|
10
11
|
"Search before asking peers to restate context: `sl session search <id> \"<topic>\" --limit 10`.",
|
|
11
12
|
"Run `sl review --diff` after each finished file or PR-ready diff and post the result summary back to the session.",
|
|
12
13
|
"Post findings through `sl session say <id> \"finding: [P2] <title> in <file>:<line>\"` with enough context for a peer to act.",
|