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
package/README.md CHANGED
@@ -142,14 +142,14 @@ Then point your agents at it:
142
142
 
143
143
  ```bash
144
144
  sl session join <session-id> --agent <agent-name>
145
- sl session say <session-id> "status: starting on auth middleware" --agent <agent-name>
145
+ sl session action <session-id> working_on --target-sequence <n>
146
146
  ```
147
147
 
148
148
  Sentinelayer includes a deterministic session coordination surface for multi-agent coding loops:
149
149
 
150
150
  - session event stream and replay (`start`, `join`, `say`, `read`, `status`, `leave`, `list`, `kill`)
151
- - low-noise message actions (`react ack|like|dislike`, `action working_on|disregard`, `reply`/`comment`, `view`, `actions`)
152
- - agent lifecycle controls (join/heartbeat/leave/kill)
151
+ - low-noise message actions (`react ack|like|dislike`, `action working_on|disregard`, `reply`/`comment`, `actions`) plus one monotonic per-agent read cursor (`view`)
152
+ - agent lifecycle controls (join/ephemeral TTL presence/leave/kill); listener liveness never enters the durable transcript
153
153
  - recap and context briefing for late-joining agents
154
154
  - analytics + lineage artifacts at session closeout
155
155
 
@@ -694,7 +694,7 @@ Use `init-aidenid` to scaffold an Anthropic-compatible tool schema wrapper for A
694
694
  Use `init-aidenid-adapter` to scaffold a deterministic AIdenID provisioning API contract (tool binding -> HTTP path/method -> response field mapping) and cross-check it against the registry with `validate-aidenid-adapter`.
695
695
  Use `token mint` to request a short-lived hosted MCP bearer token from the Sentinelayer API. Text output hides the bearer value; `--json` is the explicit operator path that includes `accessToken`. The command is blocked from the generated CLI MCP bridge.
696
696
  Use `smoke` to verify the hosted MCP resource without printing the bearer. The canonical remote MCP paste URL is `https://api.sentinelayer.com/mcp`; the token audience/resource value is `https://mcp.sentinelayer.com` and is handled by the OAuth/MCP flow. `sl mcp smoke` works today through the CLI direct-mint path; third-party browser connectors such as Claude.ai still require the OAuth browser-flow and connector-registration work to complete before users can authenticate from that product.
697
- Use `init-session` plus `server run` for the local stdio Senti MCP server. It exposes session inbox, durable posts, actions/replies/reactions, file locks, and attention requests to local MCP clients. Hosted Claude-web/ChatGPT connectors need a separate HTTPS/OAuth service and are not shipped by the local stdio server. See [docs/mcp.md](docs/mcp.md).
697
+ Use `init-session` plus `server run` for the local stdio Senti MCP server. It exposes session inbox, durable posts, actions/replies/reactions, API-authoritative file leases, and attention requests to local MCP clients. File-lease lifecycle operations never write session events; install edit preflights with `sl session guard-install` and remove them with `sl session guard-uninstall` before any CLI rollback. Hosted Claude-web/ChatGPT connectors need a separate HTTPS/OAuth service and are not shipped by the local stdio server. See [docs/mcp.md](docs/mcp.md) and [docs/file-leases.md](docs/file-leases.md).
698
698
  Use `init-cli` to generate a bridge registry for every `sl` leaf command from Commander metadata. This exposes the full CLI surface as `sl.<command.path>` tool schemas, but execution still requires a bridge-capable MCP host with OAuth/session-seat policy and human approval enforcement. The hosted connector target contract is documented in [docs/mcp-hosted-connector.md](docs/mcp-hosted-connector.md).
699
699
 
700
700
  MCP operator verification:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.39.2",
3
+ "version": "0.41.0",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -66,8 +66,7 @@
66
66
  "zod": "4.1.12"
67
67
  },
68
68
  "devDependencies": {
69
- "c8": "10.1.3",
70
- "license-checker-rseidelsohn": "4.4.2"
69
+ "c8": "11.0.0"
71
70
  },
