sentinelayer-cli 0.46.5 → 0.48.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 (133) hide show
  1. package/README.md +47 -3
  2. package/package.json +12 -7
  3. package/skills/omar-gate-setup/SKILL.md +103 -0
  4. package/skills/senti-agent-identity/SKILL.md +80 -0
  5. package/skills/senti-judge/SKILL.md +139 -0
  6. package/skills/senti-room-notifications/SKILL.md +73 -0
  7. package/skills/senti-room-notifications/listen-wrapper.cjs +150 -0
  8. package/skills/senti-room-notifications/package.json +1 -0
  9. package/skills/senti-room-notifications/test-classifier.js +109 -0
  10. package/skills/senti-room-notifications/test-receipt.js +54 -0
  11. package/skills/sentinelayer-cli/SKILL.md +222 -0
  12. package/src/agents/devtestbot/runner.js +22 -11
  13. package/src/agents/devtestbot/tool.js +24 -11
  14. package/src/agents/jules/fix-cycle.js +7 -1
  15. package/src/agents/jules/tools/runtime-audit.js +17 -12
  16. package/src/agents/shared-tools/dispatch-core.js +10 -5
  17. package/src/ai/aidenid.js +4 -2
  18. package/src/ai/domain-target-store.js +11 -5
  19. package/src/ai/identity-store.js +11 -5
  20. package/src/ai/site-store.js +8 -4
  21. package/src/audit/orchestrator.js +21 -14
  22. package/src/audit/package.js +6 -4
  23. package/src/audit/persona-loop.js +7 -0
  24. package/src/audit/replay.js +4 -2
  25. package/src/auth/gate.js +32 -1
  26. package/src/auth/session-store.js +37 -0
  27. package/src/board/evidence-resolvers.js +3 -4
  28. package/src/cli.js +23 -3
  29. package/src/commands/ai/identity-lifecycle.js +30 -26
  30. package/src/commands/ai/provision-governance.js +32 -24
  31. package/src/commands/ai/shared.js +6 -4
  32. package/src/commands/audit.js +14 -12
  33. package/src/commands/board.js +2 -2
  34. package/src/commands/chat.js +8 -6
  35. package/src/commands/judge.js +125 -0
  36. package/src/commands/legacy-args.js +1 -0
  37. package/src/commands/mcp.js +45 -52
  38. package/src/commands/omargate.js +2 -1
  39. package/src/commands/plugin.js +11 -3
  40. package/src/commands/review.js +9 -4
  41. package/src/commands/scan.js +7 -7
  42. package/src/commands/session.js +516 -30
  43. package/src/commands/skills.js +45 -0
  44. package/src/commands/watch.js +9 -7
  45. package/src/config/output-filesystem.js +156 -0
  46. package/src/config/schema.js +4 -2
  47. package/src/config/service.js +40 -4
  48. package/src/cost/history.js +15 -9
  49. package/src/daemon/artifact-lineage.js +15 -12
  50. package/src/daemon/assignment-ledger.js +31 -25
  51. package/src/daemon/ast-drift.js +7 -3
  52. package/src/daemon/budget-governor.js +26 -20
  53. package/src/daemon/error-worker.js +28 -22
  54. package/src/daemon/fix-cycle.js +5 -4
  55. package/src/daemon/hybrid-mapper.js +19 -16
  56. package/src/daemon/ingest-refresh.js +4 -0
  57. package/src/daemon/jira-lifecycle.js +21 -17
  58. package/src/daemon/operator-control.js +18 -15
  59. package/src/daemon/reliability-lane.js +18 -15
  60. package/src/daemon/scope-engine.js +12 -8
  61. package/src/daemon/watchdog.js +15 -12
  62. package/src/engram/build-state.js +24 -3
  63. package/src/engram/capability-index.js +367 -0
  64. package/src/engram/compile-graph.js +1063 -0
  65. package/src/engram/contracts/action-receipt.js +82 -0
  66. package/src/engram/contracts/capability-index.js +259 -0
  67. package/src/engram/contracts/capability.js +377 -0
  68. package/src/engram/contracts/content-hash.js +8 -0
  69. package/src/engram/contracts/evidence.js +258 -0
  70. package/src/engram/contracts/graph.js +594 -0
  71. package/src/engram/contracts/identity.js +8 -0
  72. package/src/engram/contracts/index.js +149 -0
  73. package/src/engram/contracts/media-asset.js +85 -0
  74. package/src/engram/contracts/money.js +249 -0
  75. package/src/engram/contracts/source-snapshot.js +57 -0
  76. package/src/engram/contracts/task-context-pack.js +114 -0
  77. package/src/engram/contracts/temporal-segment.js +72 -0
  78. package/src/engram/contracts/versions.js +96 -0
  79. package/src/engram/extract-html.js +692 -0
  80. package/src/engram/graph-store.js +877 -0
  81. package/src/engram/index.js +98 -14
  82. package/src/engram/ingest-site.js +132 -0
  83. package/src/engram/live-state.js +1180 -0
  84. package/src/engram/namespace.js +34 -1
  85. package/src/engram/retrieve-site.js +753 -0
  86. package/src/engram/safe-reader.js +597 -0
  87. package/src/engram/source-intake.js +251 -0
  88. package/src/engram/store.js +227 -9
  89. package/src/ingest/engine.js +21 -28
  90. package/src/judge/verdict.js +305 -0
  91. package/src/legacy-cli.js +96 -19
  92. package/src/mcp/bridge-inputs.js +25 -4
  93. package/src/mcp/cli-command-tools.js +70 -0
  94. package/src/mcp/registry.js +17 -13
  95. package/src/mcp/session-stdio-server.js +297 -19
  96. package/src/memory/blackboard.js +5 -4
  97. package/src/net/gh-api-path.js +35 -9
  98. package/src/net/test-egress-guard.js +16 -4
  99. package/src/plugin/manifest.js +12 -8
  100. package/src/review/ai-review.js +110 -10
  101. package/src/review/investor-dd-devtestbot.js +17 -6
  102. package/src/review/investor-dd-orchestrator.js +21 -13
  103. package/src/review/local-review.js +26 -16
  104. package/src/review/omargate-cache.js +5 -5
  105. package/src/review/omargate-orchestrator.js +89 -17
  106. package/src/review/replay.js +9 -4
  107. package/src/review/report.js +22 -13
  108. package/src/review/typesafe-severity.js +201 -0
  109. package/src/session/admission-auth.js +34 -7
  110. package/src/session/admission.js +110 -15
  111. package/src/session/attachment-summary.js +77 -0
  112. package/src/session/edit-summary.js +19 -0
  113. package/src/session/invitations.js +65 -1
  114. package/src/session/mentions.js +17 -9
  115. package/src/session/message-edits.js +5 -4
  116. package/src/session/preview.js +10 -2
  117. package/src/session/recap.js +3 -2
  118. package/src/session/room-files.js +519 -0
  119. package/src/session/route-classes.js +22 -8
  120. package/src/session/setup-guides.js +4 -0
  121. package/src/session/standup.js +192 -0
  122. package/src/session/sync.js +229 -10
  123. package/src/session/tickets.js +41 -0
  124. package/src/session/transcript.js +2 -1
  125. package/src/session/wake/resolve-target.js +7 -3
  126. package/src/skills/listener-runtime.cjs +92 -0
  127. package/src/skills/service.js +207 -0
  128. package/src/swarm/factory.js +5 -5
  129. package/src/swarm/pentest.js +9 -9
  130. package/src/swarm/report.js +4 -4
  131. package/src/swarm/runtime.js +15 -12
  132. package/src/swarm/scenario-dsl.js +7 -5
  133. package/src/telemetry/ledger.js +16 -7
