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
package/src/commands/session.js
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
listActiveSessions,
|
|
48
48
|
listAllSessions,
|
|
49
49
|
recordSessionProvisionedIdentities,
|
|
50
|
+
updateSessionTitle,
|
|
50
51
|
} from "../session/store.js";
|
|
51
52
|
import { appendToStream, readStream, tailStream } from "../session/stream.js";
|
|
52
53
|
import { readSessionPreview } from "../session/preview.js";
|
|
@@ -57,6 +58,8 @@ import {
|
|
|
57
58
|
} from "../session/sync.js";
|
|
58
59
|
import { hydrateSessionFromRemote } from "../session/remote-hydrate.js";
|
|
59
60
|
import { mergeLiveSources } from "../session/live-source.js";
|
|
61
|
+
import { listenSessionEvents } from "../session/listener.js";
|
|
62
|
+
import { deriveSessionTitle } from "../session/senti-naming.js";
|
|
60
63
|
import {
|
|
61
64
|
buildDashboardUrl,
|
|
62
65
|
buildTemplateLaunchPlan,
|
|
@@ -88,6 +91,233 @@ function parsePositiveInteger(rawValue, field, fallbackValue) {
|
|
|
88
91
|
return Math.floor(normalized);
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
function normalizeComparablePath(value) {
|
|
95
|
+
return String(value || "")
|
|
96
|
+
.trim()
|
|
97
|
+
.replace(/\\/g, "/")
|
|
98
|
+
.replace(/\/+$/g, "")
|
|
99
|
+
.toLowerCase();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function latestSessionActivityMs(entry = {}) {
|
|
103
|
+
for (const key of ["lastInteractionAt", "lastActivityAt", "createdAt"]) {
|
|
104
|
+
const epoch = Date.parse(normalizeString(entry[key]));
|
|
105
|
+
if (Number.isFinite(epoch)) return epoch;
|
|
106
|
+
}
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function remoteSessionLookupDisabled() {
|
|
111
|
+
return String(process.env.SENTINELAYER_SKIP_REMOTE_SYNC || "").trim() === "1";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function mergeResumeCandidate(existing, incoming) {
|
|
115
|
+
if (!existing) return incoming;
|
|
116
|
+
const existingActivity = Number(existing._activityMs || 0);
|
|
117
|
+
const incomingActivity = Number(incoming._activityMs || 0);
|
|
118
|
+
const preferIncomingPaths = existing._source !== "local" && incoming._source === "local";
|
|
119
|
+
const base = preferIncomingPaths ? incoming : existing;
|
|
120
|
+
const other = preferIncomingPaths ? existing : incoming;
|
|
121
|
+
return {
|
|
122
|
+
...base,
|
|
123
|
+
title: normalizeString(base.title) || normalizeString(other.title) || null,
|
|
124
|
+
lastActivityAt:
|
|
125
|
+
normalizeString(incoming.lastActivityAt) || normalizeString(existing.lastActivityAt) || null,
|
|
126
|
+
lastInteractionAt:
|
|
127
|
+
normalizeString(incoming.lastInteractionAt) || normalizeString(existing.lastInteractionAt) || null,
|
|
128
|
+
_activityMs: Math.max(existingActivity, incomingActivity),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function findReusableSessionCandidate({
|
|
133
|
+
targetPath,
|
|
134
|
+
reuseWindowSeconds = 3600,
|
|
135
|
+
resume = true,
|
|
136
|
+
forceNew = false,
|
|
137
|
+
} = {}) {
|
|
138
|
+
if (forceNew || resume === false) return null;
|
|
139
|
+
const cutoffMs = Date.now() - reuseWindowSeconds * 1000;
|
|
140
|
+
const byId = new Map();
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const active = await listActiveSessions({ targetPath });
|
|
144
|
+
for (const entry of active) {
|
|
145
|
+
const activityMs = latestSessionActivityMs(entry);
|
|
146
|
+
if (!activityMs || activityMs < cutoffMs) continue;
|
|
147
|
+
const candidate = {
|
|
148
|
+
...entry,
|
|
149
|
+
_source: "local",
|
|
150
|
+
_activityMs: activityMs,
|
|
151
|
+
};
|
|
152
|
+
byId.set(entry.sessionId, mergeResumeCandidate(byId.get(entry.sessionId), candidate));
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
/* local lookup failure is non-fatal */
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!remoteSessionLookupDisabled()) {
|
|
159
|
+
try {
|
|
160
|
+
const remote = await listSessionsFromApi({
|
|
161
|
+
targetPath,
|
|
162
|
+
includeArchived: false,
|
|
163
|
+
limit: 50,
|
|
164
|
+
});
|
|
165
|
+
if (remote && remote.ok) {
|
|
166
|
+
const normalizedTarget = normalizeComparablePath(targetPath);
|
|
167
|
+
for (const entry of remote.sessions || []) {
|
|
168
|
+
const codebase = normalizeComparablePath(entry.codebasePath || entry.targetPath);
|
|
169
|
+
if (!codebase || codebase !== normalizedTarget) continue;
|
|
170
|
+
if (entry.archiveStatus && entry.archiveStatus !== "active") continue;
|
|
171
|
+
const activityMs = latestSessionActivityMs(entry);
|
|
172
|
+
if (!activityMs || activityMs < cutoffMs) continue;
|
|
173
|
+
const candidate = {
|
|
174
|
+
sessionId: entry.sessionId,
|
|
175
|
+
createdAt: entry.createdAt,
|
|
176
|
+
lastActivityAt: entry.lastActivityAt,
|
|
177
|
+
expiresAt: entry.expiresAt,
|
|
178
|
+
status: entry.status || "active",
|
|
179
|
+
template: entry.templateName || null,
|
|
180
|
+
title: entry.title || null,
|
|
181
|
+
_source: "remote",
|
|
182
|
+
_activityMs: activityMs,
|
|
183
|
+
};
|
|
184
|
+
byId.set(entry.sessionId, mergeResumeCandidate(byId.get(entry.sessionId), candidate));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
/* remote lookup failure is non-fatal */
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const candidates = [...byId.values()];
|
|
193
|
+
candidates.sort((left, right) => Number(right._activityMs || 0) - Number(left._activityMs || 0));
|
|
194
|
+
return candidates[0] || null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function pushSessionTitleToApi(sessionId, title, { targetPath } = {}) {
|
|
198
|
+
const normalizedTitle = normalizeString(title);
|
|
199
|
+
if (!normalizedTitle || remoteSessionLookupDisabled()) return;
|
|
200
|
+
try {
|
|
201
|
+
const session = await resolveActiveAuthSession({
|
|
202
|
+
cwd: targetPath,
|
|
203
|
+
env: process.env,
|
|
204
|
+
autoRotate: false,
|
|
205
|
+
});
|
|
206
|
+
if (!session?.token || !session?.apiUrl) return;
|
|
207
|
+
const apiUrl = String(session.apiUrl).replace(/\/+$/, "");
|
|
208
|
+
await requestJsonMutation(
|
|
209
|
+
`${apiUrl}/api/v1/sessions/${encodeURIComponent(sessionId)}/title`,
|
|
210
|
+
{
|
|
211
|
+
method: "POST",
|
|
212
|
+
operationName: "session.set_title",
|
|
213
|
+
headers: { Authorization: `Bearer ${session.token}` },
|
|
214
|
+
body: { title: normalizedTitle },
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
} catch {
|
|
218
|
+
/* best-effort */
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function ensureWorkspaceSession({
|
|
223
|
+
targetPath,
|
|
224
|
+
ttlSeconds = DEFAULT_TTL_SECONDS,
|
|
225
|
+
template = null,
|
|
226
|
+
title = "",
|
|
227
|
+
resume = true,
|
|
228
|
+
forceNew = false,
|
|
229
|
+
reuseWindowSeconds = 3600,
|
|
230
|
+
} = {}) {
|
|
231
|
+
const titleArg = normalizeString(title);
|
|
232
|
+
const fallbackTitle = deriveSessionTitle(targetPath);
|
|
233
|
+
const startedAt = Date.now();
|
|
234
|
+
const resumedCandidate = await findReusableSessionCandidate({
|
|
235
|
+
targetPath,
|
|
236
|
+
reuseWindowSeconds,
|
|
237
|
+
resume,
|
|
238
|
+
forceNew,
|
|
239
|
+
});
|
|
240
|
+
let created;
|
|
241
|
+
const resumeTitle =
|
|
242
|
+
titleArg || normalizeString(resumedCandidate?.title) || fallbackTitle;
|
|
243
|
+
|
|
244
|
+
if (resumedCandidate) {
|
|
245
|
+
if (resumedCandidate._source === "remote" && !resumedCandidate.sessionDir) {
|
|
246
|
+
created = await createSession({
|
|
247
|
+
targetPath,
|
|
248
|
+
ttlSeconds,
|
|
249
|
+
sessionId: resumedCandidate.sessionId,
|
|
250
|
+
title: resumeTitle,
|
|
251
|
+
createdAt: resumedCandidate.createdAt,
|
|
252
|
+
expiresAt: resumedCandidate.expiresAt,
|
|
253
|
+
lastInteractionAt:
|
|
254
|
+
resumedCandidate.lastInteractionAt ||
|
|
255
|
+
resumedCandidate.lastActivityAt ||
|
|
256
|
+
resumedCandidate.createdAt,
|
|
257
|
+
});
|
|
258
|
+
} else {
|
|
259
|
+
created = {
|
|
260
|
+
sessionId: resumedCandidate.sessionId,
|
|
261
|
+
sessionDir: resumedCandidate.sessionDir || null,
|
|
262
|
+
metadataPath: resumedCandidate.metadataPath || null,
|
|
263
|
+
streamPath: resumedCandidate.streamPath || null,
|
|
264
|
+
createdAt: resumedCandidate.createdAt,
|
|
265
|
+
updatedAt: resumedCandidate.updatedAt || null,
|
|
266
|
+
lastInteractionAt: resumedCandidate.lastInteractionAt || null,
|
|
267
|
+
expiresAt: resumedCandidate.expiresAt,
|
|
268
|
+
elapsedTimer: resumedCandidate.elapsedTimer || 0,
|
|
269
|
+
renewalCount: resumedCandidate.renewalCount || 0,
|
|
270
|
+
status: resumedCandidate.status || "active",
|
|
271
|
+
template: resumedCandidate.template || null,
|
|
272
|
+
title: normalizeString(resumedCandidate.title) || null,
|
|
273
|
+
codebaseContext: resumedCandidate.codebaseContext || null,
|
|
274
|
+
};
|
|
275
|
+
if (resumeTitle && resumeTitle !== created.title) {
|
|
276
|
+
const updated = await updateSessionTitle(created.sessionId, {
|
|
277
|
+
targetPath,
|
|
278
|
+
title: resumeTitle,
|
|
279
|
+
}).catch(() => null);
|
|
280
|
+
if (updated) {
|
|
281
|
+
created = {
|
|
282
|
+
...created,
|
|
283
|
+
...updated,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
created = await createSession({
|
|
290
|
+
targetPath,
|
|
291
|
+
ttlSeconds,
|
|
292
|
+
template,
|
|
293
|
+
title: titleArg || fallbackTitle,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const effectiveTitle = titleArg || normalizeString(created.title) || fallbackTitle;
|
|
298
|
+
const titleAuto = !titleArg && !resumedCandidate;
|
|
299
|
+
const shouldPushTitle = Boolean(
|
|
300
|
+
titleArg ||
|
|
301
|
+
titleAuto ||
|
|
302
|
+
(resumedCandidate && effectiveTitle && !normalizeString(resumedCandidate.title))
|
|
303
|
+
);
|
|
304
|
+
if (shouldPushTitle) {
|
|
305
|
+
void pushSessionTitleToApi(created.sessionId, effectiveTitle, { targetPath });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
created: {
|
|
310
|
+
...created,
|
|
311
|
+
title: effectiveTitle || null,
|
|
312
|
+
resumed: Boolean(resumedCandidate),
|
|
313
|
+
},
|
|
314
|
+
resumedCandidate,
|
|
315
|
+
durationMs: Date.now() - startedAt,
|
|
316
|
+
title: effectiveTitle || null,
|
|
317
|
+
titleAuto,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
91
321
|
function normalizeAgentId(value, fallbackValue = "cli-user") {
|
|
92
322
|
const normalized = normalizeString(value)
|
|
93
323
|
.toLowerCase()
|
|
@@ -278,6 +508,15 @@ export function registerSessionCommand(program) {
|
|
|
278
508
|
"--force-new",
|
|
279
509
|
"Always create a new session even if a recent active one exists for this workspace",
|
|
280
510
|
)
|
|
511
|
+
.option(
|
|
512
|
+
"--resume",
|
|
513
|
+
"Reuse the most recent active session for this workspace when one is inside the reuse window",
|
|
514
|
+
true,
|
|
515
|
+
)
|
|
516
|
+
.option(
|
|
517
|
+
"--no-resume",
|
|
518
|
+
"Disable automatic resume and mint a new session unless --force-new is also present",
|
|
519
|
+
)
|
|
281
520
|
.option(
|
|
282
521
|
"--reuse-window-seconds <seconds>",
|
|
283
522
|
"Window in which an existing active session for this workspace will be reused (default 3600 = 1h)",
|
|
@@ -301,91 +540,22 @@ export function registerSessionCommand(program) {
|
|
|
301
540
|
"reuse-window-seconds",
|
|
302
541
|
3600,
|
|
303
542
|
);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
return Number.isFinite(createdMs) && createdMs >= cutoffMs;
|
|
318
|
-
});
|
|
319
|
-
candidates.sort((a, b) =>
|
|
320
|
-
String(b.lastActivityAt || b.createdAt || "").localeCompare(
|
|
321
|
-
String(a.lastActivityAt || a.createdAt || ""),
|
|
322
|
-
),
|
|
323
|
-
);
|
|
324
|
-
if (candidates.length > 0) {
|
|
325
|
-
resumed = candidates[0];
|
|
326
|
-
}
|
|
327
|
-
} catch (error) {
|
|
328
|
-
// listActiveSessions failure is non-fatal; fall through to fresh create.
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const startedAt = Date.now();
|
|
333
|
-
let created;
|
|
334
|
-
if (resumed) {
|
|
335
|
-
// Surface the resumed session's metadata in the same shape
|
|
336
|
-
// createSession returns so downstream code stays unchanged.
|
|
337
|
-
created = {
|
|
338
|
-
sessionId: resumed.sessionId,
|
|
339
|
-
sessionDir: resumed.sessionDir || null,
|
|
340
|
-
metadataPath: resumed.metadataPath || null,
|
|
341
|
-
streamPath: resumed.streamPath || null,
|
|
342
|
-
createdAt: resumed.createdAt,
|
|
343
|
-
expiresAt: resumed.expiresAt,
|
|
344
|
-
elapsedTimer: 0,
|
|
345
|
-
renewalCount: resumed.renewalCount || 0,
|
|
346
|
-
status: resumed.status || "active",
|
|
347
|
-
template: resumed.template || null,
|
|
348
|
-
codebaseContext: resumed.codebaseContext || null,
|
|
349
|
-
resumed: true,
|
|
350
|
-
};
|
|
351
|
-
} else {
|
|
352
|
-
created = await createSession({
|
|
353
|
-
targetPath,
|
|
354
|
-
ttlSeconds,
|
|
355
|
-
template,
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
const durationMs = Date.now() - startedAt;
|
|
543
|
+
const titleArg = normalizeString(options.title);
|
|
544
|
+
const ensured = await ensureWorkspaceSession({
|
|
545
|
+
targetPath,
|
|
546
|
+
ttlSeconds,
|
|
547
|
+
template,
|
|
548
|
+
title: titleArg,
|
|
549
|
+
resume: options.resume !== false,
|
|
550
|
+
forceNew: Boolean(options.forceNew),
|
|
551
|
+
reuseWindowSeconds,
|
|
552
|
+
});
|
|
553
|
+
const created = ensured.created;
|
|
554
|
+
const resumed = Boolean(ensured.resumedCandidate);
|
|
555
|
+
const durationMs = ensured.durationMs;
|
|
359
556
|
const launchPlan = template ? buildTemplateLaunchPlan(created.sessionId, template) : [];
|
|
360
557
|
const dashboardUrl = buildDashboardUrl(created.sessionId);
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
// If the caller passed --title, push it to the API so the web
|
|
364
|
-
// sidebar shows the label (best-effort, non-blocking).
|
|
365
|
-
if (titleArg) {
|
|
366
|
-
void (async () => {
|
|
367
|
-
try {
|
|
368
|
-
const session = await resolveActiveAuthSession({
|
|
369
|
-
cwd: targetPath,
|
|
370
|
-
env: process.env,
|
|
371
|
-
autoRotate: false,
|
|
372
|
-
});
|
|
373
|
-
if (!session?.token || !session?.apiUrl) return;
|
|
374
|
-
const apiUrl = String(session.apiUrl).replace(/\/+$/, "");
|
|
375
|
-
await requestJsonMutation(
|
|
376
|
-
`${apiUrl}/api/v1/sessions/${encodeURIComponent(created.sessionId)}/title`,
|
|
377
|
-
{
|
|
378
|
-
method: "POST",
|
|
379
|
-
operationName: "session.set_title",
|
|
380
|
-
headers: { Authorization: `Bearer ${session.token}` },
|
|
381
|
-
body: { title: titleArg },
|
|
382
|
-
},
|
|
383
|
-
);
|
|
384
|
-
} catch (_error) {
|
|
385
|
-
/* best-effort */
|
|
386
|
-
}
|
|
387
|
-
})();
|
|
388
|
-
}
|
|
558
|
+
const effectiveTitle = ensured.title;
|
|
389
559
|
|
|
390
560
|
const payload = {
|
|
391
561
|
command: "session start",
|
|
@@ -396,7 +566,9 @@ export function registerSessionCommand(program) {
|
|
|
396
566
|
metadataPath: created.metadataPath,
|
|
397
567
|
streamPath: created.streamPath,
|
|
398
568
|
createdAt: created.createdAt,
|
|
569
|
+
updatedAt: created.updatedAt,
|
|
399
570
|
expiresAt: created.expiresAt,
|
|
571
|
+
lastInteractionAt: created.lastInteractionAt,
|
|
400
572
|
ttlSeconds,
|
|
401
573
|
elapsedTimer: created.elapsedTimer,
|
|
402
574
|
renewalCount: created.renewalCount,
|
|
@@ -404,8 +576,9 @@ export function registerSessionCommand(program) {
|
|
|
404
576
|
template: created.template,
|
|
405
577
|
launchPlan,
|
|
406
578
|
dashboardUrl,
|
|
407
|
-
resumed
|
|
408
|
-
title:
|
|
579
|
+
resumed,
|
|
580
|
+
title: effectiveTitle || null,
|
|
581
|
+
titleAuto: Boolean(ensured.titleAuto),
|
|
409
582
|
};
|
|
410
583
|
|
|
411
584
|
// Best-effort admin visibility sync. Session creation remains local-first.
|
|
@@ -415,6 +588,7 @@ export function registerSessionCommand(program) {
|
|
|
415
588
|
status: created.status,
|
|
416
589
|
createdAt: created.createdAt,
|
|
417
590
|
expiresAt: created.expiresAt,
|
|
591
|
+
title: effectiveTitle || null,
|
|
418
592
|
ttlSeconds,
|
|
419
593
|
template: created.template,
|
|
420
594
|
codebaseContext: created.codebaseContext,
|
|
@@ -475,6 +649,62 @@ export function registerSessionCommand(program) {
|
|
|
475
649
|
await program.parseAsync(args, { from: "user" });
|
|
476
650
|
});
|
|
477
651
|
|
|
652
|
+
session
|
|
653
|
+
.command("ensure")
|
|
654
|
+
.description("Join or create the canonical session for this workspace and emit JSON")
|
|
655
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
656
|
+
.option("--title <title>", "Title applied if a new or unnamed resumed session needs one")
|
|
657
|
+
.option(
|
|
658
|
+
"--ttl-seconds <seconds>",
|
|
659
|
+
`Session time-to-live in seconds when a new session is minted (default ${DEFAULT_TTL_SECONDS})`
|
|
660
|
+
)
|
|
661
|
+
.option(
|
|
662
|
+
"--force-new",
|
|
663
|
+
"Always create a new session even if a recent active one exists for this workspace",
|
|
664
|
+
)
|
|
665
|
+
.option(
|
|
666
|
+
"--resume",
|
|
667
|
+
"Reuse the most recent active session for this workspace when one is inside the reuse window",
|
|
668
|
+
true,
|
|
669
|
+
)
|
|
670
|
+
.option("--no-resume", "Disable automatic resume and mint a new session")
|
|
671
|
+
.option(
|
|
672
|
+
"--reuse-window-seconds <seconds>",
|
|
673
|
+
"Window in which an existing active session for this workspace will be reused (default 3600 = 1h)",
|
|
674
|
+
"3600",
|
|
675
|
+
)
|
|
676
|
+
.option("--json", "Emit machine-readable output (default for this command)")
|
|
677
|
+
.action(async (options) => {
|
|
678
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
679
|
+
const ttlSeconds = parsePositiveInteger(
|
|
680
|
+
options.ttlSeconds,
|
|
681
|
+
"ttl-seconds",
|
|
682
|
+
DEFAULT_TTL_SECONDS,
|
|
683
|
+
);
|
|
684
|
+
const reuseWindowSeconds = parsePositiveInteger(
|
|
685
|
+
options.reuseWindowSeconds,
|
|
686
|
+
"reuse-window-seconds",
|
|
687
|
+
3600,
|
|
688
|
+
);
|
|
689
|
+
const ensured = await ensureWorkspaceSession({
|
|
690
|
+
targetPath,
|
|
691
|
+
ttlSeconds,
|
|
692
|
+
title: normalizeString(options.title),
|
|
693
|
+
resume: options.resume !== false,
|
|
694
|
+
forceNew: Boolean(options.forceNew),
|
|
695
|
+
reuseWindowSeconds,
|
|
696
|
+
});
|
|
697
|
+
const payload = {
|
|
698
|
+
command: "session ensure",
|
|
699
|
+
targetPath,
|
|
700
|
+
sessionId: ensured.created.sessionId,
|
|
701
|
+
title: ensured.title || null,
|
|
702
|
+
resumed: Boolean(ensured.resumedCandidate),
|
|
703
|
+
dashboardUrl: buildDashboardUrl(ensured.created.sessionId),
|
|
704
|
+
};
|
|
705
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
706
|
+
});
|
|
707
|
+
|
|
478
708
|
session
|
|
479
709
|
.command("set-title <sessionId> <title>")
|
|
480
710
|
.description("Set the human-readable title on a session (visible in web sidebar + transcript).")
|
|
@@ -504,10 +734,15 @@ export function registerSessionCommand(program) {
|
|
|
504
734
|
body: { title: normalizedTitle },
|
|
505
735
|
},
|
|
506
736
|
);
|
|
737
|
+
const localUpdated = await updateSessionTitle(normalizedSessionId, {
|
|
738
|
+
targetPath,
|
|
739
|
+
title: normalizedTitle,
|
|
740
|
+
}).catch(() => null);
|
|
507
741
|
const payload = {
|
|
508
742
|
command: "session set-title",
|
|
509
743
|
sessionId: normalizedSessionId,
|
|
510
744
|
title: normalizedTitle,
|
|
745
|
+
localUpdated: Boolean(localUpdated),
|
|
511
746
|
result,
|
|
512
747
|
};
|
|
513
748
|
if (shouldEmitJson(options, command)) {
|
|
@@ -637,6 +872,7 @@ export function registerSessionCommand(program) {
|
|
|
637
872
|
.command("say <sessionId> <message>")
|
|
638
873
|
.description("Send a message to the session")
|
|
639
874
|
.option("--agent <id>", "Agent id to emit from", "cli-user")
|
|
875
|
+
.option("--to <agent>", "Direct the message to a specific agent id")
|
|
640
876
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
641
877
|
.option("--json", "Emit machine-readable output")
|
|
642
878
|
.action(async (sessionId, message, options, command) => {
|
|
@@ -650,14 +886,19 @@ export function registerSessionCommand(program) {
|
|
|
650
886
|
}
|
|
651
887
|
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
652
888
|
const agentId = normalizeAgentId(options.agent, "cli-user");
|
|
889
|
+
const to = normalizeString(options.to);
|
|
890
|
+
const eventPayload = {
|
|
891
|
+
message: normalizedMessage,
|
|
892
|
+
channel: "session",
|
|
893
|
+
};
|
|
894
|
+
if (to) {
|
|
895
|
+
eventPayload.to = to;
|
|
896
|
+
}
|
|
653
897
|
const event = createAgentEvent({
|
|
654
898
|
event: "session_message",
|
|
655
899
|
agentId,
|
|
656
900
|
sessionId: normalizedSessionId,
|
|
657
|
-
payload:
|
|
658
|
-
message: normalizedMessage,
|
|
659
|
-
channel: "session",
|
|
660
|
-
},
|
|
901
|
+
payload: eventPayload,
|
|
661
902
|
});
|
|
662
903
|
const persisted = await appendToStream(normalizedSessionId, event, {
|
|
663
904
|
targetPath,
|
|
@@ -676,6 +917,93 @@ export function registerSessionCommand(program) {
|
|
|
676
917
|
console.log(formatEventLine(persisted));
|
|
677
918
|
});
|
|
678
919
|
|
|
920
|
+
session
|
|
921
|
+
.command("listen")
|
|
922
|
+
.description("Background-poll a session for events addressed to this agent or broadcast")
|
|
923
|
+
.requiredOption("--session <id>", "Session id to listen to")
|
|
924
|
+
.option(
|
|
925
|
+
"--agent <id>",
|
|
926
|
+
"Agent id to receive messages for",
|
|
927
|
+
process.env.SENTINELAYER_AGENT_ID || "cli-user",
|
|
928
|
+
)
|
|
929
|
+
.option("--interval <seconds>", "Polling interval in seconds (default 60)", "60")
|
|
930
|
+
.option("--emit <format>", "Output format: ndjson or text", "ndjson")
|
|
931
|
+
.option("--limit <n>", "Maximum events to request per poll (default 200)", "200")
|
|
932
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
933
|
+
.option("--since <cursor>", "Override the persisted listen cursor")
|
|
934
|
+
.option("--replay", "Emit matching historical events on the first poll")
|
|
935
|
+
.option("--max-polls <n>", "Stop after N poll cycles (useful for tests and smoke checks)")
|
|
936
|
+
.action(async (options) => {
|
|
937
|
+
const normalizedSessionId = resolveSessionIdOption(options);
|
|
938
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
939
|
+
const agentId = normalizeAgentId(options.agent, "cli-user");
|
|
940
|
+
const intervalSeconds = parsePositiveInteger(options.interval, "interval", 60);
|
|
941
|
+
const limit = parsePositiveInteger(options.limit, "limit", 200);
|
|
942
|
+
const emitFormat = normalizeString(options.emit).toLowerCase() || "ndjson";
|
|
943
|
+
if (!["ndjson", "text"].includes(emitFormat)) {
|
|
944
|
+
throw new Error("--emit must be one of: ndjson, text.");
|
|
945
|
+
}
|
|
946
|
+
const maxPolls =
|
|
947
|
+
options.maxPolls === undefined
|
|
948
|
+
? null
|
|
949
|
+
: parsePositiveInteger(options.maxPolls, "max-polls", 1);
|
|
950
|
+
const since = options.since === undefined ? undefined : String(options.since);
|
|
951
|
+
const ac = new AbortController();
|
|
952
|
+
const onSigint = () => ac.abort();
|
|
953
|
+
process.on("SIGINT", onSigint);
|
|
954
|
+
|
|
955
|
+
if (emitFormat === "text") {
|
|
956
|
+
console.log(
|
|
957
|
+
pc.gray(
|
|
958
|
+
`Listening to session ${normalizedSessionId} as ${agentId}; interval=${intervalSeconds}s. Press Ctrl+C to stop.`,
|
|
959
|
+
),
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
try {
|
|
964
|
+
await listenSessionEvents({
|
|
965
|
+
sessionId: normalizedSessionId,
|
|
966
|
+
targetPath,
|
|
967
|
+
agentId,
|
|
968
|
+
intervalSeconds,
|
|
969
|
+
limit,
|
|
970
|
+
since,
|
|
971
|
+
replay: Boolean(options.replay),
|
|
972
|
+
maxPolls,
|
|
973
|
+
signal: ac.signal,
|
|
974
|
+
onEvent: async (event) => {
|
|
975
|
+
if (emitFormat === "ndjson") {
|
|
976
|
+
console.log(JSON.stringify(event));
|
|
977
|
+
} else {
|
|
978
|
+
console.log(formatEventLine(event));
|
|
979
|
+
}
|
|
980
|
+
},
|
|
981
|
+
onError: async (result) => {
|
|
982
|
+
const reason = normalizeString(result?.reason) || "poll_failed";
|
|
983
|
+
if (emitFormat === "ndjson") {
|
|
984
|
+
console.log(
|
|
985
|
+
JSON.stringify(
|
|
986
|
+
createAgentEvent({
|
|
987
|
+
event: "session_listen_error",
|
|
988
|
+
agentId,
|
|
989
|
+
sessionId: normalizedSessionId,
|
|
990
|
+
payload: {
|
|
991
|
+
reason,
|
|
992
|
+
cursor: result?.cursor || null,
|
|
993
|
+
},
|
|
994
|
+
}),
|
|
995
|
+
),
|
|
996
|
+
);
|
|
997
|
+
} else {
|
|
998
|
+
console.log(pc.yellow(`Listen poll skipped (${reason}).`));
|
|
999
|
+
}
|
|
1000
|
+
},
|
|
1001
|
+
});
|
|
1002
|
+
} finally {
|
|
1003
|
+
process.removeListener("SIGINT", onSigint);
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
|
|
679
1007
|
session
|
|
680
1008
|
.command("read <sessionId>")
|
|
681
1009
|
.description("Read recent session messages")
|
package/src/commands/swarm.js
CHANGED
|
@@ -517,6 +517,9 @@ export function registerSwarmCommand(program) {
|
|
|
517
517
|
.option("--scenario-file <path>", "Scenario DSL file (.sls) for runtime actions")
|
|
518
518
|
.option("--registry-file <path>", "Optional custom swarm registry file (when building plan inline)")
|
|
519
519
|
.option("--agents <ids>", "Comma-separated agent ids for inline plan mode", "security,testing,reliability")
|
|
520
|
+
.option("--agent <id>", "Single agent id alias for --agents")
|
|
521
|
+
.option("--scope <scope>", "Runtime scope alias for --scenario, used by devTestBot")
|
|
522
|
+
.option("--identity-id <id>", "AIdenID identity id for devTestBot runtime")
|
|
520
523
|
.option("--scenario <id>", "Scenario identifier for inline plan mode", "qa_audit")
|
|
521
524
|
.option(
|
|
522
525
|
"--objective <text>",
|
|
@@ -571,7 +574,7 @@ export function registerSwarmCommand(program) {
|
|
|
571
574
|
const registry = await loadSwarmRegistry({
|
|
572
575
|
registryFile: options.registryFile,
|
|
573
576
|
});
|
|
574
|
-
const selected = selectSwarmAgents(registry.agents, options.agents);
|
|
577
|
+
const selected = selectSwarmAgents(registry.agents, options.agent || options.agents);
|
|
575
578
|
if (selected.missing.length > 0) {
|
|
576
579
|
throw new Error(`Unknown agent id(s): ${selected.missing.join(", ")}`);
|
|
577
580
|
}
|
|
@@ -581,7 +584,7 @@ export function registerSwarmCommand(program) {
|
|
|
581
584
|
const selectedAgents = ensureOmarIncluded(registry.agents, selected.selected);
|
|
582
585
|
plan = buildSwarmExecutionPlan({
|
|
583
586
|
targetPath,
|
|
584
|
-
scenario: scenarioIdOverride || options.scenario,
|
|
587
|
+
scenario: scenarioIdOverride || options.scope || options.scenario,
|
|
585
588
|
objective: options.objective,
|
|
586
589
|
agents: selectedAgents,
|
|
587
590
|
maxParallel: parseMaxParallel(options.maxParallel),
|
|
@@ -612,6 +615,8 @@ export function registerSwarmCommand(program) {
|
|
|
612
615
|
execute: Boolean(options.execute),
|
|
613
616
|
maxSteps: parseMaxSteps(options.maxSteps),
|
|
614
617
|
startUrl: startUrlOverride || options.startUrl,
|
|
618
|
+
identityId: options.identityId,
|
|
619
|
+
devTestBotScope: options.scope || scenarioIdOverride || options.scenario,
|
|
615
620
|
playbookActions,
|
|
616
621
|
outputDir: options.outputDir,
|
|
617
622
|
env: process.env,
|
|
@@ -631,6 +636,10 @@ export function registerSwarmCommand(program) {
|
|
|
631
636
|
stop: runtime.stop,
|
|
632
637
|
usage: runtime.usage,
|
|
633
638
|
eventCount: runtime.eventCount,
|
|
639
|
+
findingCount: runtime.findingCount,
|
|
640
|
+
findings: runtime.findings,
|
|
641
|
+
artifactBundles: runtime.artifactBundles,
|
|
642
|
+
devTestBotRuns: runtime.devTestBotRuns,
|
|
634
643
|
runtimeDirectory: runtime.runtimeDirectory,
|
|
635
644
|
runtimeJsonPath: runtime.runtimeJsonPath,
|
|
636
645
|
runtimeMarkdownPath: runtime.runtimeMarkdownPath,
|