sentinelayer-cli 0.4.5 → 0.8.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.
Files changed (72) hide show
  1. package/README.md +16 -18
  2. package/package.json +7 -6
  3. package/src/agents/jules/config/definition.js +13 -62
  4. package/src/agents/jules/config/system-prompt.js +8 -1
  5. package/src/agents/jules/fix-cycle.js +12 -372
  6. package/src/agents/jules/loop.js +116 -26
  7. package/src/agents/jules/pulse.js +10 -327
  8. package/src/agents/jules/stream.js +13 -12
  9. package/src/agents/jules/swarm/orchestrator.js +3 -3
  10. package/src/agents/jules/swarm/sub-agent.js +6 -3
  11. package/src/agents/jules/tools/aidenid-email.js +189 -0
  12. package/src/agents/jules/tools/auth-audit.js +1187 -45
  13. package/src/agents/jules/tools/dispatch.js +25 -12
  14. package/src/agents/jules/tools/file-edit.js +2 -180
  15. package/src/agents/jules/tools/file-read.js +2 -100
  16. package/src/agents/jules/tools/glob.js +2 -168
  17. package/src/agents/jules/tools/grep.js +2 -228
  18. package/src/agents/jules/tools/path-guards.js +2 -161
  19. package/src/agents/jules/tools/runtime-audit.js +6 -2
  20. package/src/agents/jules/tools/shell.js +2 -383
  21. package/src/agents/persona-visuals.js +64 -0
  22. package/src/agents/shared-tools/dispatch-core.js +320 -0
  23. package/src/agents/shared-tools/file-edit.js +180 -0
  24. package/src/agents/shared-tools/file-read.js +100 -0
  25. package/src/agents/shared-tools/glob.js +168 -0
  26. package/src/agents/shared-tools/grep.js +228 -0
  27. package/src/agents/shared-tools/index.js +46 -0
  28. package/src/agents/shared-tools/path-guards.js +161 -0
  29. package/src/agents/shared-tools/shell.js +383 -0
  30. package/src/ai/aidenid.js +56 -7
  31. package/src/ai/client.js +45 -0
  32. package/src/ai/proxy.js +137 -0
  33. package/src/auth/gate.js +290 -16
  34. package/src/auth/http.js +450 -39
  35. package/src/auth/service.js +262 -47
  36. package/src/auth/session-store.js +475 -21
  37. package/src/cli.js +5 -0
  38. package/src/commands/audit.js +13 -8
  39. package/src/commands/auth.js +53 -9
  40. package/src/commands/omargate.js +10 -2
  41. package/src/commands/scan.js +10 -4
  42. package/src/commands/session.js +590 -0
  43. package/src/commands/spec.js +62 -0
  44. package/src/commands/watch.js +3 -2
  45. package/src/daemon/assignment-ledger.js +196 -0
  46. package/src/daemon/error-worker.js +599 -16
  47. package/src/daemon/fix-cycle.js +384 -0
  48. package/src/daemon/ingest-refresh.js +10 -9
  49. package/src/daemon/jira-lifecycle.js +135 -0
  50. package/src/daemon/pulse.js +327 -0
  51. package/src/daemon/scope-engine.js +1068 -0
  52. package/src/events/schema.js +190 -0
  53. package/src/interactive/index.js +18 -16
  54. package/src/legacy-cli.js +606 -37
  55. package/src/prompt/generator.js +19 -1
  56. package/src/review/ai-review.js +11 -1
  57. package/src/review/local-review.js +75 -19
  58. package/src/review/omargate-interactive.js +68 -0
  59. package/src/review/omargate-orchestrator.js +404 -0
  60. package/src/review/persona-prompts.js +296 -0
  61. package/src/review/scan-modes.js +48 -0
  62. package/src/scan/generator.js +1 -1
  63. package/src/session/agent-registry.js +352 -0
  64. package/src/session/daemon.js +801 -0
  65. package/src/session/paths.js +33 -0
  66. package/src/session/runtime-bridge.js +739 -0
  67. package/src/session/store.js +388 -0
  68. package/src/session/stream.js +325 -0
  69. package/src/spec/generator.js +100 -0
  70. package/src/telemetry/session-tracker.js +148 -32
  71. package/src/telemetry/sync.js +6 -2
  72. package/src/ui/command-hints.js +13 -0
