sentinelayer-cli 0.18.2 → 0.19.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.18.2",
3
+ "version": "0.19.0",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -17,6 +17,9 @@ const DEFAULT_RECAP_INTERVAL_MS = 300_000;
17
17
  const DEFAULT_RECAP_INACTIVITY_MS = 600_000;
18
18
  const DEFAULT_RECAP_ACTIVITY_THRESHOLD = 5;
19
19
  const DEFAULT_TASK_SUMMARY_LIMIT = 3;
20
+ const DEFAULT_WORK_PLAN_SUMMARY_LIMIT = 5;
21
+ const MAX_WORK_PLAN_BYTES = 128_000;
22
+ const WORK_PLAN_RELATIVE_PATH = "tasks/todo.md";
20
23
  const RECAP_SOURCE_IGNORED_EVENTS = new Set([
21
24
  "agent_heartbeat",
22
25
  "agent_join",
@@ -399,6 +402,105 @@ function emptyTaskLedgerSummary() {
399
402
  };
400
403
  }
401
404
 
405
+ function emptyWorkPlanSummary() {
406
+ return {
407
+ path: WORK_PLAN_RELATIVE_PATH,
408
+ exists: false,
409
+ truncated: false,
410
+ total: 0,
411
+ open: 0,
412
+ completed: 0,
413
+ currentSection: "",
414
+ recentOpen: [],
415
+ recent: [],
416
+ };
417
+ }
418
+
419
+ function shortWorkPlanText(value) {
420
+ const text = normalizeString(value)
421
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
422
+ .replace(/`([^`]+)`/g, "$1")
423
+ .replace(/\s+/g, " ");
424
+ if (text.length <= 100) {
425
+ return text;
426
+ }
427
+ return `${text.slice(0, 97)}...`;
428
+ }
429
+
430
+ function summarizeWorkPlanMarkdown(raw = "", { limit = DEFAULT_WORK_PLAN_SUMMARY_LIMIT, truncated = false } = {}) {
431
+ const summary = emptyWorkPlanSummary();
432
+ summary.exists = true;
433
+ summary.truncated = Boolean(truncated);
434
+
435
+ const records = [];
436
+ let section = "";
437
+ for (const line of String(raw || "").split(/\r?\n/)) {
438
+ const headingMatch = /^(#{1,4})\s+(.+?)\s*$/.exec(line);
439
+ if (headingMatch) {
440
+ section = shortWorkPlanText(headingMatch[2]);
441
+ continue;
442
+ }
443
+
444
+ const taskMatch = /^\s*[-*]\s+\[([ xX])\]\s+(.+?)\s*$/.exec(line);
445
+ if (!taskMatch) {
446
+ continue;
447
+ }
448
+ const completed = taskMatch[1].toLowerCase() === "x";
449
+ const record = {
450
+ status: completed ? "completed" : "open",
451
+ section,
452
+ task: shortWorkPlanText(taskMatch[2]),
453
+ };
454
+ if (!record.task) {
455
+ continue;
456
+ }
457
+ records.push(record);
458
+ summary.total += 1;
459
+ if (completed) {
460
+ summary.completed += 1;
461
+ } else {
462
+ summary.open += 1;
463
+ }
464
+ summary.currentSection = section || summary.currentSection;
465
+ }
466
+
467
+ const normalizedLimit = Math.max(1, normalizePositiveInteger(limit, DEFAULT_WORK_PLAN_SUMMARY_LIMIT));
468
+ summary.recentOpen = records
469
+ .filter((record) => record.status === "open")
470
+ .slice(-normalizedLimit);
471
+ summary.recent = records.slice(-normalizedLimit);
472
+ return summary;
473
+ }
474
+
475
+ async function readWorkPlanSummary({ targetPath = process.cwd(), limit = DEFAULT_WORK_PLAN_SUMMARY_LIMIT } = {}) {
476
+ const filePath = path.join(path.resolve(String(targetPath || ".")), WORK_PLAN_RELATIVE_PATH);
477
+ try {
478
+ const stats = await fsp.stat(filePath);
479
+ let source = "";
480
+ let truncated = false;
481
+ if (stats.size > MAX_WORK_PLAN_BYTES) {
482
+ truncated = true;
483
+ const handle = await fsp.open(filePath, "r");
484
+ try {
485
+ const buffer = Buffer.alloc(MAX_WORK_PLAN_BYTES);
486
+ const position = Math.max(0, stats.size - MAX_WORK_PLAN_BYTES);
487
+ const { bytesRead } = await handle.read(buffer, 0, MAX_WORK_PLAN_BYTES, position);
488
+ source = buffer.subarray(0, bytesRead).toString("utf-8");
489
+ } finally {
490
+ await handle.close();
491
+ }
492
+ } else {
493
+ source = await fsp.readFile(filePath, "utf-8");
494
+ }
495
+ return summarizeWorkPlanMarkdown(source, { limit, truncated });
496
+ } catch (error) {
497
+ if (error && typeof error === "object" && error.code === "ENOENT") {
498
+ return emptyWorkPlanSummary();
499
+ }
500
+ return emptyWorkPlanSummary();
501
+ }
502
+ }
503
+
402
504
  function summarizeTaskLedger(tasks = [], { limit = DEFAULT_TASK_SUMMARY_LIMIT } = {}) {
403
505
  const summary = emptyTaskLedgerSummary();
404
506
  const owners = new Map();
@@ -541,6 +643,7 @@ function buildRecapText({
541
643
  activeLocks = 0,
542
644
  pendingTasks = 0,
543
645
  taskLedger = emptyTaskLedgerSummary(),
646
+ workPlan = emptyWorkPlanSummary(),
544
647
  usageSummary = normalizeUsageSummary(),
545
648
  snippets = [],
546
649
  } = {}) {
@@ -554,8 +657,9 @@ function buildRecapText({
554
657
  pendingTasks > 0 ? `You have ${pendingTasks} pending task${pendingTasks === 1 ? "" : "s"}.` : "";
555
658
  const taskText = buildTaskLedgerText(taskLedger);
556
659
  const usageText = buildUsageLedgerText(usageSummary);
660
+ const workPlanText = buildWorkPlanText(workPlan);
557
661
  const snippetText = snippets.length > 0 ? `Recent: ${snippets.join(" | ")}` : "";
558
- return `While you were away: ${agentText}. ${findingText}. ${lockText}. ${pendingText} ${taskText}. ${usageText} ${snippetText}`.replace(
662
+ return `While you were away: ${agentText}. ${findingText}. ${lockText}. ${pendingText} ${taskText}. ${workPlanText} ${usageText} ${snippetText}`.replace(
559
663
  /\s+/g,
560
664
  " "
561
665
  ).trim();
@@ -596,6 +700,31 @@ function buildTaskLedgerText(taskLedger = emptyTaskLedgerSummary()) {
596
700
  .join(". ");
597
701
  }
598
702
 
703
+ function buildWorkPlanText(workPlan = emptyWorkPlanSummary()) {
704
+ if (!workPlan || typeof workPlan !== "object" || !workPlan.exists) {
705
+ return "";
706
+ }
707
+ const open = Number(workPlan.open || 0);
708
+ const completed = Number(workPlan.completed || 0);
709
+ const pathText = normalizeString(workPlan.path) || WORK_PLAN_RELATIVE_PATH;
710
+ const currentSection = normalizeString(workPlan.currentSection);
711
+ const currentText = currentSection ? ` Current: ${currentSection}.` : "";
712
+ const recentOpen = Array.isArray(workPlan.recentOpen) ? workPlan.recentOpen : [];
713
+ const nextText =
714
+ recentOpen.length > 0
715
+ ? ` Next: ${recentOpen
716
+ .map((item) => {
717
+ const section = normalizeString(item.section);
718
+ const task = normalizeString(item.task);
719
+ return section ? `${section} - ${task}` : task;
720
+ })
721
+ .filter(Boolean)
722
+ .join("; ")}.`
723
+ : "";
724
+ const truncatedText = workPlan.truncated ? " Recent window only." : "";
725
+ return `Plan: ${open} open / ${completed} done in ${pathText}.${currentText}${nextText}${truncatedText}`;
726
+ }
727
+
599
728
  function roundCurrency(value) {
600
729
  const normalized = Number(value || 0);
601
730
  if (!Number.isFinite(normalized) || normalized < 0) {
@@ -702,11 +831,12 @@ function buildPeriodicText(recap = {}) {
702
831
  const lastActor = normalizeString(summary.lastActorId);
703
832
  const actorText = lastActor ? `${lastActor} active` : "no active actor";
704
833
  const taskText = buildTaskLedgerText(summary.taskLedger);
834
+ const workPlanText = buildWorkPlanText(summary.workPlan);
705
835
  const usageText = buildUsageLedgerText({
706
836
  totals: summary.usageTotals,
707
837
  topAgents: summary.usageTopAgents,
708
838
  });
709
- return `Session active for ${elapsedMinutes}m. ${activeAgents} agents. ${totalFindings} findings. ${activeLocks} locks. ${taskText}. ${usageText} ${actorText}.`.replace(
839
+ return `Session active for ${elapsedMinutes}m. ${activeAgents} agents. ${totalFindings} findings. ${activeLocks} locks. ${taskText}. ${workPlanText} ${usageText} ${actorText}.`.replace(
710
840
  /\s+/g,
711
841
  " ",
712
842
  ).trim();
@@ -774,6 +904,9 @@ export async function buildSessionRecap(
774
904
  const taskLedger = await readTaskLedgerSummary(normalizedSessionId, {
775
905
  targetPath: normalizedTargetPath,
776
906
  });
907
+ const workPlan = await readWorkPlanSummary({
908
+ targetPath: normalizedTargetPath,
909
+ });
777
910
  const snippets = summarizeRecentActivity(visibleEvents, {
778
911
  forAgentId: normalizedForAgentId,
779
912
  limit: 2,
@@ -789,6 +922,7 @@ export async function buildSessionRecap(
789
922
  activeLocks,
790
923
  pendingTasks,
791
924
  taskLedger,
925
+ workPlan,
792
926
  usageSummary,
793
927
  snippets,
794
928
  });
@@ -809,6 +943,7 @@ export async function buildSessionRecap(
809
943
  activeLocks,
810
944
  pendingTasksForAgent: pendingTasks,
811
945
  taskLedger,
946
+ workPlan,
812
947
  usageTotals: usageSummary.totals,
813
948
  usageTopAgents: usageSummary.topAgents,
814
949
  snippets,
@@ -243,6 +243,23 @@ function eventSequenceNumber(event = {}) {
243
243
  return 0;
244
244
  }
245
245
 
246
+ function nextBeforeSequenceFromPayload(payload = {}, events = []) {
247
+ const explicit = Number(payload?.next_before_sequence ?? payload?.nextBeforeSequence);
248
+ if (Number.isFinite(explicit) && explicit > 0) {
249
+ return Math.floor(explicit);
250
+ }
251
+
252
+ let minimumSequence = 0;
253
+ for (const event of Array.isArray(events) ? events : []) {
254
+ const sequence = eventSequenceNumber(event);
255
+ if (sequence <= 0) continue;
256
+ if (minimumSequence === 0 || sequence < minimumSequence) {
257
+ minimumSequence = sequence;
258
+ }
259
+ }
260
+ return minimumSequence || null;
261
+ }
262
+
246
263
  function chronologicalSessionEvents(events = []) {
247
264
  return (Array.isArray(events) ? events : [])
248
265
  .map((event, index) => ({ event, index }))
@@ -1219,13 +1236,12 @@ export async function pollSessionEventsBefore(
1219
1236
 
1220
1237
  const events = chronologicalSessionEvents(payload?.events || []);
1221
1238
  const lastEvent = events[events.length - 1] || null;
1222
- const firstEvent = events[0] || null;
1223
1239
  return {
1224
1240
  ok: true,
1225
1241
  reason: "",
1226
1242
  events,
1227
1243
  cursor: normalizeString(lastEvent?.cursor) || null,
1228
- beforeSequence: eventSequenceNumber(firstEvent) || null,
1244
+ beforeSequence: nextBeforeSequenceFromPayload(payload, payload?.events || []),
1229
1245
  };
1230
1246
  } catch (error) {
1231
1247
  recordCircuitFailure(inboundCircuit, normalizedNowMs);