@@ -0,0 +1,305 @@
1
+ // judge-verdict v1: the document a judging model posts back to a coder.
2
+ //
3
+ // One schema, enforced the same way by this CLI and by the SentinelLayer API (the shared
4
+ // agreement cases live in tests/fixtures/judge-verdict-cases.json). The judge is never named
5
+ // in the document: the server records who judged from the credential or the stored post.
6
+
7
+ export const JUDGE_VERDICT_KIND = "judge-verdict";
8
+ export const JUDGE_VERDICT_VERSION = 1;
9
+ export const JUDGE_VERDICT_MAX_BYTES = 32768;
10
+ export const JUDGE_VERDICT_DEFAULT_MAX_ROUNDS = 3;
11
+ export const JUDGE_VERDICT_SEVERITIES = Object.freeze(["P0", "P1", "P2", "P3"]);
12
+ export const JUDGE_VERDICT_TARGET_TYPES = Object.freeze(["pr", "ticket", "message"]);
13
+
14
+ const TOP_KEYS = new Set([
15
+ "kind", "version", "verdict", "round", "maxRounds", "target", "coder", "summary", "comments",
16
+ "escalate", "doNotTrain",
17
+ ]);
18
+ const TARGET_KEYS = new Set(["type", "ref", "sha"]);
19
+ const COMMENT_KEYS = new Set(["file", "line", "severity", "text", "fix"]);
20
+ const MAX_ROUNDS_LIMIT = 10;
21
+ const MAX_COMMENTS = 50;
22
+ const MAX_LINE = 10_000_000;
23
+ const PR_REF_RE = /^[A-Za-z0-9_.-]{1,100}\/[A-Za-z0-9_.-]{1,100}#[1-9][0-9]{0,9}$/;
24
+ const MESSAGE_REF_RE = /^[1-9][0-9]{0,18}$/;
25
+ const SHA_RE = /^[0-9a-fA-F]{7,64}$/;
26
+ const AGENT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
27
+ // Control characters are U+0000..U+001F and U+007F. Multi-line text may keep \t \n \r.
28
+ const ANY_CONTROL_RE = /[\u0000-\u001f\u007f]/;
29
+ const TEXT_CONTROL_RE = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/;
30
+ const ASCII_EDGE_WHITESPACE_RE = /^[ \t\n\r\f\v]+|[ \t\n\r\f\v]+$/g;
31
+ // The fenced-block scanner works line by line, so its cost is linear in the message whatever the
32
+ // message holds (an unclosed fence included). Each pattern is tested on one line, never the text.
33
+ const FENCE_OPEN_RE = /^```(?:judge-verdict|json)[ \t]*$/;
34
+ const FENCE_CLOSE_RE = /^```[ \t]*$/;
35
+ export const JUDGE_VERDICT_BLOCK_MAX_BYTES = 64 * 1024;
36
+
37
+ function isPlainObject(value) {
38
+ return value !== null && typeof value === "object" && !Array.isArray(value);
39
+ }
40
+
41
+ function trimAscii(value) {
42
+ return value.replace(ASCII_EDGE_WHITESPACE_RE, "");
43
+ }
44
+
45
+ function codePoints(value) {
46
+ return [...value].length;
47
+ }
48
+
49
+ function isWholeNumber(value) {
50
+ return typeof value === "number" && Number.isInteger(value);
51
+ }
52
+
53
+ /**
54
+ * Validate a judge-verdict document.
55
+ *
56
+ * Returns `{ ok: true, verdict }` with the normalized document (strings trimmed, defaults
57
+ * filled, sha lowercased), or `{ ok: false, errors }` where each error is `{ path, message }`.
58
+ */
59
+ export function validateJudgeVerdict(document) {
60
+ const errors = [];
61
+ const fail = (path, message) => errors.push({ path, message });
62
+
63
+ if (!isPlainObject(document)) {
64
+ return { ok: false, errors: [{ path: "", message: "must be a JSON object" }] };
65
+ }
66
+ let serialized;
67
+ try {
68
+ serialized = JSON.stringify(document);
69
+ } catch {
70
+ return { ok: false, errors: [{ path: "", message: "must be serializable JSON" }] };
71
+ }
72
+ if (Buffer.byteLength(serialized, "utf8") > JUDGE_VERDICT_MAX_BYTES) {
73
+ return { ok: false, errors: [{ path: "", message: `must be at most ${JUDGE_VERDICT_MAX_BYTES} bytes as compact JSON` }] };
74
+ }
75
+ for (const key of Object.keys(document)) {
76
+ if (!TOP_KEYS.has(key)) fail(key, "is not a judge-verdict field (the judge is never named in the document)");
77
+ }
78
+
79
+ const text = (path, value, { min, max, multiline = false, required = true }) => {
80
+ if (value === undefined && !required) return undefined;
81
+ if (typeof value !== "string") {
82
+ fail(path, "must be a string");
83
+ return undefined;
84
+ }
85
+ if ((multiline ? TEXT_CONTROL_RE : ANY_CONTROL_RE).test(value)) {
86
+ fail(path, multiline ? "must not contain control characters other than tab and newline" : "must not contain control characters");
87
+ return undefined;
88
+ }
89
+ const trimmed = trimAscii(value);
90
+ const length = codePoints(trimmed);
91
+ if (length < min || length > max) {
92
+ fail(path, min > 0 ? `must be ${min}..${max} characters` : `must be at most ${max} characters`);
93
+ return undefined;
94
+ }
95
+ return trimmed;
96
+ };
97
+ const integer = (path, value, min, max) => {
98
+ if (!isWholeNumber(value) || value < min || value > max) {
99
+ fail(path, `must be an integer ${min}..${max}`);
100
+ return undefined;
101
+ }
102
+ return value;
103
+ };
104
+ const flag = (path, value) => {
105
+ if (value === undefined) return false;
106
+ if (typeof value !== "boolean") {
107
+ fail(path, "must be true or false");
108
+ return false;
109
+ }
110
+ return value;
111
+ };
112
+
113
+ if (document.kind !== JUDGE_VERDICT_KIND) fail("kind", `must be "${JUDGE_VERDICT_KIND}"`);
114
+ if (document.version !== JUDGE_VERDICT_VERSION) fail("version", `must be ${JUDGE_VERDICT_VERSION}`);
115
+ const verdict = document.verdict;
116
+ if (verdict !== "ACCEPT" && verdict !== "CHANGES") fail("verdict", 'must be "ACCEPT" or "CHANGES"');
117
+ const round = integer("round", document.round, 1, MAX_ROUNDS_LIMIT);
118
+ const maxRounds = document.maxRounds === undefined
119
+ ? JUDGE_VERDICT_DEFAULT_MAX_ROUNDS
120
+ : integer("maxRounds", document.maxRounds, 1, MAX_ROUNDS_LIMIT);
121
+ if (round !== undefined && maxRounds !== undefined && round > maxRounds) {
122
+ fail("round", "must not exceed maxRounds");
123
+ }
124
+
125
+ let target;
126
+ if (!isPlainObject(document.target)) {
127
+ fail("target", "must be an object with type and ref");
128
+ } else {
129
+ for (const key of Object.keys(document.target)) {
130
+ if (!TARGET_KEYS.has(key)) fail(`target.${key}`, "is not a target field");
131
+ }
132
+ const type = document.target.type;
133
+ if (!JUDGE_VERDICT_TARGET_TYPES.includes(type)) fail("target.type", 'must be "pr", "ticket" or "message"');
134
+ const ref = text("target.ref", document.target.ref, { min: 1, max: 200 });
135
+ if (ref !== undefined && type === "pr" && !PR_REF_RE.test(ref)) fail("target.ref", "must look like owner/repo#123");
136
+ if (ref !== undefined && type === "message" && !MESSAGE_REF_RE.test(ref)) {
137
+ fail("target.ref", "must be the message's sequence id");
138
+ }
139
+ let sha;
140
+ if (document.target.sha !== undefined) {
141
+ sha = text("target.sha", document.target.sha, { min: 1, max: 64 });
142
+ if (sha !== undefined && !SHA_RE.test(sha)) {
143
+ fail("target.sha", "must be 7..64 hex characters");
144
+ sha = undefined;
145
+ }
146
+ } else if (type === "pr") {
147
+ fail("target.sha", "is required for a pr target (the exact head you judged)");
148
+ }
149
+ target = { type, ref, ...(sha !== undefined ? { sha: sha.toLowerCase() } : {}) };
150
+ }
151
+
152
+ const coder = text("coder", document.coder, { min: 1, max: 64 });
153
+ if (coder !== undefined && (!AGENT_ID_RE.test(coder) || coder.toLowerCase().startsWith("human-"))) {
154
+ fail("coder", "must be the coding agent's id");
155
+ }
156
+ const summary = text("summary", document.summary, { min: 1, max: 2000, multiline: true });
157
+
158
+ const comments = [];
159
+ if (!Array.isArray(document.comments)) {
160
+ fail("comments", "must be an array");
161
+ } else if (document.comments.length > MAX_COMMENTS) {
162
+ fail("comments", `must have at most ${MAX_COMMENTS} entries`);
163
+ } else {
164
+ document.comments.forEach((comment, index) => {
165
+ const at = `comments[${index}]`;
166
+ if (!isPlainObject(comment)) {
167
+ fail(at, "must be an object with file, line, severity and text");
168
+ return;
169
+ }
170
+ for (const key of Object.keys(comment)) {
171
+ if (!COMMENT_KEYS.has(key)) fail(`${at}.${key}`, "is not a comment field");
172
+ }
173
+ const file = text(`${at}.file`, comment.file, { min: 1, max: 300 });
174
+ const line = integer(`${at}.line`, comment.line, 0, MAX_LINE);
175
+ const severity = comment.severity;
176
+ if (!JUDGE_VERDICT_SEVERITIES.includes(severity)) fail(`${at}.severity`, "must be P0, P1, P2 or P3");
177
+ const body = text(`${at}.text`, comment.text, { min: 1, max: 2000, multiline: true });
178
+ const fix = text(`${at}.fix`, comment.fix, { min: 0, max: 500, multiline: true, required: false });
179
+ comments.push({ file, line, severity, text: body, ...(fix !== undefined ? { fix } : {}) });
180
+ });
181
+ }
182
+
183
+ const escalate = flag("escalate", document.escalate);
184
+ const doNotTrain = flag("doNotTrain", document.doNotTrain);
185
+ if (verdict === "CHANGES" && Array.isArray(document.comments) && document.comments.length === 0) {
186
+ fail("comments", "CHANGES needs at least one comment");
187
+ }
188
+ if (verdict === "ACCEPT" && comments.some((comment) => comment.severity === "P0" || comment.severity === "P1")) {
189
+ fail("verdict", "ACCEPT cannot carry a P0 or P1 comment");
190
+ }
191
+ if (escalate && verdict !== "CHANGES") fail("escalate", "only a CHANGES verdict escalates");
192
+ if (verdict === "CHANGES" && round !== undefined && maxRounds !== undefined && round === maxRounds && !escalate) {
193
+ fail("escalate", "the last round's CHANGES verdict must escalate to the human");
194
+ }
195
+
196
+ if (errors.length) return { ok: false, errors };
197
+ return {
198
+ ok: true,
199
+ verdict: {
200
+ kind: JUDGE_VERDICT_KIND,
201
+ version: JUDGE_VERDICT_VERSION,
202
+ verdict,
203
+ round,
204
+ maxRounds,
205
+ target,
206
+ coder,
207
+ summary,
208
+ comments,
209
+ escalate,
210
+ doNotTrain,
211
+ },
212
+ };
213
+ }
214
+
215
+ export function formatJudgeVerdictErrors(errors = []) {
216
+ return errors.map((error) => (error.path ? `${error.path} ${error.message}` : error.message)).join("; ");
217
+ }
218
+
219
+ function targetLabel(target) {
220
+ if (target.type === "pr") return `${target.ref} @ ${target.sha.slice(0, 12)}`;
221
+ if (target.type === "message") return `message #${target.ref}${target.sha ? ` @ ${target.sha.slice(0, 12)}` : ""}`;
222
+ return `ticket ${target.ref}${target.sha ? ` @ ${target.sha.slice(0, 12)}` : ""}`;
223
+ }
224
+
225
+ export function formatJudgeVerdictBlock(verdict) {
226
+ return ["```judge-verdict", JSON.stringify(verdict), "```"].join("\n");
227
+ }
228
+
229
+ /**
230
+ * Render a validated verdict as the room message the coder receives: the mention that wakes
231
+ * the coder, the numbered comments in plain words, and the machine-readable block.
232
+ */
233
+ export function renderJudgeVerdictMessage(verdict, { escalateTo = [] } = {}) {
234
+ const mentions = [`@${verdict.coder}`];
235
+ if (verdict.escalate) {
236
+ for (const handle of escalateTo) mentions.push(`@${handle}`);
237
+ }
238
+ const lines = [
239
+ `${mentions.join(" ")} Judge verdict: ${verdict.verdict} (round ${verdict.round} of ${verdict.maxRounds}) on ${targetLabel(verdict.target)}`,
240
+ verdict.summary,
241
+ ];
242
+ if (verdict.comments.length) {
243
+ lines.push("");
244
+ verdict.comments.forEach((comment, index) => {
245
+ const where = comment.line > 0 ? `${comment.file}:${comment.line}` : comment.file;
246
+ lines.push(`${index + 1}. [${comment.severity}] ${where} - ${comment.text}`);
247
+ if (comment.fix) lines.push(` Fix: ${comment.fix}`);
248
+ });
249
+ }
250
+ lines.push("");
251
+ if (verdict.escalate) {
252
+ lines.push(`Round limit reached. Stopping the loop here; a human decides what happens next.`);
253
+ } else if (verdict.verdict === "CHANGES") {
254
+ lines.push(`Fix each numbered comment, then reply with the new head and what changed per number. I will re-check every one.`);
255
+ } else {
256
+ lines.push(`Accepted. No further rounds.`);
257
+ }
258
+ lines.push("", formatJudgeVerdictBlock(verdict));
259
+ return lines.join("\n");
260
+ }
261
+
262
+ /**
263
+ * Find the judge-verdict block in a room message. Returns `{ status, document }` where status
264
+ * is "none", "one" or "ambiguous" (more than one block: nothing is taken). The document is
265
+ * returned unvalidated; pass it to validateJudgeVerdict.
266
+ *
267
+ * A block opens at a line that is exactly ```judge-verdict or ```json (trailing spaces or tabs
268
+ * allowed) and closes at the next line that is exactly ```. An opener with no closing line ends
269
+ * the scan. A block body over JUDGE_VERDICT_BLOCK_MAX_BYTES (UTF-8) is skipped unparsed, and the
270
+ * scan stops at the second candidate. The API runs the same algorithm against the same vectors
271
+ * (tests/fixtures/judge-verdict-block-cases.json).
272
+ */
273
+ export function extractJudgeVerdictBlock(message) {
274
+ const text = typeof message === "string" ? message : "";
275
+ // Every candidate carries "kind": "judge-verdict", so a message without the word has none.
276
+ if (!text.includes(JUDGE_VERDICT_KIND)) return { status: "none", document: null };
277
+ const lines = text.split("\n");
278
+ const line = (index) => (lines[index].endsWith("\r") ? lines[index].slice(0, -1) : lines[index]);
279
+ let document = null;
280
+ for (let index = 0; index < lines.length; index += 1) {
281
+ if (!FENCE_OPEN_RE.test(line(index))) continue;
282
+ const body = [];
283
+ let bytes = 0;
284
+ let close = index + 1;
285
+ for (; close < lines.length; close += 1) {
286
+ const current = line(close);
287
+ if (FENCE_CLOSE_RE.test(current)) break;
288
+ bytes += Buffer.byteLength(current, "utf8") + (close > index + 1 ? 1 : 0);
289
+ if (bytes <= JUDGE_VERDICT_BLOCK_MAX_BYTES) body.push(current);
290
+ }
291
+ if (close >= lines.length) break;
292
+ index = close;
293
+ if (bytes > JUDGE_VERDICT_BLOCK_MAX_BYTES) continue;
294
+ let parsed;
295
+ try {
296
+ parsed = JSON.parse(body.join("\n"));
297
+ } catch {
298
+ continue;
299
+ }
300
+ if (!isPlainObject(parsed) || parsed.kind !== JUDGE_VERDICT_KIND) continue;
301
+ if (document) return { status: "ambiguous", document: null };
302
+ document = parsed;
303
+ }
304
+ return document ? { status: "one", document } : { status: "none", document: null };
305
+ }
package/src/legacy-cli.js CHANGED
@@ -23,12 +23,12 @@ import {
23
23
  resolveTrustContext,
24
24
  userCredential,
25
25
  } from "./auth/credential-destinations.js";