@@ -0,0 +1,590 @@
1
+ import path from "node:path";
2
+ import process from "node:process";
3
+
4
+ import pc from "picocolors";
5
+
6
+ import {
7
+ listAssignments,
8
+ releaseLease,
9
+ } from "../daemon/assignment-ledger.js";
10
+ import { stopScopeEngine } from "../daemon/scope-engine.js";
11
+ import { createAgentEvent } from "../events/schema.js";
12
+ import {
13
+ detectStaleAgents,
14
+ listAgents,
15
+ registerAgent,
16
+ unregisterAgent,
17
+ } from "../session/agent-registry.js";
18
+ import { stopSenti } from "../session/daemon.js";
19
+ import { listRuntimeRuns } from "../session/runtime-bridge.js";
20
+ import {
21
+ createSession,
22
+ DEFAULT_TTL_SECONDS,
23
+ getSession,
24
+ listActiveSessions,
25
+ } from "../session/store.js";
26
+ import { appendToStream, readStream, tailStream } from "../session/stream.js";
27
+
28
+ function shouldEmitJson(options, command) {
29
+ const local = Boolean(options && options.json);
30
+ const globalFromCommand =
31
+ command && command.optsWithGlobals ? Boolean(command.optsWithGlobals().json) : false;
32
+ return local || globalFromCommand;
33
+ }
34
+
35
+ function normalizeString(value) {
36
+ return String(value || "").trim();
37
+ }
38
+
39
+ function parsePositiveInteger(rawValue, field, fallbackValue) {
40
+ if (rawValue === undefined || rawValue === null || String(rawValue).trim() === "") {
41
+ return fallbackValue;
42
+ }
43
+ const normalized = Number(rawValue);
44
+ if (!Number.isFinite(normalized) || normalized <= 0) {
45
+ throw new Error(`${field} must be a positive integer.`);
46
+ }
47
+ return Math.floor(normalized);
48
+ }
49
+
50
+ function normalizeAgentId(value, fallbackValue = "cli-user") {
51
+ const normalized = normalizeString(value)
52
+ .toLowerCase()
53
+ .replace(/[^a-z0-9._-]+/g, "-")
54
+ .replace(/^-+|-+$/g, "");
55
+ return normalized || fallbackValue;
56
+ }
57
+
58
+ function resolveSessionIdOption(options = {}) {
59
+ const sessionId = normalizeString(options.session || options.id);
60
+ if (!sessionId) {
61
+ throw new Error("session id is required (use --session <id>).");
62
+ }
63
+ return sessionId;
64
+ }
65
+
66
+ function formatEventLine(event = {}) {
67
+ const ts = normalizeString(event.ts || event.timestamp);
68
+ const type = normalizeString(event.event || event.type) || "event";
69
+ const agentId = normalizeString(event.agent?.id || event.agentId || "unknown");
70
+ const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
71
+ const message = normalizeString(payload.message || payload.response || payload.alert || payload.reason || "");
72
+ if (message) {
73
+ return `${ts} ${agentId} ${type}: ${message}`;
74
+ }
75
+ return `${ts} ${agentId} ${type}`;
76
+ }
77
+
78
+ async function revokeAgentLeases(sessionId, agentId, { targetPath, reason } = {}) {
79
+ const active = await listAssignments({
80
+ targetPath,
81
+ sessionId,
82
+ agentIdentity: agentId,
83
+ statuses: ["CLAIMED", "IN_PROGRESS"],
84
+ includeExpired: true,
85
+ limit: 500,
86
+ });
87
+ let releasedCount = 0;
88
+ for (const assignment of active.assignments) {
89
+ await releaseLease({
90
+ targetPath,
91
+ sessionId,
92
+ workItemId: assignment.workItemId,
93
+ agentIdentity: agentId,
94
+ status: "QUEUED",
95
+ reason,
96
+ });
97
+ releasedCount += 1;
98
+ }
99
+ return releasedCount;
100
+ }
101
+
102
+ async function emitAgentKilledEvent(sessionId, agentId, {
103
+ targetPath,
104
+ reason,
105
+ leaseRevocations = 0,
106
+ } = {}) {
107
+ const event = createAgentEvent({
108
+ event: "agent_killed",
109
+ agentId,
110
+ sessionId,
111
+ payload: {
112
+ target: agentId,
113
+ reason: normalizeString(reason) || "manual_stop",
114
+ leaseRevocations: Number(leaseRevocations || 0),
115
+ },
116
+ });
117
+ await appendToStream(sessionId, event, { targetPath });
118
+ return event;
119
+ }
120
+
121
+ export function registerSessionCommand(program) {
122
+ const session = program
123
+ .command("session")
124
+ .description("Multi-agent ephemeral coordination sessions");
125
+
126
+ session
127
+ .command("start")
128
+ .description("Create a new persistent session with metadata + NDJSON stream")
129
+ .option("--path <path>", "Workspace path for the session", ".")
130
+ .option(
131
+ "--ttl-seconds <seconds>",
132
+ `Session time-to-live in seconds (default ${DEFAULT_TTL_SECONDS})`,
133
+ String(DEFAULT_TTL_SECONDS)
134
+ )
135
+ .option("--json", "Emit machine-readable output")
136
+ .action(async (options, command) => {
137
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
138
+ const ttlSeconds = parsePositiveInteger(options.ttlSeconds, "ttl-seconds", DEFAULT_TTL_SECONDS);
139
+ const startedAt = Date.now();
140
+ const created = await createSession({
141
+ targetPath,
142
+ ttlSeconds,
143
+ });
144
+ const durationMs = Date.now() - startedAt;
145
+
146
+ const payload = {
147
+ command: "session start",
148
+ targetPath,
149
+ durationMs,
150
+ sessionId: created.sessionId,
151
+ sessionDir: created.sessionDir,
152
+ metadataPath: created.metadataPath,
153
+ streamPath: created.streamPath,
154
+ createdAt: created.createdAt,
155
+ expiresAt: created.expiresAt,
156
+ elapsedTimer: created.elapsedTimer,
157
+ renewalCount: created.renewalCount,
158
+ status: created.status,
159
+ };
160
+
161
+ if (shouldEmitJson(options, command)) {
162
+ console.log(JSON.stringify(payload, null, 2));
163
+ return;
164
+ }
165
+
166
+ console.log(pc.bold("Session created"));
167
+ console.log(pc.gray(`Session: ${created.sessionId}`));
168
+ console.log(pc.gray(`Stream: ${created.streamPath}`));
169
+ console.log(pc.gray(`Created in ${durationMs}ms`));
170
+ console.log(
171
+ `status=${created.status} created_at=${created.createdAt} expires_at=${created.expiresAt} ttl_seconds=${ttlSeconds}`
172
+ );
173
+ });
174
+
175
+ session
176
+ .command("join <sessionId>")
177
+ .description("Join an active session")
178
+ .option("--name <name>", "Agent display name")
179
+ .option("--role <role>", "Agent role: coder, reviewer, tester, observer", "coder")
180
+ .option("--model <model>", "Agent model hint", "cli")
181
+ .option("--path <path>", "Workspace path for the session", ".")
182
+ .option("--json", "Emit machine-readable output")
183
+ .action(async (sessionId, options, command) => {
184
+ const normalizedSessionId = normalizeString(sessionId);
185
+ if (!normalizedSessionId) {
186
+ throw new Error("session id is required.");
187
+ }
188
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
189
+ const joined = await registerAgent(normalizedSessionId, {
190
+ targetPath,
191
+ agentId: normalizeAgentId(options.name, "cli-user"),
192
+ model: normalizeString(options.model) || "cli",
193
+ role: options.role || "coder",
194
+ });
195
+ const payload = {
196
+ command: "session join",
197
+ targetPath,
198
+ sessionId: normalizedSessionId,
199
+ agentId: joined.agentId,
200
+ role: joined.role,
201
+ model: joined.model,
202
+ status: joined.status,
203
+ joinedAt: joined.joinedAt,
204
+ };
205
+ if (shouldEmitJson(options, command)) {
206
+ console.log(JSON.stringify(payload, null, 2));
207
+ return;
208
+ }
209
+ console.log(pc.bold(`Joined session ${normalizedSessionId}`));
210
+ console.log(pc.gray(`agent=${joined.agentId} role=${joined.role} model=${joined.model}`));
211
+ });
212
+
213
+ session
214
+ .command("say <sessionId> <message>")
215
+ .description("Send a message to the session")
216
+ .option("--agent <id>", "Agent id to emit from", "cli-user")
217
+ .option("--path <path>", "Workspace path for the session", ".")
218
+ .option("--json", "Emit machine-readable output")
219
+ .action(async (sessionId, message, options, command) => {
220
+ const normalizedSessionId = normalizeString(sessionId);
221
+ if (!normalizedSessionId) {
222
+ throw new Error("session id is required.");
223
+ }
224
+ const normalizedMessage = normalizeString(message);
225
+ if (!normalizedMessage) {
226
+ throw new Error("message is required.");
227
+ }
228
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
229
+ const agentId = normalizeAgentId(options.agent, "cli-user");
230
+ const event = createAgentEvent({
231
+ event: "session_message",
232
+ agentId,
233
+ sessionId: normalizedSessionId,
234
+ payload: {
235
+ message: normalizedMessage,
236
+ channel: "session",
237
+ },
238
+ });
239
+ const persisted = await appendToStream(normalizedSessionId, event, {
240
+ targetPath,
241
+ });
242
+ const payload = {
243
+ command: "session say",
244
+ targetPath,
245
+ sessionId: normalizedSessionId,
246
+ agentId,
247
+ event: persisted,
248
+ };
249
+ if (shouldEmitJson(options, command)) {
250
+ console.log(JSON.stringify(payload, null, 2));
251
+ return;
252
+ }
253
+ console.log(formatEventLine(persisted));
254
+ });
255
+
256
+ session
257
+ .command("read <sessionId>")
258
+ .description("Read recent session messages")
259
+ .option("--tail <n>", "Number of recent events", "20")
260
+ .option("--follow", "Continuously follow new events")
261
+ .option("--path <path>", "Workspace path for the session", ".")
262
+ .option("--json", "Emit machine-readable output")
263
+ .action(async (sessionId, options, command) => {
264
+ const normalizedSessionId = normalizeString(sessionId);
265
+ if (!normalizedSessionId) {
266
+ throw new Error("session id is required.");
267
+ }
268
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
269
+ const tail = parsePositiveInteger(options.tail, "tail", 20);
270
+ const emitJson = shouldEmitJson(options, command);
271
+
272
+ if (!options.follow) {
273
+ const events = await readStream(normalizedSessionId, {
274
+ targetPath,
275
+ tail,
276
+ });
277
+ const payload = {
278
+ command: "session read",
279
+ targetPath,
280
+ sessionId: normalizedSessionId,
281
+ tail,
282
+ count: events.length,
283
+ events,
284
+ };
285
+ if (emitJson) {
286
+ console.log(JSON.stringify(payload, null, 2));
287
+ return;
288
+ }
289
+ for (const event of events) {
290
+ console.log(formatEventLine(event));
291
+ }
292
+ return;
293
+ }
294
+
295
+ if (!emitJson) {
296
+ console.log(pc.gray(`Following session ${normalizedSessionId}... Press Ctrl+C to stop.`));
297
+ }
298
+ for await (const event of tailStream(normalizedSessionId, {
299
+ targetPath,
300
+ replayTail: tail,
301
+ })) {
302
+ if (emitJson) {
303
+ console.log(JSON.stringify(event));
304
+ } else {
305
+ console.log(formatEventLine(event));
306
+ }
307
+ }
308
+ });
309
+
310
+ session
311
+ .command("status <sessionId>")
312
+ .description("Show session status, agents, and health")
313
+ .option("--path <path>", "Workspace path for the session", ".")
314
+ .option("--json", "Emit machine-readable output")
315
+ .action(async (sessionId, options, command) => {
316
+ const normalizedSessionId = normalizeString(sessionId);
317
+ if (!normalizedSessionId) {
318
+ throw new Error("session id is required.");
319
+ }
320
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
321
+ const sessionPayload = await getSession(normalizedSessionId, {
322
+ targetPath,
323
+ });
324
+ if (!sessionPayload) {
325
+ throw new Error(`Session '${normalizedSessionId}' was not found.`);
326
+ }
327
+
328
+ const [agents, runtimeRuns, leases, recentEvents] = await Promise.all([
329
+ listAgents(normalizedSessionId, {
330
+ targetPath,
331
+ includeInactive: false,
332
+ }),
333
+ Promise.resolve(
334
+ listRuntimeRuns({
335
+ sessionId: normalizedSessionId,
336
+ targetPath,
337
+ includeStopped: false,
338
+ })
339
+ ),
340
+ listAssignments({
341
+ targetPath,
342
+ sessionId: normalizedSessionId,
343
+ statuses: ["CLAIMED", "IN_PROGRESS"],
344
+ includeExpired: true,
345
+ limit: 100,
346
+ }),
347
+ readStream(normalizedSessionId, {
348
+ targetPath,
349
+ tail: 10,
350
+ }),
351
+ ]);
352
+
353
+ const staleAgents = detectStaleAgents(agents, {});
354
+ const payload = {
355
+ command: "session status",
356
+ targetPath,
357
+ sessionId: normalizedSessionId,
358
+ session: sessionPayload,
359
+ activeAgents: agents,
360
+ staleAgents,
361
+ runtimeRuns,
362
+ activeLeases: leases.assignments,
363
+ recentEvents,
364
+ };
365
+ if (shouldEmitJson(options, command)) {
366
+ console.log(JSON.stringify(payload, null, 2));
367
+ return;
368
+ }
369
+
370
+ console.log(pc.bold(`Session ${normalizedSessionId}`));
371
+ console.log(
372
+ pc.gray(
373
+ `status=${sessionPayload.status} agents=${agents.length} stale=${staleAgents.length} runs=${runtimeRuns.length} leases=${leases.assignments.length}`
374
+ )
375
+ );
376
+ for (const event of recentEvents) {
377
+ console.log(formatEventLine(event));
378
+ }
379
+ });
380
+
381
+ session
382
+ .command("leave <sessionId>")
383
+ .description("Leave a session")
384
+ .option("--agent <id>", "Agent id to unregister", "cli-user")
385
+ .option("--reason <reason>", "Leave reason", "manual")
386
+ .option("--path <path>", "Workspace path for the session", ".")
387
+ .option("--json", "Emit machine-readable output")
388
+ .action(async (sessionId, options, command) => {
389
+ const normalizedSessionId = normalizeString(sessionId);
390
+ if (!normalizedSessionId) {
391
+ throw new Error("session id is required.");
392
+ }
393
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
394
+ const agentId = normalizeAgentId(options.agent, "cli-user");
395
+ const left = await unregisterAgent(normalizedSessionId, agentId, {
396
+ reason: options.reason || "manual",
397
+ targetPath,
398
+ });
399
+ const payload = {
400
+ command: "session leave",
401
+ targetPath,
402
+ sessionId: normalizedSessionId,
403
+ agentId: left.agentId,
404
+ reason: left.leaveReason,
405
+ leftAt: left.leftAt,
406
+ };
407
+ if (shouldEmitJson(options, command)) {
408
+ console.log(JSON.stringify(payload, null, 2));
409
+ return;
410
+ }
411
+ console.log(pc.bold(`Left session ${normalizedSessionId}`));
412
+ console.log(pc.gray(`agent=${left.agentId} reason=${left.leaveReason}`));
413
+ });
414
+
415
+ session
416
+ .command("list")
417
+ .description("List active sessions")
418
+ .option("--path <path>", "Workspace path for sessions", ".")
419
+ .option("--json", "Emit machine-readable output")
420
+ .action(async (options, command) => {
421
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
422
+ const sessions = await listActiveSessions({
423
+ targetPath,
424
+ });
425
+ const payload = {
426
+ command: "session list",
427
+ targetPath,
428
+ count: sessions.length,
429
+ sessions,
430
+ };
431
+ if (shouldEmitJson(options, command)) {
432
+ console.log(JSON.stringify(payload, null, 2));
433
+ return;
434
+ }
435
+ if (sessions.length === 0) {
436
+ console.log(pc.yellow("No active sessions."));
437
+ return;
438
+ }
439
+ for (const item of sessions) {
440
+ console.log(
441
+ `${item.sessionId} status=${item.status} created_at=${item.createdAt} expires_at=${item.expiresAt}`
442
+ );
443
+ }
444
+ });
445
+
446
+ session
447
+ .command("kill")
448
+ .description("Kill a single agent or all agents in a session")
449
+ .option("--agent <id>", "Specific agent id to stop")
450
+ .option("--all", "Kill every known agent in the session")
451
+ .option("--session <id>", "Session id")
452
+ .option("--id <sessionId>", "Deprecated alias for --session")
453
+ .option("--path <path>", "Workspace path for the session", ".")
454
+ .option("--reason <reason>", "Kill reason code", "manual_stop")
455
+ .option("--json", "Emit machine-readable output")
456
+ .action(async (options, command) => {
457
+ const sessionId = resolveSessionIdOption(options);
458
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
459
+ const reason = normalizeString(options.reason) || "manual_stop";
460
+ const requestedAgent = normalizeString(options.agent).toLowerCase();
461
+
462
+ if (!options.all && !requestedAgent) {
463
+ throw new Error("session kill requires --agent <id> or --all.");
464
+ }
465
+
466
+ const startedAt = Date.now();
467
+ const discoveredAgents = await listAgents(sessionId, {
468
+ targetPath,
469
+ includeInactive: false,
470
+ });
471
+ const agentsToKill = new Set();
472
+ if (options.all) {
473
+ agentsToKill.add("senti");
474
+ agentsToKill.add("scope-engine");
475
+ for (const agent of discoveredAgents) {
476
+ const agentId = normalizeString(agent.agentId).toLowerCase();
477
+ if (agentId) {
478
+ agentsToKill.add(agentId);
479
+ }
480
+ }
481
+ } else {
482
+ agentsToKill.add(requestedAgent);
483
+ }
484
+
485
+ const results = [];
486
+ let runtimeStops = 0;
487
+ let scopeStops = 0;
488
+ let leaseRevocations = 0;
489
+ let anyStopped = false;
490
+
491
+ for (const agentId of agentsToKill) {
492
+ let stopped = false;
493
+ let stopDetails = {};
494
+ if (agentId === "senti") {
495
+ const stopResult = await stopSenti(sessionId, {
496
+ targetPath,
497
+ reason,
498
+ });
499
+ runtimeStops += Number(stopResult?.runtimeStopSummary?.stoppedCount || 0);
500
+ stopped = Boolean(stopResult?.stopped);
501
+ stopDetails = {
502
+ runtimeStops: Number(stopResult?.runtimeStopSummary?.stoppedCount || 0),
503
+ scopeStops: 0,
504
+ };
505
+ } else if (agentId === "scope-engine") {
506
+ const stopResult = await stopScopeEngine({
507
+ targetPath,
508
+ sessionId,
509
+ reason,
510
+ });
511
+ scopeStops += Number(stopResult?.count || 0);
512
+ stopped = Boolean(stopResult?.stopped);
513
+ stopDetails = {
514
+ runtimeStops: 0,
515
+ scopeStops: Number(stopResult?.count || 0),
516
+ };
517
+ } else {
518
+ try {
519
+ await unregisterAgent(sessionId, agentId, {
520
+ reason: "killed",
521
+ targetPath,
522
+ });
523
+ stopped = true;
524
+ } catch {
525
+ stopped = false;
526
+ }
527
+ if (stopped) {
528
+ await emitAgentKilledEvent(sessionId, agentId, {
529
+ targetPath,
530
+ reason,
531
+ leaseRevocations: 0,
532
+ });
533
+ }
534
+ stopDetails = {
535
+ runtimeStops: 0,
536
+ scopeStops: 0,
537
+ };
538
+ }
539
+
540
+ const releasedCount = await revokeAgentLeases(sessionId, agentId, {
541
+ targetPath,
542
+ reason: `agent_killed:${reason}`,
543
+ });
544
+ leaseRevocations += releasedCount;
545
+ anyStopped = anyStopped || stopped;
546
+
547
+ results.push({
548
+ agentId,
549
+ stopped,
550
+ runtimeStops: stopDetails.runtimeStops,
551
+ scopeStops: stopDetails.scopeStops,
552
+ leaseRevocations: releasedCount,
553
+ });
554
+ }
555
+
556
+ const durationMs = Date.now() - startedAt;
557
+ const primaryAgentId = !options.all ? requestedAgent : null;
558
+ const payload = {
559
+ command: "session kill",
560
+ targetPath,
561
+ durationMs,
562
+ sessionId,
563
+ agentId: primaryAgentId,
564
+ all: Boolean(options.all),
565
+ reason,
566
+ stopped: anyStopped,
567
+ runtimeStops,
568
+ scopeStops,
569
+ leaseRevocations,
570
+ results,
571
+ };
572
+
573
+ if (shouldEmitJson(options, command)) {
574
+ console.log(JSON.stringify(payload, null, 2));
575
+ return;
576
+ }
577
+
578
+ if (payload.stopped) {
579
+ console.log(pc.bold("Kill complete"));
580
+ } else {
581
+ console.log(pc.yellow(`No active target found in session ${sessionId}.`));
582
+ }
583
+ console.log(
584
+ pc.gray(
585
+ `session=${sessionId} runtime_stops=${runtimeStops} scope_stops=${scopeStops} lease_revocations=${leaseRevocations}`
586
+ )
587
+ );
588
+ console.log(`stopped=${payload.stopped} reason=${reason} duration_ms=${durationMs}`);
589
+ });
590
+ }
@@ -91,6 +91,60 @@ function resolveSpecArtifactPath(targetPath, explicitPath) {
91
91
  throw new Error("No spec artifact found. Generate one with 'spec generate' or pass --file.");
92
92
  }
