sentinelayer-cli 0.39.2 → 0.41.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 (62) hide show
  1. package/README.md +4 -4
  2. package/package.json +2 -3
  3. package/src/agents/ai-governance/tools/base.js +19 -166
  4. package/src/agents/backend/tools/base.js +33 -186
  5. package/src/agents/code-quality/tools/base.js +27 -154
  6. package/src/agents/data-layer/tools/base.js +35 -180
  7. package/src/agents/documentation/tools/base.js +18 -146
  8. package/src/agents/infrastructure/tools/base.js +17 -164
  9. package/src/agents/observability/tools/base.js +35 -180
  10. package/src/agents/performance/tools/base.js +35 -205
  11. package/src/agents/release/tools/base.js +35 -180
  12. package/src/agents/reliability/tools/base.js +35 -180
  13. package/src/agents/security/tools/base.js +33 -187
  14. package/src/agents/shared-tools/domain-base.js +246 -0
  15. package/src/agents/supply-chain/tools/base.js +15 -144
  16. package/src/agents/testing/tools/base.js +33 -184
  17. package/src/cli.js +5 -0
  18. package/src/commands/mcp.js +1 -1
  19. package/src/commands/ring-owner.js +58 -0
  20. package/src/commands/session.js +703 -263
  21. package/src/engram/governance.js +67 -0
  22. package/src/engram/index.js +64 -0
  23. package/src/engram/namespace.js +89 -0
  24. package/src/engram/observations.js +34 -0
  25. package/src/engram/sla.js +104 -0
  26. package/src/engram/store.js +115 -0
  27. package/src/engram/tools.js +146 -0
  28. package/src/engram/trust.js +105 -0
  29. package/src/legacy-cli.js +6 -3
  30. package/src/mcp/registry.js +27 -10
  31. package/src/mcp/session-stdio-server.js +209 -18
  32. package/src/mcp/smoke.js +9 -8
  33. package/src/pocket/ring-owner.js +97 -0
  34. package/src/review/local-review.js +9 -5
  35. package/src/review/omargate-orchestrator.js +111 -14
  36. package/src/review/spec-binding.js +16 -1
  37. package/src/scan/generator.js +1 -1
  38. package/src/session/agent-registry.js +22 -63
  39. package/src/session/checkpoints.js +194 -0
  40. package/src/session/coordination-guidance.js +6 -6
  41. package/src/session/daemon.js +19 -124
  42. package/src/session/file-lease-integrations.js +1300 -0
  43. package/src/session/file-locks.js +967 -464
  44. package/src/session/listener.js +243 -20
  45. package/src/session/listeners.js +71 -80
  46. package/src/session/paths.js +2 -0
  47. package/src/session/recall/embedder.js +181 -0
  48. package/src/session/recall/entities.js +0 -0
  49. package/src/session/recall/index-build.js +38 -0
  50. package/src/session/recall/index-core.js +108 -0
  51. package/src/session/recall/index.js +131 -0
  52. package/src/session/recall/needle-eval.js +279 -0
  53. package/src/session/recall/observation-core.js +183 -0
  54. package/src/session/recall/observations.js +21 -0
  55. package/src/session/recall/provenance.js +153 -0
  56. package/src/session/recall/retrieve.js +374 -0
  57. package/src/session/recall/text.js +0 -0
  58. package/src/session/recall/token-stats.js +81 -0
  59. package/src/session/recap.js +29 -23
  60. package/src/session/remote-hydrate.js +5 -1
  61. package/src/session/sync.js +661 -11
  62. package/src/session/wake/pump.js +8 -3
@@ -1,151 +1,22 @@
1
- // Shared helpers for Nora's (supply-chain) domain tools (#A22).
1
+ // Shared helpers for Dima's (supply-chain) domain tools (#A22).
2
2
 
3
- import fsp from "node:fs/promises";
4
- import path from "node:path";
5
- import process from "node:process";
3
+ import {
4
+ createFinding as createDomainFinding,
5
+ toPosix,
6
+ walkRepoFiles as walkDomainRepoFiles,
7
+ } from "../../shared-tools/domain-base.js";
6
8
 
7
- import ignore from "ignore";
9
+ const DEFAULT_PERSONA = "supply-chain";
8
10
 
