sentinelayer-cli 0.9.3 → 0.9.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -13,6 +13,14 @@ const DEFAULT_RECAP_MAX_EVENTS = 100;
13
13
  const DEFAULT_RECAP_INTERVAL_MS = 300_000;
14
14
  const DEFAULT_RECAP_INACTIVITY_MS = 600_000;
15
15
  const DEFAULT_RECAP_ACTIVITY_THRESHOLD = 5;
16
+ const DEFAULT_TASK_SUMMARY_LIMIT = 3;
17
+ const ACTIVE_TASK_STATUSES = new Set(["PENDING", "ACCEPTED", "BLOCKED"]);
18
+ const TASK_STATUS_KEYS = {
19
+ PENDING: "pending",
20
+ ACCEPTED: "accepted",
21
+ COMPLETED: "completed",
22
+ BLOCKED: "blocked",
23
+ };
16
24
 
17
25
  const ACTIVE_RECAP_EMITTERS = new Map();
18
26
 
@@ -160,6 +168,117 @@ async function readPendingTasks(sessionId, { forAgentId = "", targetPath = proce
160
168
  }
161
169
  }
162
170
 
171
+ function normalizeTaskStatus(value) {
172
+ const normalized = normalizeString(value).toUpperCase();
173
+ return Object.prototype.hasOwnProperty.call(TASK_STATUS_KEYS, normalized)
174
+ ? normalized
175
+ : "PENDING";
176
+ }
177
+
178
+ function taskOwner(task = {}) {
179
+ return (
180
+ normalizeString(task.toAgentId) ||
181
+ normalizeString(task.requestedToAgentId) ||
182
+ (normalizeString(task.roleFilter) ? `role:${normalizeString(task.roleFilter)}` : "") ||
183
+ "unassigned"
184
+ );
185
+ }
186
+
187
+ function shortTaskText(value) {
188
+ const text = normalizeString(value).replace(/\s+/g, " ");
189
+ if (text.length <= 80) {
190
+ return text;
191
+ }
192
+ return `${text.slice(0, 77)}...`;
193
+ }
194
+
195
+ function emptyTaskLedgerSummary() {
196
+ return {
197
+ total: 0,
198
+ active: 0,
199
+ pending: 0,
200
+ accepted: 0,
201
+ blocked: 0,
202
+ completed: 0,
203
+ owners: [],
204
+ recent: [],
205
+ };
206
+ }
207
+
208
+ function summarizeTaskLedger(tasks = [], { limit = DEFAULT_TASK_SUMMARY_LIMIT } = {}) {
209
+ const summary = emptyTaskLedgerSummary();
210
+ const owners = new Map();
211
+ const normalizedTasks = [];
212
+
213
+ for (const task of Array.isArray(tasks) ? tasks : []) {
214
+ if (!task || typeof task !== "object") {
215
+ continue;
216
+ }
217
+ const status = normalizeTaskStatus(task.status);
218
+ const statusKey = TASK_STATUS_KEYS[status];
219
+ const owner = taskOwner(task);
220
+ const priority = normalizeString(task.priority) || "when-free";
221
+ const taskId = normalizeString(task.taskId) || "task";
222
+ const updatedAt = normalizeIsoTimestamp(
223
+ task.updatedAt || task.completedAt || task.acceptedAt || task.createdAt,
224
+ new Date().toISOString(),
225
+ );
226
+ const record = {
227
+ taskId,
228
+ status,
229
+ priority,
230
+ owner,
231
+ task: shortTaskText(task.task),
232
+ updatedAt,
233
+ };
234
+ normalizedTasks.push(record);
235
+ summary.total += 1;
236
+ summary[statusKey] += 1;
237
+
238
+ if (ACTIVE_TASK_STATUSES.has(status)) {
239
+ summary.active += 1;
240
+ const ownerRecord = owners.get(owner) || {
241
+ agentId: owner,
242
+ active: 0,
243
+ pending: 0,
244
+ accepted: 0,
245
+ blocked: 0,
246
+ };
247
+ ownerRecord.active += 1;
248
+ ownerRecord[statusKey] += 1;
249
+ owners.set(owner, ownerRecord);
250
+ }
251
+ }
252
+
253
+ summary.owners = [...owners.values()]
254
+ .sort((left, right) => {
255
+ if (right.active !== left.active) return right.active - left.active;
256
+ return left.agentId.localeCompare(right.agentId);
257
+ })
258
+ .slice(0, Math.max(1, limit));
259
+ summary.recent = normalizedTasks
260
+ .sort((left, right) => toEpoch(right.updatedAt) - toEpoch(left.updatedAt))
261
+ .slice(0, Math.max(1, limit));
262
+ return summary;
263
+ }
264
+
265
+ async function readTaskLedgerSummary(
266
+ sessionId,
267
+ { targetPath = process.cwd(), limit = DEFAULT_TASK_SUMMARY_LIMIT } = {},
268
+ ) {
269
+ const paths = resolveSessionPaths(sessionId, { targetPath });
270
+ try {
271
+ const raw = await fsp.readFile(paths.tasksPath, "utf-8");
272
+ const parsed = JSON.parse(raw);
273
+ return summarizeTaskLedger(parsed?.tasks || [], { limit });
274
+ } catch (error) {
275
+ if (error && typeof error === "object" && error.code === "ENOENT") {
276
+ return emptyTaskLedgerSummary();
277
+ }
278
+ return emptyTaskLedgerSummary();
279
+ }
280
+ }
281
+
163
282
  function buildElapsedMinutes(events = [], nowIso = new Date().toISOString()) {
164
283
  if (!Array.isArray(events) || events.length === 0) {
165
284
  return 0;
@@ -181,6 +300,7 @@ function buildRecapText({
181
300
  totalFindings = 0,
182
301
  activeLocks = 0,
183
302
  pendingTasks = 0,
303
+ taskLedger = emptyTaskLedgerSummary(),
184
304
  snippets = [],
185
305
  } = {}) {
186
306
  const agentText =
@@ -191,13 +311,49 @@ function buildRecapText({
191
311
  const lockText = `${activeLocks} file lock${activeLocks === 1 ? "" : "s"} active`;
192
312
  const pendingText =
193
313
  pendingTasks > 0 ? `You have ${pendingTasks} pending task${pendingTasks === 1 ? "" : "s"}.` : "";
314
+ const taskText = buildTaskLedgerText(taskLedger);
194
315
  const snippetText = snippets.length > 0 ? `Recent: ${snippets.join(" | ")}` : "";
195
- return `While you were away: ${agentText}. ${findingText}. ${lockText}. ${pendingText} ${snippetText}`.replace(
316
+ return `While you were away: ${agentText}. ${findingText}. ${lockText}. ${pendingText} ${taskText}. ${snippetText}`.replace(
196
317
  /\s+/g,
197
318
  " "
198
319
  ).trim();
199
320
  }
200
321
 
322
+ function buildTaskLedgerText(taskLedger = emptyTaskLedgerSummary()) {
323
+ const total = Number(taskLedger.total || 0);
324
+ if (!total) {
325
+ return "Tasks: none queued";
326
+ }
327
+ const active = Number(taskLedger.active || 0);
328
+ const counts = [
329
+ `${Number(taskLedger.pending || 0)} pending`,
330
+ `${Number(taskLedger.accepted || 0)} accepted`,
331
+ `${Number(taskLedger.blocked || 0)} blocked`,
332
+ `${Number(taskLedger.completed || 0)} done`,
333
+ ].join(", ");
334
+ const ownerText =
335
+ Array.isArray(taskLedger.owners) && taskLedger.owners.length > 0
336
+ ? `Owners: ${taskLedger.owners
337
+ .map((owner) => {
338
+ const parts = [];
339
+ if (owner.pending) parts.push(`${owner.pending} pending`);
340
+ if (owner.accepted) parts.push(`${owner.accepted} accepted`);
341
+ if (owner.blocked) parts.push(`${owner.blocked} blocked`);
342
+ return `${owner.agentId} (${parts.join("/") || `${owner.active || 0} active`})`;
343
+ })
344
+ .join("; ")}`
345
+ : "Owners: none active";
346
+ const recentText =
347
+ Array.isArray(taskLedger.recent) && taskLedger.recent.length > 0
348
+ ? `Recent tasks: ${taskLedger.recent
349
+ .map((task) => `${task.priority} ${task.status} ${task.owner}: ${task.task}`)
350
+ .join(" | ")}`
351
+ : "";
352
+ return [`Tasks: ${active} active of ${total} total (${counts})`, ownerText, recentText]
353
+ .filter(Boolean)
354
+ .join(". ");
355
+ }
356
+
201
357
  // Multi-agent session etiquette + read-path rules surfaced in the
202
358
  // context_briefing payload an agent receives on first join. Web
203
359
  // renders this as markdown (see sentinelayer-web Session.tsx
@@ -235,7 +391,8 @@ function buildPeriodicText(recap = {}) {
235
391
  const activeLocks = Number(summary.activeLocks || 0);
236
392
  const lastActor = normalizeString(summary.lastActorId);
237
393
  const actorText = lastActor ? `${lastActor} active` : "no active actor";
238
- return `Session active for ${elapsedMinutes}m. ${activeAgents} agents. ${totalFindings} findings. ${activeLocks} locks. ${actorText}.`;
394
+ const taskText = buildTaskLedgerText(summary.taskLedger);
395
+ return `Session active for ${elapsedMinutes}m. ${activeAgents} agents. ${totalFindings} findings. ${activeLocks} locks. ${taskText}. ${actorText}.`;
239
396
  }
240
397
 
241
398
  export async function buildSessionRecap(
@@ -288,6 +445,9 @@ export async function buildSessionRecap(
288
445
  forAgentId: normalizedForAgentId,
289
446
  targetPath: normalizedTargetPath,
290
447
  });
448
+ const taskLedger = await readTaskLedgerSummary(normalizedSessionId, {
449
+ targetPath: normalizedTargetPath,
450
+ });
291
451
  const snippets = summarizeRecentActivity(visibleEvents, {
292
452
  forAgentId: normalizedForAgentId,
293
453
  limit: 2,
@@ -299,6 +459,7 @@ export async function buildSessionRecap(
299
459
  totalFindings: totalFindingsCount,
300
460
  activeLocks,
301
461
  pendingTasks,
462
+ taskLedger,
302
463
  snippets,
303
464
  });
304
465
 
@@ -317,6 +478,7 @@ export async function buildSessionRecap(
317
478
  totalFindingsCount,
318
479
  activeLocks,
319
480
  pendingTasksForAgent: pendingTasks,
481
+ taskLedger,
320
482
  snippets,
321
483
  elapsedMinutes,
322
484
  lastActorId: normalizeString(latestEvent?.agent?.id || latestEvent?.agentId) || null,