93
93
 
94
+ async function readAgentsMarkdown(targetPath) {
95
+ const agentsPath = path.join(targetPath, "AGENTS.md");
96
+ try {
97
+ return await fsp.readFile(agentsPath, "utf-8");
98
+ } catch (error) {
99
+ if (error && typeof error === "object" && error.code === "ENOENT") {
100
+ return "";
101
+ }
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ function isSessionMetadataActive(metadata = {}, nowEpoch = Date.now()) {
107
+ const status = String(metadata.status || "").trim().toLowerCase();
108
+ if (status === "expired" || status === "archived") {
109
+ return false;
110
+ }
111
+ const expiryEpoch = Date.parse(String(metadata.expiresAt || ""));
112
+ if (!Number.isFinite(expiryEpoch)) {
113
+ return false;
114
+ }
115
+ return expiryEpoch > nowEpoch;
116
+ }
117
+
118
+ async function detectSessionActive(targetPath) {
119
+ const sessionsRoot = path.join(targetPath, ".sentinelayer", "sessions");
120
+ let entries = [];
121
+ try {
122
+ entries = await fsp.readdir(sessionsRoot, { withFileTypes: true });
123
+ } catch (error) {
124
+ if (error && typeof error === "object" && error.code === "ENOENT") {
125
+ return false;
126
+ }
127
+ throw error;
128
+ }
129
+ const nowEpoch = Date.now();
130
+ for (const entry of entries) {
131
+ if (!entry.isDirectory()) {
132
+ continue;
133
+ }
134
+ const metadataPath = path.join(sessionsRoot, entry.name, "metadata.json");
135
+ try {
136
+ const raw = await fsp.readFile(metadataPath, "utf-8");
137
+ const metadata = JSON.parse(raw);
138
+ if (isSessionMetadataActive(metadata, nowEpoch)) {
139
+ return true;
140
+ }
141
+ } catch {
142
+ // Ignore malformed or missing metadata for one session and continue scanning.
143
+ }
144
+ }
145
+ return false;
146
+ }
147
+
94
148
  function estimateTokenCount(text) {
95
149
  const normalized = String(text || "");
96
150
  if (!normalized) {
@@ -466,6 +520,8 @@ export function registerSpecCommand(program) {
466
520
  refresh: Boolean(options.refresh),
467
521
  });
468
522
  const ingest = ingestResolution.ingest;
523
+ const agentsMarkdown = await readAgentsMarkdown(targetPath);
524
+ const sessionActive = await detectSessionActive(targetPath);
469
525
  const explicitProjectType = parseProjectTypeOption(options.projectType);
470
526
  const resolvedProjectType = resolveProjectType({
471
527
  projectType: explicitProjectType,
@@ -479,6 +535,8 @@ export function registerSpecCommand(program) {
479
535
  ingest,
480
536
  projectPath: targetPath,
481
537
  projectType: resolvedProjectType,
538
+ agentsMarkdown,
539
+ sessionActive,
482
540
  });
483
541
 
484
542
  progress.update(65, "spec generate: optional AI refinement");
@@ -579,6 +637,8 @@ export function registerSpecCommand(program) {
579
637
  refresh: Boolean(options.refresh),
580
638
  });
581
639
  const ingest = ingestResolution.ingest;
640
+ const agentsMarkdown = await readAgentsMarkdown(targetPath);
641
+ const sessionActive = await detectSessionActive(targetPath);
582
642
  const explicitProjectType = parseProjectTypeOption(options.projectType);
583
643
  const inferredProjectType = inferProjectTypeFromSpecMarkdown(existingMarkdown);
584
644
  const resolvedProjectType = resolveProjectType({
@@ -592,6 +652,8 @@ export function registerSpecCommand(program) {
592
652
  ingest,
593
653
  projectPath: targetPath,
594
654
  projectType: resolvedProjectType,
655
+ agentsMarkdown,
656
+ sessionActive,
595
657
  });
596
658
 
597
659
  progress.update(55, "spec regenerate: preserving manual sections");
@@ -12,6 +12,7 @@ import {
12
12
  resolveActiveAuthSession,
13
13
  } from "../auth/service.js";
14
14
  import { resolveOutputRoot } from "../config/service.js";
15
+ import { authLoginHint } from "../ui/command-hints.js";
15
16
 
16
17
  const TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "cancelled"]);
17
18
 
@@ -369,7 +370,7 @@ export function registerWatchCommand(program) {
369
370
  }
370
371
 
371
372
  if (!session || !session.token) {
372
- throw new Error("No active auth token found. Run `sl auth login` first.");
373
+ throw new Error(`No active auth token found. Run \`${authLoginHint()}\` first.`);
373
374
  }
374
375
 
375
376
  const startedAtEpoch = Date.now();
@@ -446,7 +447,7 @@ export function registerWatchCommand(program) {
446
447
  }
447
448
  } catch (error) {
448
449
  if (error instanceof SentinelayerApiError && (error.status === 401 || error.status === 403)) {
449
- throw new Error("Authentication failed while watching runtime events. Run `sl auth login`.");
450
+ throw new Error(`Authentication failed while watching runtime events. Run \`${authLoginHint()}\`.`);
450
451
  }
451
452
  throw new Error(formatApiError(error));
452
453
  }