9
- const DEFAULT_IGNORED_DIRS = new Set([
10
- ".git",
11
- "node_modules",
12
- ".venv",
13
- ".next",
14
- "dist",
15
- "build",
16
- "coverage",
17
- ".sentinelayer",
18
- ".sentinel",
19
- ".turbo",
20
- ".idea",
21
- ".vscode",
22
- "__pycache__",
23
- ".cache",
24
- ]);
25
- const MAX_FILE_SIZE_BYTES = 1024 * 1024;
26
- const SEVERITIES = Object.freeze(["P0", "P1", "P2", "P3"]);
11
+ export { toPosix };
27
12
 
28
- export function toPosix(value) {
29
- return String(value || "").replace(/\\/g, "/");
13
+ export function createFinding(options = {}) {
14
+ return createDomainFinding(options, {
15
+ defaultPersona: DEFAULT_PERSONA,
16
+ defaultKind: DEFAULT_PERSONA,
17
+ });
30
18
  }
31
19
 
32
- export function createFinding({
33
- severity,
34
- kind,
35
- file,
36
- line = 0,
37
- evidence = "",
38
- rootCause = "",
39
- recommendedFix = "",
40
- confidence = null,
41
- tool = "",
42
- persona = "supply-chain",
43
- } = {}) {
44
- const normalizedSeverity = SEVERITIES.includes(String(severity || "").toUpperCase())
45
- ? String(severity).toUpperCase()
46
- : "P2";
47
- return {
48
- persona,
49
- tool: String(tool || "").trim(),
50
- kind: String(kind || "").trim() || "supply-chain",
51
- severity: normalizedSeverity,
52
- file: toPosix(file || ""),
53
- line: Number.isFinite(Number(line)) ? Math.max(0, Math.floor(Number(line))) : 0,
54
- evidence: String(evidence || "").trim().slice(0, 400),
55
- rootCause: String(rootCause || "").trim(),
56
- recommendedFix: String(recommendedFix || "").trim(),
57
- confidence:
58
- confidence === null || confidence === undefined
59
- ? null
60
- : Math.max(0, Math.min(1, Number(confidence) || 0)),
61
- };
62
- }
63
-
64
- async function readIgnorePatterns(filePath) {
65
- try {
66
- const raw = await fsp.readFile(filePath, "utf-8");
67
- return String(raw || "")
68
- .split(/\r?\n/)
69
- .map((line) => line.trim())
70
- .filter((line) => line && !line.startsWith("#"));
71
- } catch (err) {
72
- if (err && typeof err === "object" && err.code === "ENOENT") {
73
- return [];
74
- }
75
- throw err;
76
- }
77
- }
78
-
79
- async function createIgnoreMatcher(rootPath) {
80
- const matcher = ignore();
81
- const gitignore = await readIgnorePatterns(path.join(rootPath, ".gitignore"));
82
- const sentinel = await readIgnorePatterns(
83
- path.join(rootPath, ".sentinelayerignore")
84
- );
85
- matcher.add([...gitignore, ...sentinel]);
86
- return (relativePath, isDirectory) => {
87
- const normalized = toPosix(relativePath);
88
- if (!normalized) {
89
- return false;
90
- }
91
- const candidate = isDirectory ? `${normalized}/` : normalized;
92
- return matcher.ignores(candidate);
93
- };
94
- }
95
-
96
- export async function* walkRepoFiles({
97
- rootPath = process.cwd(),
98
- extensions = new Set(),
99
- maxFileSize = MAX_FILE_SIZE_BYTES,
100
- } = {}) {
101
- const resolvedRoot = path.resolve(rootPath);
102
- const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
103
- const wantedExtensions =
104
- extensions instanceof Set
105
- ? extensions
106
- : new Set(Array.isArray(extensions) ? extensions : []);
107
- const stack = [resolvedRoot];
108
- while (stack.length > 0) {
109
- const current = stack.pop();
110
- let entries = [];
111
- try {
112
- entries = await fsp.readdir(current, { withFileTypes: true });
113
- } catch {
114
- continue;
115
- }
116
- for (const entry of entries) {
117
- const fullPath = path.join(current, entry.name);
118
- const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
119
- if (entry.isDirectory()) {
120
- if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
121
- continue;
122
- }
123
- if (ignoreMatcher(relativePath, true)) {
124
- continue;
125
- }
126
- stack.push(fullPath);
127
- continue;
128
- }
129
- if (!entry.isFile()) {
130
- continue;
131
- }
132
- if (ignoreMatcher(relativePath, false)) {
133
- continue;
134
- }
135
- const ext = path.extname(entry.name).toLowerCase();
136
- if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
137
- continue;
138
- }
139
- let stat = null;
140
- try {
141
- stat = await fsp.stat(fullPath);
142
- } catch {
143
- stat = null;
144
- }
145
- if (!stat || stat.size > maxFileSize) {
146
- continue;
147
- }
148
- yield { fullPath, relativePath };
149
- }
150
- }
20
+ export async function* walkRepoFiles(options = {}) {
21
+ yield* walkDomainRepoFiles(options);
151
22
  }