26
- import { resolveOutputRoot } from "./config/service.js";
26
+ import { resolveOutputContext } from "./config/service.js";
27
27
  import { normalizeAgentEvent } from "./events/schema.js";
28
28
  import { collectCodebaseIngest, formatIngestSummary } from "./ingest/engine.js";
29
29
  import { getExpressTemplate, getPackageJsonTemplate, buildReadmeContent } from "./scaffold/templates.js";
30
30
  import { generateScaffold } from "./scaffold/generator.js";
31
- import { isGhRepoSlug } from "./net/gh-api-path.js";
31
+ import { ghRepoApiPath, isGhRepoSlug } from "./net/gh-api-path.js";
32
32
  import {
33
33
  getCoordinationEtiquetteItems,
34
34
  renderCoordinationNumberedList,
@@ -42,6 +42,7 @@ let DEFAULT_GITHUB_CLONE_BASE_URL =
42
42
  const DEFAULT_AUTH_TIMEOUT_MS = 10 * 60 * 1000;
43
43
  const DEFAULT_REQUEST_TIMEOUT_MS = 20_000;
44
44
  const PACKAGE_JSON_PATH = new URL("../package.json", import.meta.url);
45
+ const GH_REPOS_ENDPOINT = "/user/repos?per_page=100&sort=updated&affiliation=owner,collaborator,organization_member";
45
46
 
46
47
  function refreshRuntimeDefaults() {
47
48
  DEFAULT_API_URL = process.env.SENTINELAYER_API_URL || "https://api.sentinelayer.com";
@@ -232,7 +233,8 @@ function printUsage() {
232
233
  console.log(" sl session start --json Create an agent coordination session");
233
234
  console.log(" sl session start --template code-review Start from quick-start preset + launch plan");
234
235
  console.log(" sl session templates --json List available session quick-start templates");
235
- console.log(" sl session join <id> --name <n> Join a session as an agent");
236
+ console.log(" sl session join <id> --agent <agent> --goal \"<goal>\" Request owner-approved agent admission");
237
+ console.log(" sl session access --help Request and govern scoped agent access");
236
238
  console.log(" sl session say <id> \"msg\" --json Append a message event to session stream");
237
239
  console.log(" sl session lock <id> <file> --agent <agent> --intent <text> Claim an API-backed file lease outside chat");
238
240
  console.log(" sl session guard-install <id> --agent <agent> Install terminal/editor lease preflights");
@@ -248,6 +250,7 @@ function printUsage() {
248
250
  console.log(" sl session comment <id> <seq> \"msg\" Alias for threaded reply");
249
251
  console.log(" sl session read <id> --remote --agent <id> Read events and advance delivery cursor");
250
252
  console.log(" sl session edit <id> <seq|reply-uuid> \"msg\" Edit your message without losing history");
253
+ console.log(" sl session observe <id> \"summary\" --agent <agent> Record product/process feedback");
251
254
  console.log(" sl session pins <id> --json List pinned messages with content (readable by agents)");
252
255
  console.log(" sl session lock <id> <files...> --intent <why> Claim file locks (fail-closed, TTL auto-release)");
253
256
  console.log(" sl session unlock <id> <files...> Release file locks you hold");
@@ -256,7 +259,10 @@ function printUsage() {
256
259
  console.log(" sl session recap now <id> --remote --json Summarize current owners, locks, and work");
257
260
  console.log(" sl session daemon --session <id> Run Senti recaps/checkpoints for long rooms");
258
261
  console.log(" sl session read <id> --tail 20 Read local session stream events");
259
- console.log(" sl session status <id> --json Show session health, agents, runs, leases");
262
+ console.log(" sl session status <id> --json Show workspace status and file-lock health");
263
+ console.log(" sl session ticket --help List, claim, renew, release, submit tickets and read events");
264
+ console.log(" sl session ticket list <id> --available --agent <agent> Find currently claimable tickets");
265
+ console.log(" sl session checkpoint --help List, show, create and generate durable checkpoints");
260
266
  console.log(" sl session leave <id> Leave a session");
261
267
  console.log(" sl session list --json List active sessions");
262
268
  console.log(" sl session setup-guides <id> --json Upsert AGENTS.md/CLAUDE.md coordination section");
@@ -321,7 +327,7 @@ function printUsage() {
321
327
  console.log(" sl mcp registry init-session --force && sl mcp server run --path .");
322
328
  console.log(" sl mcp registry init-cli --json");
323
329
  console.log("");
324
- console.log("Docs: https://github.com/mrrCarter/create-sentinelayer#readme");
330
+ console.log("Docs: https://github.com/Sentinelayer-AIdenID/create-sentinelayer#readme");
325
331
  }
326
332
 
327
333
  function normalizeInterviewInput(
@@ -597,7 +603,7 @@ async function issueBootstrapToken({ apiUrl, credential }) {
597
603
  }
598
604
 
599
605
  // The init flow's credential steps, for tests.
600
- export const __legacyCredentialFlowForTests = Object.freeze({ pollCliSession, generateArtifacts, issueBootstrapToken, requestJson });
606
+ export const __initCallsForTests = Object.freeze({ pollCliSession, generateArtifacts, issueBootstrapToken, requestJson });
601
607
 
602
608
  function detectRepoSlug(cwd) {
603
609
  const gitRemote = spawnSync("git", ["config", "--get", "remote.origin.url"], {
@@ -675,14 +681,13 @@ function ensureGhAuthSession(ghCommand) {
675
681
  }
676
682
 
677
683
  function listReposViaGh(ghCommand) {
678
- const endpoint = "/user/repos?per_page=100&sort=updated&affiliation=owner,collaborator,organization_member";
679
684
  const apiResult = spawnSync(
680
685
  ghCommand,
681
- ["api", "--paginate", "--slurp", endpoint],
686
+ ["api", "--paginate", "--slurp", GH_REPOS_ENDPOINT],
682
687
  { encoding: "utf-8" }
683
688
  );
684
689
  if (apiResult.status !== 0) {
685
- const fallback = spawnSync(ghCommand, ["api", endpoint], { encoding: "utf-8" });
690
+ const fallback = spawnSync(ghCommand, ["api", GH_REPOS_ENDPOINT], { encoding: "utf-8" });
686
691
  if (fallback.status !== 0) {
687
692
  throw new Error(
688
693
  String(
@@ -1059,14 +1064,14 @@ async function runCredentialScan(targetPath) {
1059
1064
  }
1060
1065
 
1061
1066
  async function writeLocalCommandReport(targetPath, prefix, body, { outputDir = "" } = {}) {
1062
- const outputRoot = await resolveOutputRoot({
1067
+ const { outputRoot, outputFs } = await resolveOutputContext({
1063
1068
  cwd: targetPath,
1064
1069
  outputDirOverride: outputDir,
1065
1070
  });
1066
1071
  const reportDir = path.join(outputRoot, "reports");
1067
- await ensureDirectory(reportDir);
1072
+ await outputFs.mkdir(reportDir, { recursive: true });
1068
1073
  const reportPath = path.join(reportDir, `${prefix}-${formatTimestampForFile()}.md`);
1069
- await writeTextFile(reportPath, `${body}\n`);
1074
+ await outputFs.writeFile(reportPath, `${body}\n`, "utf-8");
1070
1075
  return reportPath;
1071
1076
  }
1072
1077
 
@@ -1348,7 +1353,12 @@ async function runLocalOmarGateCommand(args) {
1348
1353
  const outputDirArg = getCommandOptionValue(args, "--output-dir") || "";
1349
1354
  const aiEnabled = !hasCommandOption(args, "--no-ai");
1350
1355
  const aiDryRun = hasCommandOption(args, "--ai-dry-run");
1351
- const maxCostUsd = parseFloat(getCommandOptionValue(args, "--max-cost") || "5.0") || 5.0;
1356
+ const { normalizeSeverityClassifier, parseSeverityEvaluationMaxCost } = await import("./review/typesafe-severity.js");
1357
+ const severityClassifier = normalizeSeverityClassifier(getCommandOptionValue(args, "--severity-classifier") || "off");
1358
+ const maxCostFlag = getCommandOptionValue(args, "--max-cost");
1359
+ const maxCostUsd = severityClassifier === "typesafe"
1360
+ ? parseSeverityEvaluationMaxCost(maxCostFlag, 5)
1361
+ : parseFloat(maxCostFlag || "5.0") || 5.0;
1352
1362
  const modelOverride = getCommandOptionValue(args, "--model") || "";
1353
1363
  const providerOverride = getCommandOptionValue(args, "--provider") || "";
1354
1364
  const notifySessionFlag = getCommandOptionValue(args, "--notify-session") || "";
@@ -1444,6 +1454,7 @@ async function runLocalOmarGateCommand(args) {
1444
1454
  provider: providerOverride || undefined,
1445
1455
  model: modelOverride || undefined,
1446
1456
  maxCostUsd,
1457
+ ...(severityClassifier === "typesafe" ? { severityClassifier } : {}),
1447
1458
  dryRun: aiDryRun,
1448
1459
  outputDir: outputDirArg,
1449
1460
  deterministic: {
@@ -1479,6 +1490,7 @@ async function runLocalOmarGateCommand(args) {
1479
1490
  summary: aiOnlySummary,
1480
1491
  costUsd: orchestratorResult.totalCostUsd || 0,
1481
1492
  model: modelOverride || "multi-persona",
1493
+ ...(severityClassifier === "typesafe" ? { severityClassification: orchestratorResult.severityClassification } : {}),
1482
1494
  provider: providerOverride || "sentinelayer",
1483
1495
  dryRun: aiDryRun,
1484
1496
  personas: (orchestratorResult.personas || []).map((p) => ({
@@ -1520,6 +1532,7 @@ async function runLocalOmarGateCommand(args) {
1520
1532
  model: modelOverride || undefined,
1521
1533
  sessionId: notifySession,
1522
1534
  maxCostUsd,
1535
+ ...(severityClassifier === "typesafe" ? { severityClassifier } : {}),
1523
1536
  dryRun: aiDryRun,
1524
1537
  requireUsageLedger,
1525
1538
  sourceCommand: "omargate deep",
@@ -1543,17 +1556,26 @@ async function runLocalOmarGateCommand(args) {
1543
1556
  // via reconcileReviewFindings. Use its output directly to avoid double-counting.
1544
1557
  // Fallback path (legacy single ai-review): union of det + AI findings.
1545
1558
  const aiFindings = aiResult?.findings || [];
1559
+ const aiCoverageFailed = aiEnabled && aiResult?.skipped === true;
1560
+ const coverage = orchestratorResult?.coverage || (aiCoverageFailed ? { status: "incomplete" } : null);
1561
+ const coverageFailureFindings = aiCoverageFailed ? [{
1562
+ severity: "P1", file: "<omargate>", line: 1,
1563
+ message: "Omar AI coverage incomplete: the requested AI review did not complete.",
1564
+ category: "Omar AI Coverage", source: "orchestrator", sources: ["orchestrator"],
1565
+ confidence: 1,
1566
+ remediation: "Resolve the reported AI review failure and rerun the requested review. Partial coverage cannot satisfy a full AI review gate.",
1567
+ }] : [];
1546
1568
  const reconciledFromOrchestrator = orchestratorResult?.findings;
1547
1569
  const allFindings = reconciledFromOrchestrator && reconciledFromOrchestrator.length >= 0
1548
1570
  ? reconciledFromOrchestrator
1549
- : [...detFindings, ...aiFindings];
1571
+ : [...detFindings, ...aiFindings, ...coverageFailureFindings];
1550
1572
  const combinedSummary = orchestratorResult?.summary || {
1551
1573
  P0: detSummary.P0 + (aiResult?.summary?.P0 || 0),
1552
- P1: detSummary.P1 + (aiResult?.summary?.P1 || 0),
1574
+ P1: detSummary.P1 + (aiResult?.summary?.P1 || 0) + coverageFailureFindings.length,
1553
1575
  P2: detSummary.P2 + (aiResult?.summary?.P2 || 0),
1554
1576
  P3: (detSummary.P3 || 0) + (aiResult?.summary?.P3 || 0),
1555
1577
  blocking: (detSummary.P0 + (aiResult?.summary?.P0 || 0)) > 0 ||
1556
- (detSummary.P1 + (aiResult?.summary?.P1 || 0)) > 0,
1578
+ (detSummary.P1 + (aiResult?.summary?.P1 || 0) + coverageFailureFindings.length) > 0,
1557
1579
  };
1558
1580
  const combinedP0 = combinedSummary.P0 || 0;
1559
1581
  const combinedP1 = combinedSummary.P1 || 0;
@@ -1582,6 +1604,7 @@ async function runLocalOmarGateCommand(args) {
1582
1604
  roster: orchestratorResult.roster,
1583
1605
  basePersonas: orchestratorResult.basePersonas || [],
1584
1606
  personaRouting: orchestratorResult.personaRouting || null,
1607
+ coverage,
1585
1608
  findings: (orchestratorResult.personas || []).flatMap((p) => []),
1586
1609
  aiFindings: aiFindings,
1587
1610
  personaCount: (orchestratorResult.personas || []).length,
@@ -1591,6 +1614,7 @@ async function runLocalOmarGateCommand(args) {
1591
1614
  artifactPaths.personas = await writeJsonArtifact("REVIEW_PERSONAS.json", {
1592
1615
  runId: orchestratorResult.runId,
1593
1616
  personas: orchestratorResult.personas || [],
1617
+ coverage,
1594
1618
  });
1595
1619
  artifactPaths.reconciled = await writeJsonArtifact("REVIEW_RECONCILED.json", {
1596
1620
  runId: orchestratorResult.runId,
@@ -1599,6 +1623,7 @@ async function runLocalOmarGateCommand(args) {
1599
1623
  reconciliation: orchestratorResult.reconciliation,
1600
1624
  personaRouting: orchestratorResult.personaRouting || null,
1601
1625
  findingsBySource: orchestratorResult.findingsBySource,
1626
+ coverage,
1602
1627
  });
1603
1628
  }
1604
1629
 
@@ -1616,6 +1641,7 @@ Summary:
1616
1641
  - Files scanned: ${scannedFiles}
1617
1642
  - Scope mode: ${scopeMode}
1618
1643
  - Persona routing: ${orchestratorResult?.personaRouting?.enabled ? `${orchestratorResult.personaRouting.effectivePersonas.length}/${orchestratorResult.personaRouting.basePersonas.length} personas for ${orchestratorResult.personaRouting.changedFileCount} changed files` : "full roster"}
1644
+ - AI coverage: ${coverage?.status || (aiEnabled ? "unknown" : "not requested")}
1619
1645
  - Deterministic findings: P0=${detSummary.P0} P1=${detSummary.P1} P2=${detSummary.P2} P3=${detSummary.P3 || 0}
1620
1646
  - AI findings (raw, pre-reconciliation): ${aiResult ? `P0=${aiResult.summary?.P0 || 0} P1=${aiResult.summary?.P1 || 0} P2=${aiResult.summary?.P2 || 0} P3=${aiResult.summary?.P3 || 0}` : "skipped"}
1621
1647
  - Reconciled (deduped + confidence-boosted): P0=${combinedP0} P1=${combinedP1} P2=${combinedP2} P3=${combinedP3}
@@ -1657,6 +1683,7 @@ ${formatFindingsMarkdown(allFindings)}
1657
1683
  p2: combinedP2,
1658
1684
  p3: combinedP3,
1659
1685
  blocking: combinedP0 > 0 || combinedP1 > 0,
1686
+ coverage,
1660
1687
  elapsedMs: totalElapsedMs,
1661
1688
  artifacts: artifactPaths,
1662
1689
  deterministic: {
@@ -2134,6 +2161,55 @@ async function ensureEnvFileIgnored(projectDir) {
2134
2161
  await writeTextFile(gitignorePath, next);
2135
2162
  }
2136
2163
 
2164
+ export async function ensureSentinelConfigIgnored(projectDir) {
2165
+ const gitignorePath = path.join(projectDir, ".gitignore");
2166
+ let existing = "";
2167
+ if (fs.existsSync(gitignorePath)) {
2168
+ existing = await fsp.readFile(gitignorePath, "utf-8");
2169
+ }
2170
+
2171
+ const entry = ".sentinelayer/";
2172
+ const lines = existing.split(/\r?\n/);
2173
+ const hasEntry = lines.some((line) => {
2174
+ const normalized = String(line || "").trim();
2175
+ return normalized === entry || normalized === ".sentinelayer";
2176
+ });
2177
+ if (hasEntry) {
2178
+ return;
2179
+ }
2180
+
2181
+ let next = "";
2182
+ if (existing.trim().length === 0) {
2183
+ next = `${entry}\n`;
2184
+ } else if (existing.endsWith("\n")) {
2185
+ next = `${existing}${entry}\n`;
2186
+ } else {
2187
+ next = `${existing}\n${entry}\n`;
2188
+ }
2189
+ await writeTextFile(gitignorePath, next);
2190
+ }
2191
+
2192
+ export async function warnIfSentinelConfigTracked(projectDir) {
2193
+ try {
2194
+ const configRelPath = ".sentinelayer/config.json";
2195
+ const result = spawnSync("git", ["-C", projectDir, "ls-files", "--error-unmatch", configRelPath], {
2196
+ encoding: "utf-8",
2197
+ stdio: ["ignore", "pipe", "pipe"],
2198
+ });
2199
+ if (result.status === 0) {
2200
+ console.log(
2201
+ pc.yellow(
2202
+ `! ${configRelPath} is tracked by git and may contain a token from a previous init. ` +
2203
+ `To remove it: git rm --cached ${configRelPath} && git commit -m "Remove tracked SentinelLayer config". ` +
2204
+ `Then rotate your token at https://sentinelayer.com/dashboard.`
2205
+ )
2206
+ );
2207
+ }
2208
+ } catch {
2209
+ // git may not be installed or projectDir may not be a repo; silently skip.
2210
+ }
2211
+ }
2212
+
2137
2213
  async function ensureSentinelStartScript(projectDir, projectName) {
2138
2214
  const packagePath = path.join(projectDir, "package.json");
2139
2215
  const fallback = {
@@ -2789,7 +2865,7 @@ function confirmSecretsRepository(repoSlug) {
2789
2865
  } catch (error) {
2790
2866
  return { ok: false, reason: error instanceof Error ? error.message : String(error) };
2791
2867
  }
2792
- const view = spawnSync(ghCommand, ["api", `repos/${repoSlug}`], { encoding: "utf-8" });
2868
+ const view = spawnSync(ghCommand, ["api", ghRepoApiPath(repoSlug)], { encoding: "utf-8" });
2793
2869
  if (view.status !== 0) {
2794
2870
  return { ok: false, reason: `gh could not read ${repoSlug}: ${String(view.stderr || view.stdout || "").trim()}` };
2795
2871
  }
@@ -2895,7 +2971,7 @@ function resolveGeneratedSpecId(generated) {
2895
2971
  return "";
2896
2972
  }
2897
2973
 
2898
- async function writeInitConfigLockfile({
2974
+ export async function writeInitConfigLockfile({
2899
2975
  projectDir,
2900
2976
  specId,
2901
2977
  sentinelayerToken,
@@ -2910,7 +2986,6 @@ async function writeInitConfigLockfile({
2910
2986
  schema_version: 1,
2911
2987
  generated_at: new Date().toISOString(),
2912
2988
  spec_id: String(specId || "").trim(),
2913
- sentinelayer_token: String(sentinelayerToken || "").trim(),
2914
2989
  required_secret_name: String(secretName || "SENTINELAYER_TOKEN").trim() || "SENTINELAYER_TOKEN",
2915
2990
  repo_slug: normalizeRepoSlug(repoSlug || ""),
2916
2991
  workflow_path: path.relative(projectDir, workflowPath).replace(/\\/g, "/"),
@@ -3602,8 +3677,10 @@ export async function runLegacyCli(rawArgs = process.argv.slice(2)) {
3602
3677
 
3603
3678
  if (sentinelayerToken) {
3604
3679
  await ensureEnvFileIgnored(projectDir);
3680
+ await ensureSentinelConfigIgnored(projectDir);
3605
3681
  await upsertEnvVariable(path.join(projectDir, ".env"), secretName, sentinelayerToken);
3606
3682
  }
3683
+ await warnIfSentinelConfigTracked(projectDir);
3607
3684
  await ensureGitRepositorySetup({
3608
3685
  projectDir,
3609
3686
  repoSlug: interview.connectRepo ? interview.repoSlug : "",