sentinelayer-cli 0.36.5 → 0.37.1

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.36.5",
3
+ "version": "0.37.1",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -481,37 +481,64 @@ Docs: https://github.com/mrrCarter/create-sentinelayer/blob/main/docs/mcp.md`,
481
481
  console.log(pc.gray("Next: validate with registry cross-check before runtime wiring."));
482
482
  });
483
483
 
484
- registry
485
- .command("init-session")
486
- .description("Write the built-in SentinelLayer session MCP tool registry")
487
- .option("--path <path>", "Destination file path override")
488
- .option("--output-dir <path>", "Optional artifact output root override")
489
- .option("--force", "Overwrite destination file if it already exists")
490
- .option("--json", "Emit machine-readable output")
491
- .action(async (options, command) => {
492
- const defaultPath = await resolveDefaultMcpOutputPath({
493
- cwd: process.cwd(),
494
- outputDir: options.outputDir,
495
- env: process.env,
496
- });
497
- const outputPath = normalizeOutputPath(options.path, defaultPath.replace(".schema", ".session-tools"));
498
- const template = buildSentinelayerSessionRegistryTemplate();
499
- validateMcpToolRegistry(template);
500
- const writtenPath = await writeJsonFile(outputPath, template, { force: Boolean(options.force) });
501
- const payload = {
502
- command: "mcp registry init-session",
503
- outputPath: writtenPath,
504
- toolCount: template.tools.length,
505
- tools: template.tools.map((tool) => tool.name),
506
- };
484
+ const registerSessionRegistryInit = ({
485
+ commandName,
486
+ description,
487
+ outputSuffix,
488
+ payloadCommand,
489
+ successLabel,
490
+ nextMessage,
491
+ }) => {
492
+ registry
493
+ .command(commandName)
494
+ .description(description)
495
+ .option("--path <path>", "Destination file path override")
496
+ .option("--output-dir <path>", "Optional artifact output root override")
497
+ .option("--force", "Overwrite destination file if it already exists")
498
+ .option("--json", "Emit machine-readable output")
499
+ .action(async (options, command) => {
500
+ const defaultPath = await resolveDefaultMcpOutputPath({
501
+ cwd: process.cwd(),
502
+ outputDir: options.outputDir,
503
+ env: process.env,
504
+ });
505
+ const outputPath = normalizeOutputPath(options.path, defaultPath.replace(".schema", outputSuffix));
506
+ const template = buildSentinelayerSessionRegistryTemplate();
507
+ validateMcpToolRegistry(template);
508
+ const writtenPath = await writeJsonFile(outputPath, template, { force: Boolean(options.force) });
509
+ const payload = {
510
+ command: payloadCommand,
511
+ outputPath: writtenPath,
512
+ toolCount: template.tools.length,
513
+ tools: template.tools.map((tool) => tool.name),
514
+ };
507
515
 
508
- if (shouldEmitJson(options, command)) {
509
- console.log(JSON.stringify(payload, null, 2));
510
- return;
511
- }
512
- console.log(pc.green(`Wrote SentinelLayer session MCP registry template: ${writtenPath}`));
513
- console.log(pc.gray("Next: run 'sl mcp server init --id sentinelayer-session --registry-file <path>'."));
514
- });
516
+ if (shouldEmitJson(options, command)) {
517
+ console.log(JSON.stringify(payload, null, 2));
518
+ return;
519
+ }
520
+ console.log(pc.green(`Wrote ${successLabel}: ${writtenPath}`));
521
+ console.log(pc.gray(nextMessage));
522
+ });
523
+ };
524
+
525
+ registerSessionRegistryInit({
526
+ commandName: "init-session",
527
+ description: "Write the built-in SentinelLayer session MCP tool registry",
528
+ outputSuffix: ".session-tools",
529
+ payloadCommand: "mcp registry init-session",
530
+ successLabel: "SentinelLayer session MCP registry template",
531
+ nextMessage: "Next: run 'sl mcp server init --id sentinelayer-session --registry-file <path>'.",
532
+ });
533
+
534
+ registerSessionRegistryInit({
535
+ commandName: "init-senti-session",
536
+ description: "Write the built-in Senti session MCP tool registry",
537
+ outputSuffix: ".senti-session-tools",
538
+ payloadCommand: "mcp registry init-senti-session",
539
+ successLabel: "Senti session MCP registry template",
540
+ nextMessage: "Next: run 'sl mcp registry init-hosted-session-connector --registry-file <path>' for hosted MCP.",
541
+ });
515
542
 