72
71
  "overrides": {
73
72
  "lighthouse": {
@@ -1,171 +1,24 @@
1
1
  // Shared helpers for Amina's (ai-governance) domain tools (#A24).
2
2
 
3
- import fsp from "node:fs/promises";
4
- import path from "node:path";
5
- import process from "node:process";
6
-
7
- import ignore from "ignore";
8
-
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"]);
27
-
28
- export function toPosix(value) {
29
- return String(value || "").replace(/\\/g, "/");
30
- }
31
-
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 = "ai-governance",
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() || "ai-governance",
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 function findLineMatches(content, pattern) {
97
- const text = String(content || "");
98
- const global = new RegExp(
99
- pattern.source,
100
- pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`
101
- );
102
- const matches = [];
103
- let match;
104
- while ((match = global.exec(text)) !== null) {
105
- const lineIndex = text.slice(0, match.index).split(/\r?\n/).length;
106
- matches.push({ index: match.index, line: lineIndex, match: match[0] });
107
- }
108
- return matches;
109
- }
110
-
111
- export function getLineContent(content, line) {
112
- const lines = String(content || "").split(/\r?\n/);
113
- return (lines[Math.max(0, (Number(line) || 1) - 1)] || "").trim();
3
+ import {
4
+ createFinding as createDomainFinding,
5
+ findLineMatches,
6
+ getLineContent,
7
+ toPosix,
8
+ walkRepoFiles as walkDomainRepoFiles,
9
+ } from "../../shared-tools/domain-base.js";
10
+
11
+ const DEFAULT_PERSONA = "ai-governance";
12
+
13
+ export { findLineMatches, getLineContent, toPosix };
14
+
15
+ export function createFinding(options = {}) {
16
+ return createDomainFinding(options, {
17
+ defaultPersona: DEFAULT_PERSONA,
18
+ defaultKind: DEFAULT_PERSONA,
19
+ });
114
20
  }
115
21
 
116
- export async function* walkRepoFiles({
117
- rootPath = process.cwd(),
118
- extensions = new Set(),
119
- maxFileSize = MAX_FILE_SIZE_BYTES,
120
- } = {}) {
121
- const resolvedRoot = path.resolve(rootPath);
122
- const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
123
- const wantedExtensions =
124
- extensions instanceof Set
125
- ? extensions
126
- : new Set(Array.isArray(extensions) ? extensions : []);
127
- const stack = [resolvedRoot];
128
- while (stack.length > 0) {
129
- const current = stack.pop();
130
- let entries = [];
131
- try {
132
- entries = await fsp.readdir(current, { withFileTypes: true });
133
- } catch {
134
- continue;
135
- }
136
- for (const entry of entries) {
137
- const fullPath = path.join(current, entry.name);
138
- const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
139
- if (entry.isDirectory()) {
140
- if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
141
- continue;
142
- }
143
- if (ignoreMatcher(relativePath, true)) {
144
- continue;
145
- }
146
- stack.push(fullPath);
147
- continue;
148
- }
149
- if (!entry.isFile()) {
150
- continue;
151
- }
152
- if (ignoreMatcher(relativePath, false)) {
153
- continue;
154
- }
155
- const ext = path.extname(entry.name).toLowerCase();
156
- if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
157
- continue;
158
- }
159
- let stat = null;
160
- try {
161
- stat = await fsp.stat(fullPath);
162
- } catch {
163
- stat = null;
164
- }
165
- if (!stat || stat.size > maxFileSize) {
166
- continue;
167
- }
168
- yield { fullPath, relativePath, stat };
169
- }
170
- }
22
+ export async function* walkRepoFiles(options = {}) {
23
+ yield* walkDomainRepoFiles(options, { entryMetadata: "stat" });
171
24
  }
@@ -1,189 +1,36 @@
1
1
  // Shared helpers for Maya's (backend) domain tools (#A14).
2
- // Mirrors src/agents/security/tools/base.js but with persona defaulting to
3
- // "backend". Intentional per-persona copy — a later refactor (#A25+) can
4
- // hoist this to src/agents/shared-tools/ once the 12 persona modules are
5
- // shipped and the common surface is stable.
6
2
 
7
- import fsp from "node:fs/promises";
8
- import path from "node:path";
9
- import process from "node:process";
10
-
11
- import ignore from "ignore";
12
-
13
- const DEFAULT_IGNORED_DIRS = new Set([
14
- ".git",
15
- "node_modules",
16
- ".venv",
17
- ".next",
18
- "dist",
19
- "build",
20
- "coverage",
21
- ".sentinelayer",
22
- ".sentinel",
23
- ".turbo",
24
- ".idea",
25
- ".vscode",
26
- "__pycache__",
27
- ".cache",
28
- ]);
29
- const MAX_FILE_SIZE_BYTES = 1024 * 1024;
30
- const SEVERITIES = Object.freeze(["P0", "P1", "P2", "P3"]);
31
-
32
- export function toPosix(value) {
33
- return String(value || "").replace(/\\/g, "/");
34
- }
35
-
36
- export function normalizeSeverity(value) {
37
- const normalized = String(value || "").trim().toUpperCase();
38
- if (SEVERITIES.includes(normalized)) {
39
- return normalized;
40
- }
41
- return "P2";
42
- }
43
-
44
- export function createFinding({
45
- severity,
46
- kind,
47
- file,
48
- line = 0,
49
- evidence = "",
50
- rootCause = "",
51
- recommendedFix = "",
52
- confidence = null,
53
- tool = "",
54
- persona = "backend",
55
- } = {}) {
56
- return {
57
- persona,
58
- tool: String(tool || "").trim(),
59
- kind: String(kind || "").trim() || "backend",
60
- severity: normalizeSeverity(severity),
61
- file: toPosix(file || ""),
62
- line: Number.isFinite(Number(line)) ? Math.max(0, Math.floor(Number(line))) : 0,
63
- evidence: String(evidence || "").trim().slice(0, 400),
64
- rootCause: String(rootCause || "").trim(),
65
- recommendedFix: String(recommendedFix || "").trim(),
66
- confidence:
67
- confidence === null || confidence === undefined
68
- ? null
69
- : Math.max(0, Math.min(1, Number(confidence) || 0)),
70
- };
71
- }
72
-
73
- async function readIgnorePatterns(filePath) {
74
- try {
75
- const raw = await fsp.readFile(filePath, "utf-8");
76
- return String(raw || "")
77
- .split(/\r?\n/)
78
- .map((line) => line.trim())
79
- .filter((line) => line && !line.startsWith("#"));
80
- } catch (err) {
81
- if (err && typeof err === "object" && err.code === "ENOENT") {
82
- return [];
83
- }
84
- throw err;
85
- }
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";
14
+
15
+ const DEFAULT_PERSONA = "backend";
16
+
17
+ export {
18
+ DEFAULT_IGNORED_DIRS,
19
+ MAX_FILE_SIZE_BYTES,
20
+ SEVERITIES,
21
+ findLineMatches,
22
+ getLineContent,
23
+ normalizeSeverity,
24
+ toPosix,
25
+ };
26
+
27
+ export function createFinding(options = {}) {
28
+ return createDomainFinding(options, {
29
+ defaultPersona: DEFAULT_PERSONA,
30
+ defaultKind: DEFAULT_PERSONA,
31
+ });
32
+ }
33
+
34
+ export async function* walkRepoFiles(options = {}) {
35
+ yield* walkDomainRepoFiles(options);
86
36
  }
87
-
88
- async function createIgnoreMatcher(rootPath) {
89
- const matcher = ignore();
90
- const gitignore = await readIgnorePatterns(path.join(rootPath, ".gitignore"));
91
- const sentinel = await readIgnorePatterns(
92
- path.join(rootPath, ".sentinelayerignore")
93
- );
94
- matcher.add([...gitignore, ...sentinel]);
95
- return (relativePath, isDirectory) => {
96
- const normalized = toPosix(relativePath);
97
- if (!normalized) {
98
- return false;
99
- }
100
- const candidate = isDirectory ? `${normalized}/` : normalized;
101
- return matcher.ignores(candidate);
102
- };
103
- }
104
-
105
- export async function* walkRepoFiles({
106
- rootPath = process.cwd(),
107
- extensions = new Set(),
108
- maxFileSize = MAX_FILE_SIZE_BYTES,
109
- } = {}) {
110
- const resolvedRoot = path.resolve(rootPath);
111
- const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
112
- const wantedExtensions =
113
- extensions instanceof Set
114
- ? extensions
115
- : new Set(Array.isArray(extensions) ? extensions : []);
116
- const stack = [resolvedRoot];
117
- while (stack.length > 0) {
118
- const current = stack.pop();
119
- let entries = [];
120
- try {
121
- entries = await fsp.readdir(current, { withFileTypes: true });
122
- } catch {
123
- continue;
124
- }
125
- for (const entry of entries) {
126
- const fullPath = path.join(current, entry.name);
127
- const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
128
- if (entry.isDirectory()) {
129
- if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
130
- continue;
131
- }
132
- if (ignoreMatcher(relativePath, true)) {
133
- continue;
134
- }
135
- stack.push(fullPath);
136
- continue;
137
- }
138
- if (!entry.isFile()) {
139
- continue;
140
- }
141
- if (ignoreMatcher(relativePath, false)) {
142
- continue;
143
- }
144
- const ext = path.extname(entry.name).toLowerCase();
145
- if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
146
- continue;
147
- }
148
- let stat = null;
149
- try {
150
- stat = await fsp.stat(fullPath);
151
- } catch {
152
- stat = null;
153
- }
154
- if (!stat || stat.size > maxFileSize) {
155
- continue;
156
- }
157
- yield { fullPath, relativePath };
158
- }
159
- }
160
- }
161
-
162
- export function findLineMatches(content, pattern) {
163
- const text = String(content || "");
164
- if (!pattern) {
165
- return [];
166
- }
167
- const global = new RegExp(
168
- pattern.source,
169
- pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`
170
- );
171
- const matches = [];
172
- let match;
173
- while ((match = global.exec(text)) !== null) {
174
- const lineIndex = text.slice(0, match.index).split(/\r?\n/).length;
175
- matches.push({
176
- index: match.index,
177
- line: lineIndex,
178
- match: match[0],
179
- });
180
- }
181
- return matches;
182
- }
183
-
184
- export function getLineContent(content, line) {
185
- const lines = String(content || "").split(/\r?\n/);
186
- return (lines[Math.max(0, (Number(line) || 1) - 1)] || "").trim();
187
- }
188
-
189
- export { DEFAULT_IGNORED_DIRS, MAX_FILE_SIZE_BYTES, SEVERITIES };
@@ -1,159 +1,32 @@
1
1
  // Shared helpers for Ethan's (code-quality) domain tools (#A16).
2
- // Same per-persona base pattern as security / backend / testing.
3
2
 
4
- import fsp from "node:fs/promises";
5
- import path from "node:path";
6
- import process from "node:process";
7
-
8
- import ignore from "ignore";
9
-
10
- const DEFAULT_IGNORED_DIRS = new Set([
11
- ".git",
12
- "node_modules",
13
- ".venv",
14
- ".next",
15
- "dist",
16
- "build",
17
- "coverage",
18
- ".sentinelayer",
19
- ".sentinel",
20
- ".turbo",
21
- ".idea",
22
- ".vscode",
23
- "__pycache__",
24
- ".cache",
25
- ]);
26
- const MAX_FILE_SIZE_BYTES = 1024 * 1024;
27
- const SEVERITIES = Object.freeze(["P0", "P1", "P2", "P3"]);
28
-
29
- export function toPosix(value) {
30
- return String(value || "").replace(/\\/g, "/");
31
- }
32
-
33
- export function normalizeSeverity(value) {
34
- const normalized = String(value || "").trim().toUpperCase();
35
- if (SEVERITIES.includes(normalized)) {
36
- return normalized;
37
- }
38
- return "P2";
39
- }
40
-
41
- export function createFinding({
42
- severity,
43
- kind,
44
- file,
45
- line = 0,
46
- evidence = "",
47
- rootCause = "",
48
- recommendedFix = "",
49
- confidence = null,
50
- tool = "",
51
- persona = "code-quality",
52
- } = {}) {
53
- return {
54
- persona,
55
- tool: String(tool || "").trim(),
56
- kind: String(kind || "").trim() || "code-quality",
57
- severity: normalizeSeverity(severity),
58
- file: toPosix(file || ""),
59
- line: Number.isFinite(Number(line)) ? Math.max(0, Math.floor(Number(line))) : 0,
60
- evidence: String(evidence || "").trim().slice(0, 400),
61
- rootCause: String(rootCause || "").trim(),
62
- recommendedFix: String(recommendedFix || "").trim(),
63
- confidence:
64
- confidence === null || confidence === undefined
65
- ? null
66
- : Math.max(0, Math.min(1, Number(confidence) || 0)),
67
- };
68
- }
69
-
70
- async function readIgnorePatterns(filePath) {
71
- try {
72
- const raw = await fsp.readFile(filePath, "utf-8");
73
- return String(raw || "")
74
- .split(/\r?\n/)
75
- .map((line) => line.trim())
76
- .filter((line) => line && !line.startsWith("#"));
77
- } catch (err) {
78
- if (err && typeof err === "object" && err.code === "ENOENT") {
79
- return [];
80
- }
81
- throw err;
82
- }
3
+ import {
4
+ DEFAULT_IGNORED_DIRS,
5
+ MAX_FILE_SIZE_BYTES,
6
+ SEVERITIES,
7
+ createFinding as createDomainFinding,
8
+ normalizeSeverity,
9
+ toPosix,
10
+ walkRepoFiles as walkDomainRepoFiles,
11
+ } from "../../shared-tools/domain-base.js";
12
+
13
+ const DEFAULT_PERSONA = "code-quality";
14
+
15
+ export {
16
+ DEFAULT_IGNORED_DIRS,
17
+ MAX_FILE_SIZE_BYTES,
18
+ SEVERITIES,
19
+ normalizeSeverity,
20
+ toPosix,
21
+ };
22
+
23
+ export function createFinding(options = {}) {
24
+ return createDomainFinding(options, {
25
+ defaultPersona: DEFAULT_PERSONA,
26
+ defaultKind: DEFAULT_PERSONA,
27
+ });
83
28
  }
84
29
 
85
- async function createIgnoreMatcher(rootPath) {
86
- const matcher = ignore();
87
- const gitignore = await readIgnorePatterns(path.join(rootPath, ".gitignore"));
88
- const sentinel = await readIgnorePatterns(
89
- path.join(rootPath, ".sentinelayerignore")
90
- );
91
- matcher.add([...gitignore, ...sentinel]);
92
- return (relativePath, isDirectory) => {
93
- const normalized = toPosix(relativePath);
94
- if (!normalized) {
95
- return false;
96
- }
97
- const candidate = isDirectory ? `${normalized}/` : normalized;
98
- return matcher.ignores(candidate);
99
- };
30
+ export async function* walkRepoFiles(options = {}) {
31
+ yield* walkDomainRepoFiles(options);
100
32
  }
101
-
102
- export async function* walkRepoFiles({
103
- rootPath = process.cwd(),
104
- extensions = new Set(),
105
- maxFileSize = MAX_FILE_SIZE_BYTES,
106
- } = {}) {
107
- const resolvedRoot = path.resolve(rootPath);
108
- const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
109
- const wantedExtensions =
110
- extensions instanceof Set
111
- ? extensions
112
- : new Set(Array.isArray(extensions) ? extensions : []);
113
- const stack = [resolvedRoot];
114
- while (stack.length > 0) {
115
- const current = stack.pop();
116
- let entries = [];
117
- try {
118
- entries = await fsp.readdir(current, { withFileTypes: true });
119
- } catch {
120
- continue;
121
- }
122
- for (const entry of entries) {
123
- const fullPath = path.join(current, entry.name);
124
- const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
125
- if (entry.isDirectory()) {
126
- if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
127
- continue;
128
- }
129
- if (ignoreMatcher(relativePath, true)) {
130
- continue;
131
- }
132
- stack.push(fullPath);
133
- continue;
134
- }
135
- if (!entry.isFile()) {
136
- continue;
137
- }
138
- if (ignoreMatcher(relativePath, false)) {
139
- continue;
140
- }
141
- const ext = path.extname(entry.name).toLowerCase();
142
- if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
143
- continue;
144
- }
145
- let stat = null;
146
- try {
147
- stat = await fsp.stat(fullPath);
148
- } catch {
149
- stat = null;
150
- }
151
- if (!stat || stat.size > maxFileSize) {
152
- continue;
153
- }
154
- yield { fullPath, relativePath };
155
- }
156
- }
157
- }
158
-
159
- export { DEFAULT_IGNORED_DIRS, MAX_FILE_SIZE_BYTES, SEVERITIES };