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.
- package/README.md +16 -18
- package/package.json +7 -6
- package/src/agents/jules/config/definition.js +13 -62
- package/src/agents/jules/config/system-prompt.js +8 -1
- package/src/agents/jules/fix-cycle.js +12 -372
- package/src/agents/jules/loop.js +116 -26
- package/src/agents/jules/pulse.js +10 -327
- package/src/agents/jules/stream.js +13 -12
- package/src/agents/jules/swarm/orchestrator.js +3 -3
- package/src/agents/jules/swarm/sub-agent.js +6 -3
- package/src/agents/jules/tools/aidenid-email.js +189 -0
- package/src/agents/jules/tools/auth-audit.js +1187 -45
- package/src/agents/jules/tools/dispatch.js +25 -12
- package/src/agents/jules/tools/file-edit.js +2 -180
- package/src/agents/jules/tools/file-read.js +2 -100
- package/src/agents/jules/tools/glob.js +2 -168
- package/src/agents/jules/tools/grep.js +2 -228
- package/src/agents/jules/tools/path-guards.js +2 -161
- package/src/agents/jules/tools/runtime-audit.js +6 -2
- package/src/agents/jules/tools/shell.js +2 -383
- package/src/agents/persona-visuals.js +64 -0
- package/src/agents/shared-tools/dispatch-core.js +320 -0
- package/src/agents/shared-tools/file-edit.js +180 -0
- package/src/agents/shared-tools/file-read.js +100 -0
- package/src/agents/shared-tools/glob.js +168 -0
- package/src/agents/shared-tools/grep.js +228 -0
- package/src/agents/shared-tools/index.js +46 -0
- package/src/agents/shared-tools/path-guards.js +161 -0
- package/src/agents/shared-tools/shell.js +383 -0
- package/src/ai/aidenid.js +56 -7
- package/src/ai/client.js +45 -0
- package/src/ai/proxy.js +137 -0
- package/src/auth/gate.js +290 -16
- package/src/auth/http.js +450 -39
- package/src/auth/service.js +262 -47
- package/src/auth/session-store.js +475 -21
- package/src/cli.js +5 -0
- package/src/commands/audit.js +13 -8
- package/src/commands/auth.js +53 -9
- package/src/commands/omargate.js +10 -2
- package/src/commands/scan.js +10 -4
- package/src/commands/session.js +590 -0
- package/src/commands/spec.js +62 -0
- package/src/commands/watch.js +3 -2
- package/src/daemon/assignment-ledger.js +196 -0
- package/src/daemon/error-worker.js +599 -16
- package/src/daemon/fix-cycle.js +384 -0
- package/src/daemon/ingest-refresh.js +10 -9
- package/src/daemon/jira-lifecycle.js +135 -0
- package/src/daemon/pulse.js +327 -0
- package/src/daemon/scope-engine.js +1068 -0
- package/src/events/schema.js +190 -0
- package/src/interactive/index.js +18 -16
- package/src/legacy-cli.js +606 -37
- package/src/prompt/generator.js +19 -1
- package/src/review/ai-review.js +11 -1
- package/src/review/local-review.js +75 -19
- package/src/review/omargate-interactive.js +68 -0
- package/src/review/omargate-orchestrator.js +404 -0
- package/src/review/persona-prompts.js +296 -0
- package/src/review/scan-modes.js +48 -0
- package/src/scan/generator.js +1 -1
- package/src/session/agent-registry.js +352 -0
- package/src/session/daemon.js +801 -0
- package/src/session/paths.js +33 -0
- package/src/session/runtime-bridge.js +739 -0
- package/src/session/store.js +388 -0
- package/src/session/stream.js +325 -0
- package/src/spec/generator.js +100 -0
- package/src/telemetry/session-tracker.js +148 -32
- package/src/telemetry/sync.js +6 -2
- 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
|
+
}
|
package/src/commands/spec.js
CHANGED
|
@@ -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");
|
package/src/commands/watch.js
CHANGED
|
@@ -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(
|
|
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(
|
|
450
|
+
throw new Error(`Authentication failed while watching runtime events. Run \`${authLoginHint()}\`.`);
|
|
450
451
|
}
|
|
451
452
|
throw new Error(formatApiError(error));
|
|
452
453
|
}
|