516
543
  registry
517
544
  .command("init-hosted-session-connector")
@@ -23,6 +23,7 @@ const DEFAULT_WORK_PLAN_SUMMARY_LIMIT = 5;
23
23
  const DEFAULT_ACTIVITY_SNIPPET_MAX_CHARS = 120;
24
24
  const MAX_WORK_PLAN_BYTES = 128_000;
25
25
  const WORK_PLAN_RELATIVE_PATH = "tasks/todo.md";
26
+ const ACTIVE_WORK_PLAN_FILE_PATTERN = /^reconciled-fix-plan-\d{4}-\d{2}-\d{2}\.md$/;
26
27
  const HISTORICAL_WORK_PLAN_MIN_COMPLETED = 25;
27
28
  const HISTORICAL_WORK_PLAN_MIN_TOTAL = 50;
28
29
  const HISTORICAL_WORK_PLAN_COMPLETED_RATIO = 0.75;
@@ -91,18 +92,22 @@ function normalizePositiveInteger(value, fallbackValue) {
91
92
  return Math.max(1, Math.floor(normalized));
92
93
  }
93
94
 
94
- function buildWorkPlanSourceContext(targetPath = process.cwd(), { sourceReason = "caller_target_path" } = {}) {
95
+ function buildWorkPlanSourceContext(
96
+ targetPath = process.cwd(),
97
+ { sourceReason = "caller_target_path", relativePath = WORK_PLAN_RELATIVE_PATH } = {},
98
+ ) {
95
99
  const workspacePath = path.resolve(String(targetPath || "."));
96
100
  const workspaceLabel = normalizeString(path.basename(workspacePath)) || ".";
97
101
  const workspaceFingerprint = createHash("sha256")
98
102
  .update(workspacePath)
99
103
  .digest("hex")
100
104
  .slice(0, 8);
101
- const sourceLabel = `${WORK_PLAN_RELATIVE_PATH} from ${workspaceLabel}#${workspaceFingerprint}`;
105
+ const normalizedRelativePath = normalizeString(relativePath) || WORK_PLAN_RELATIVE_PATH;
106
+ const sourceLabel = `${normalizedRelativePath} from ${workspaceLabel}#${workspaceFingerprint}`;
102
107
  return {
103
108
  workspacePath,
104
- filePath: path.join(workspacePath, WORK_PLAN_RELATIVE_PATH),
105
- path: WORK_PLAN_RELATIVE_PATH,
109
+ filePath: path.join(workspacePath, normalizedRelativePath),
110
+ path: normalizedRelativePath,
106
111
  workspaceLabel,
107
112
  workspaceFingerprint,
108
113
  sourceLabel,
@@ -559,6 +564,37 @@ function selectActionableWorkPlanOpenRecords(records = [], { limit = DEFAULT_WOR
559
564
  return selected.slice(-Math.max(1, normalizePositiveInteger(limit, DEFAULT_WORK_PLAN_SUMMARY_LIMIT)));
560
565
  }
561
566
 
567
+ function parseMarkdownTableCells(line = "") {
568
+ const trimmed = String(line || "").trim();
569
+ if (!trimmed.startsWith("|") || !trimmed.endsWith("|")) {
570
+ return [];
571
+ }
572
+ const cells = trimmed
573
+ .slice(1, -1)
574
+ .split("|")
575
+ .map((cell) => shortWorkPlanText(cell));
576
+ if (cells.length === 0 || cells.every((cell) => /^:?-{3,}:?$/.test(cell))) {
577
+ return [];
578
+ }
579
+ return cells;
580
+ }
581
+
582
+ function isCompletedPairProgrammingRow(cells = []) {
583
+ const joined = cells.map((cell) => normalizeString(cell).toLowerCase()).join(" ");
584
+ return /\bclosed\b/.test(joined) || /\bfully closed\b/.test(joined);
585
+ }
586
+
587
+ function pairProgrammingRowTask(cells = []) {
588
+ const lane = normalizeString(cells[0]);
589
+ const state = normalizeString(cells[3]);
590
+ const gate = normalizeString(cells[4]);
591
+ if (!lane || lane.toLowerCase() === "lane") {
592
+ return "";
593
+ }
594
+ const detail = state || gate;
595
+ return shortWorkPlanText(detail ? `${lane}: ${detail}` : lane);
596
+ }
597
+
562
598
  function formatWorkPlanOpenItems(items = []) {
563
599
  return (Array.isArray(items) ? items : [])
564
600
  .map((item) => {
@@ -593,6 +629,25 @@ function summarizeWorkPlanMarkdown(
593
629
 
594
630
  const taskMatch = /^\s*[-*]\s+\[([ xX])\]\s+(.+?)\s*$/.exec(line);
595
631
  if (!taskMatch) {
632
+ if (/pair-programming matrix/i.test(section)) {
633
+ const cells = parseMarkdownTableCells(line);
634
+ const task = pairProgrammingRowTask(cells);
635
+ if (task) {
636
+ const record = {
637
+ status: isCompletedPairProgrammingRow(cells) ? "completed" : "open",
638
+ section,
639
+ task,
640
+ };
641
+ records.push(record);
642
+ summary.total += 1;
643
+ if (record.status === "completed") {
644
+ summary.completed += 1;
645
+ } else {
646
+ summary.open += 1;
647
+ }
648
+ summary.currentSection = section || summary.currentSection;
649
+ }
650
+ }
596
651
  continue;
597
652
  }
598
653
  const completed = taskMatch[1].toLowerCase() === "x";
@@ -631,12 +686,28 @@ function summarizeWorkPlanMarkdown(
631
686
  return summary;
632
687
  }
633
688
 
634
- async function readWorkPlanSummary({
635
- targetPath = process.cwd(),
636
- limit = DEFAULT_WORK_PLAN_SUMMARY_LIMIT,
637
- sourceReason = "caller_target_path",
638
- } = {}) {
639
- const sourceContext = buildWorkPlanSourceContext(targetPath, { sourceReason });
689
+ async function findActiveWorkPlanSourceContext(targetPath = process.cwd(), { sourceReason = "caller_target_path" } = {}) {
690
+ const workspacePath = path.resolve(String(targetPath || "."));
691
+ const tasksPath = path.join(workspacePath, "tasks");
692
+ try {
693
+ const entries = await fsp.readdir(tasksPath, { withFileTypes: true });
694
+ const candidates = entries
695
+ .filter((entry) => entry.isFile() && ACTIVE_WORK_PLAN_FILE_PATTERN.test(entry.name))
696
+ .map((entry) => entry.name)
697
+ .sort((a, b) => b.localeCompare(a));
698
+ if (candidates.length === 0) {
699
+ return null;
700
+ }
701
+ return buildWorkPlanSourceContext(targetPath, {
702
+ sourceReason,
703
+ relativePath: path.posix.join("tasks", candidates[0]),
704
+ });
705
+ } catch {
706
+ return null;
707
+ }
708
+ }
709
+
710
+ async function readWorkPlanSummaryFromSource(sourceContext, { limit = DEFAULT_WORK_PLAN_SUMMARY_LIMIT } = {}) {
640
711
  try {
641
712
  const stats = await fsp.stat(sourceContext.filePath);
642
713
  let source = "";
@@ -668,6 +739,24 @@ async function readWorkPlanSummary({
668
739
  }
669
740
  }
670
741
 
742
+ async function readWorkPlanSummary({
743
+ targetPath = process.cwd(),
744
+ limit = DEFAULT_WORK_PLAN_SUMMARY_LIMIT,
745
+ sourceReason = "caller_target_path",
746
+ } = {}) {
747
+ const activePlanContext = await findActiveWorkPlanSourceContext(targetPath, { sourceReason });
748
+ if (activePlanContext) {
749
+ const activePlanSummary = await readWorkPlanSummaryFromSource(activePlanContext, { limit });
750
+ if (activePlanSummary.exists && Number(activePlanSummary.total || 0) > 0) {
751
+ return activePlanSummary;
752
+ }
753
+ }
754
+ return readWorkPlanSummaryFromSource(
755
+ buildWorkPlanSourceContext(targetPath, { sourceReason }),
756
+ { limit },
757
+ );
758
+ }
759
+
671
760
  function summarizeTaskLedger(tasks = [], { limit = DEFAULT_TASK_SUMMARY_LIMIT } = {}) {
672
761
  const summary = emptyTaskLedgerSummary();
673
762
  const owners = new Map();