@@ -1,188 +1,18 @@
1
1
  // Shared helpers for Priya's (testing) domain tools (#A15).
2
- // Same pattern as security/tools/base.js and backend/tools/base.js — default
3
- // persona is "testing". Later refactor (#A25+) can hoist to shared-tools/.
4
2
 
5
- import fsp from "node:fs/promises";
6
- import path from "node:path";
7
- import process from "node:process";
3
+ import {
4
+ DEFAULT_IGNORED_DIRS,
5
+ MAX_FILE_SIZE_BYTES,
6
+ SEVERITIES,
7
+ createFinding as createDomainFinding,
8
+ findLineMatches,
9
+ getLineContent,
10
+ normalizeSeverity,
11
+ toPosix,
12
+ walkRepoFiles as walkDomainRepoFiles,
13
+ } from "../../shared-tools/domain-base.js";
8
14
 
9
- import ignore from "ignore";
10
-
11
- const DEFAULT_IGNORED_DIRS = new Set([
12
- ".git",
13
- "node_modules",
14
- ".venv",
15
- ".next",
16
- "dist",
17
- "build",
18
- "coverage",
19
- ".sentinelayer",
20
- ".sentinel",
21
- ".turbo",
22
- ".idea",
23
- ".vscode",
24
- "__pycache__",
25
- ".cache",
26
- ]);
27
- const MAX_FILE_SIZE_BYTES = 1024 * 1024;
28
- const SEVERITIES = Object.freeze(["P0", "P1", "P2", "P3"]);
29
-
30
- export function toPosix(value) {
31
- return String(value || "").replace(/\\/g, "/");
32
- }
33
-
34
- export function normalizeSeverity(value) {
35
- const normalized = String(value || "").trim().toUpperCase();
36
- if (SEVERITIES.includes(normalized)) {
37
- return normalized;
38
- }
39
- return "P2";
40
- }
41
-
42
- export function createFinding({
43
- severity,
44
- kind,
45
- file,
46
- line = 0,
47
- evidence = "",
48
- rootCause = "",
49
- recommendedFix = "",
50
- confidence = null,
51
- tool = "",
52
- persona = "testing",
53
- } = {}) {
54
- return {
55
- persona,
56
- tool: String(tool || "").trim(),
57
- kind: String(kind || "").trim() || "testing",
58
- severity: normalizeSeverity(severity),
59
- file: toPosix(file || ""),
60
- line: Number.isFinite(Number(line)) ? Math.max(0, Math.floor(Number(line))) : 0,
61
- evidence: String(evidence || "").trim().slice(0, 400),
62
- rootCause: String(rootCause || "").trim(),
63
- recommendedFix: String(recommendedFix || "").trim(),
64
- confidence:
65
- confidence === null || confidence === undefined
66
- ? null
67
- : Math.max(0, Math.min(1, Number(confidence) || 0)),
68
- };
69
- }
70
-
71
- async function readIgnorePatterns(filePath) {
72
- try {
73
- const raw = await fsp.readFile(filePath, "utf-8");
74
- return String(raw || "")
75
- .split(/\r?\n/)
76
- .map((line) => line.trim())
77
- .filter((line) => line && !line.startsWith("#"));
78
- } catch (err) {
79
- if (err && typeof err === "object" && err.code === "ENOENT") {
80
- return [];
81
- }
82
- throw err;
83
- }
84
- }
85
-
86
- async function createIgnoreMatcher(rootPath) {
87
- const matcher = ignore();
88
- const gitignore = await readIgnorePatterns(path.join(rootPath, ".gitignore"));
89
- const sentinel = await readIgnorePatterns(
90
- path.join(rootPath, ".sentinelayerignore")
91
- );
92
- matcher.add([...gitignore, ...sentinel]);
93
- return (relativePath, isDirectory) => {
94
- const normalized = toPosix(relativePath);
95
- if (!normalized) {
96
- return false;
97
- }
98
- const candidate = isDirectory ? `${normalized}/` : normalized;
99
- return matcher.ignores(candidate);
100
- };
101
- }
102
-
103
- export async function* walkRepoFiles({
104
- rootPath = process.cwd(),
105
- extensions = new Set(),
106
- maxFileSize = MAX_FILE_SIZE_BYTES,
107
- } = {}) {
108
- const resolvedRoot = path.resolve(rootPath);
109
- const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
110
- const wantedExtensions =
111
- extensions instanceof Set
112
- ? extensions
113
- : new Set(Array.isArray(extensions) ? extensions : []);
114
- const stack = [resolvedRoot];
115
- while (stack.length > 0) {
116
- const current = stack.pop();
117
- let entries = [];
118
- try {
119
- entries = await fsp.readdir(current, { withFileTypes: true });
120
- } catch {
121
- continue;
122
- }
123
- for (const entry of entries) {
124
- const fullPath = path.join(current, entry.name);
125
- const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
126
- if (entry.isDirectory()) {
127
- if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
128
- continue;
129
- }
130
- if (ignoreMatcher(relativePath, true)) {
131
- continue;
132
- }
133
- stack.push(fullPath);
134
- continue;
135
- }
136
- if (!entry.isFile()) {
137
- continue;
138
- }
139
- if (ignoreMatcher(relativePath, false)) {
140
- continue;
141
- }
142
- const ext = path.extname(entry.name).toLowerCase();
143
- if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
144
- continue;
145
- }
146
- let stat = null;
147
- try {
148
- stat = await fsp.stat(fullPath);
149
- } catch {
150
- stat = null;
151
- }
152
- if (!stat || stat.size > maxFileSize) {
153
- continue;
154
- }
155
- yield { fullPath, relativePath, stat };
156
- }
157
- }
158
- }
159
-
160
- export function findLineMatches(content, pattern) {
161
- const text = String(content || "");
162
- if (!pattern) {
163
- return [];
164
- }
165
- const global = new RegExp(
166
- pattern.source,
167
- pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`
168
- );
169
- const matches = [];
170
- let match;
171
- while ((match = global.exec(text)) !== null) {
172
- const lineIndex = text.slice(0, match.index).split(/\r?\n/).length;
173
- matches.push({
174
- index: match.index,
175
- line: lineIndex,
176
- match: match[0],
177
- });
178
- }
179
- return matches;
180
- }
181
-
182
- export function getLineContent(content, line) {
183
- const lines = String(content || "").split(/\r?\n/);
184
- return (lines[Math.max(0, (Number(line) || 1) - 1)] || "").trim();
185
- }
15
+ const DEFAULT_PERSONA = "testing";
186
16
 
187
17
  const TEST_FILE_PATTERNS = [
188
18
  /\.test\.(js|jsx|ts|tsx|mjs|cjs)$/,
@@ -194,9 +24,28 @@ const TEST_FILE_PATTERNS = [
194
24
  /(^|\/)tests?\/[^/]*\.py$/,
195
25
  ];
196
26
 
27
+ export {
28
+ DEFAULT_IGNORED_DIRS,
29
+ MAX_FILE_SIZE_BYTES,
30
+ SEVERITIES,
31
+ findLineMatches,
32
+ getLineContent,
33
+ normalizeSeverity,
34
+ toPosix,
35
+ };
36
+
37
+ export function createFinding(options = {}) {
38
+ return createDomainFinding(options, {
39
+ defaultPersona: DEFAULT_PERSONA,
40
+ defaultKind: DEFAULT_PERSONA,
41
+ });
42
+ }
43
+
44
+ export async function* walkRepoFiles(options = {}) {
45
+ yield* walkDomainRepoFiles(options, { entryMetadata: "stat" });
46
+ }
47
+
197
48
  export function isTestFile(relativePath) {
198
49
  const normalized = toPosix(relativePath);
199
50
  return TEST_FILE_PATTERNS.some((pattern) => pattern.test(normalized));
200
51
  }
201
-
202
- export { DEFAULT_IGNORED_DIRS, MAX_FILE_SIZE_BYTES, SEVERITIES };
package/src/cli.js CHANGED
@@ -124,6 +124,11 @@ const COMMAND_REGISTRARS = {
124
124
  exportName: "registerSessionCommand",
125
125
  needsLegacy: false,
126
126
  },
127
+ "ring-owner": {
128
+ loader: () => import("./commands/ring-owner.js"),
129
+ exportName: "registerRingOwnerCommand",
130
+ needsLegacy: false,
131
+ },
127
132
  };
128
133
 
129
134
  const COMMAND_SET = new Set(Object.keys(COMMAND_REGISTRARS));
@@ -236,7 +236,7 @@ Docs: https://github.com/mrrCarter/create-sentinelayer/blob/main/docs/mcp.md`,
236
236
  .description(
237
237
  "Smoke the hosted MCP resource with an in-memory bearer; prints only redacted proof"
238
238
  )
239
- .option("--session <id>", "Optional Senti session id to read through sessions.events.list")
239
+ .option("--session <id>", "Optional Senti session id to read through sessions_events_list")
240
240
  .option("--limit <n>", "Maximum session events to read when --session is set", "5")
241
241
  .option(
242
242
  "--scope <scopes>",
@@ -0,0 +1,58 @@
1
+ import process from "node:process";
2
+
3
+ import { ringOwner } from "../pocket/ring-owner.js";
4
+
5
+ function shouldEmitJson(options, command) {
6
+ if (options && options.json) return true;
7
+ return typeof command?.optsWithGlobals === "function" ? Boolean(command.optsWithGlobals().json) : false;
8
+ }
9
+
10
+ export function registerRingOwnerCommand(program) {
11
+ program
12
+ .command("ring-owner")
13
+ .description("Ring the owner's phone about a decision that needs them (Senti Pocket dial)")
14
+ .argument("<question>", "The question / decision to put to the owner")
15
+ .requiredOption("--session <id>", "The session this decision belongs to")
16
+ .option("--kind <kind>", "decisionYours | pickOption | go | info | checkpointReady", "decisionYours")
17
+ .option(
18
+ "--option <label>",
19
+ "A pickOption choice (repeat for multiple)",
20
+ (value, acc) => { acc.push(value); return acc; },
21
+ [],
22
+ )
23
+ .option("--what-we-need <text>", "A short 'what we need' lead-in for the ring")
24
+ .option("--checkpoint <id>", "An optional checkpoint id for jump-to context")
25
+ .option("--idempotency-key <key>", "Dedupe key so an accidental retry rings only once")
26
+ .option("--gateway-url <url>", "Pocket gateway base URL (else the SENTI_POCKET_URL env var)")
27
+ .option("--json", "Emit machine-readable output")
28
+ .action(async (question, options, command) => {
29
+ const emitJson = shouldEmitJson(options, command);
30
+ try {
31
+ const result = await ringOwner(question, {
32
+ kind: options.kind,
33
+ sessionId: options.session,
34
+ options: options.option || [],
35
+ whatWeNeed: options.whatWeNeed,
36
+ checkpointId: options.checkpoint,
37
+ idempotencyKey: options.idempotencyKey,
38
+ gatewayUrl: options.gatewayUrl,
39
+ env: process.env,
40
+ });
41
+ if (emitJson) {
42
+ process.stdout.write(`${JSON.stringify(result)}\n`);
43
+ return;
44
+ }
45
+ if (result && result.dispatched) {
46
+ const replay = result.idempotent ? " (idempotent replay — already ringing)" : "";
47
+ process.stdout.write(`Ringing the owner — dialId ${result.dialId}${replay}\n`);
48
+ } else {
49
+ const reason = result && result.reason ? `: ${result.reason}` : "";
50
+ process.stdout.write(`Not dispatched${reason} (dialId ${(result && result.dialId) || "n/a"})\n`);
51
+ process.exitCode = 1;
52
+ }
53
+ } catch (err) {
54
+ process.stderr.write(`ring-owner failed: ${err && err.message ? err.message : String(err)}\n`);
55
+ process.exitCode = 1;
56
+ }
57
+ });
58
+ }