sentinelayer-cli 0.32.1 → 0.32.3

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/README.md CHANGED
@@ -929,6 +929,18 @@ Ledger contract:
929
929
  - network access to Sentinelayer API/web
930
930
  - optional: GitHub CLI (`gh`) authenticated for secret injection
931
931
 
932
+ ## Operator and architecture references
933
+
934
+ - First-run engineering onboarding: `docs/ENGINEERING_ONBOARDING.md`
935
+ - Senti session operations, coordination, kill, and rollback conventions: `docs/sessions.md`
936
+ - Release workflow map and rollback drill ownership: `docs/workflow-matrix.md`
937
+ - Release rollback execution: `.github/workflows/rollback.yml` and `.github/scripts/release-rollback-readiness.sh`
938
+ - Architecture and decision records for major surfaces:
939
+ - Hosted MCP connector boundary: `docs/mcp-hosted-connector.md`
940
+ - MCP local/runtime entry points: `docs/mcp.md`
941
+ - Investor-DD architecture: `docs/INVESTOR_DD_ARCHITECTURE.md`
942
+ - CI/CD acknowledged release risks: `.github/policies/cicd-acknowledged-risks.md`
943
+
932
944
  ## Release to npm
933
945
 
934
946
  This repo includes `.github/workflows/release.yml`.
@@ -949,14 +961,15 @@ Release flow:
949
961
 
950
962
  1. Merge to `main` and let `Release Please` open/update the release PR.
951
963
  2. Merge the release PR after Omar, Quality Gates, and Attestation are green.
952
- 3. From a clean checkout of the release commit, run the guarded release command:
964
+ 3. Wait for the main-branch `Release Please` workflow run on that merge commit to complete successfully.
965
+ 4. From a clean checkout of the release commit, run the guarded release command:
953
966
  ```bash
954
967
  git fetch origin main --tags
955
968
  git checkout main && git pull --ff-only
956
969
  npm run release:publish -- --tag v0.1.1 --notes-file release-notes.md
957
970
  ```
958
- 4. The guarded command creates the tag with `git tag -s`, verifies it with `git tag -v`, pushes it, confirms the remote GitHub tag object is annotated and cryptographically verified by an allowlisted signer, then creates the GitHub release with `gh release create --verify-tag`.
959
- 5. The tag push triggers `Release`, which re-verifies upstream checks, release artifact attestations, rollback readiness, protected `package-release` approval, and npm trusted publishing OIDC before publishing.
971
+ 5. The guarded command confirms the exact release commit has one successful main `Release Please` run, creates the tag with `git tag -s`, verifies it with `git tag -v`, pushes it, confirms the remote GitHub tag object is annotated and cryptographically verified by an allowlisted signer, then creates the GitHub release with `gh release create --verify-tag`.
972
+ 6. The tag push triggers `Release`, which re-verifies upstream checks, release artifact attestations, rollback readiness, protected `package-release` approval, and npm trusted publishing OIDC before publishing.
960
973
 
961
974
  Do not run `gh release create v0.1.1` before pushing the signed tag. GitHub release creation creates a lightweight tag when the tag does not already exist, and the Release workflow intentionally fails closed for lightweight tags. If a remote tag already exists as a lightweight tag, `npm run release:publish` refuses to create the GitHub release.
962
975
 
@@ -964,6 +977,7 @@ Release publish now enforces tarball checksum-manifest validation and attestatio
964
977
 
965
978
  Release guardrails now require successful upstream checks on the target commit:
966
979
 
980
+ - `Release Please`
967
981
  - `Quality Summary`
968
982
  - `Omar Gate`
969
983
  - `Attestation Summary`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.32.1",
3
+ "version": "0.32.3",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -20,7 +20,7 @@ export function registerOmarGateCommand(program, invokeLegacy) {
20
20
  .option("--model <id>", "LLM model override (default: gpt-5.3-codex)")
21
21
  .option("--provider <name>", "LLM provider: sentinelayer, openai, anthropic")
22
22
  .option("--max-cost <usd>", "Maximum AI layer cost in USD (default: 5.0)")
23
- .option("--notify-session <session-id>", "Write Omar Deep billing-grade usage entries into this Senti session")
23
+ .option("--notify-session <session-id>", "Write Omar Deep billing-grade usage entries into this Senti session (default: auto-detect active Senti session)")
24
24
  .option("--require-usage-ledger", "Fail if Omar Deep AI calls cannot write billing-grade session_usage")
25
25
  .option("--persona <csv>", "Only run these personas (comma-separated IDs); unknown IDs are dropped + warned")
26
26
  .option("--skip-persona <csv>", "Skip these personas (comma-separated IDs)")
@@ -54,7 +54,7 @@ export function registerOmarGateCommand(program, invokeLegacy) {
54
54
  .option("--stream", "Emit NDJSON events to stdout as personas work file-by-file")
55
55
  .option("--notify-email <addr>", "Send final report to this email (default: account email)")
56
56
  .option("--email-on-complete <addr>", "Trigger the API-side DD report email after the run completes")
57
- .option("--notify-session <session-id>", "Stream progress into this Senti session (default: auto-start)")
57
+ .option("--notify-session <session-id>", "Stream progress + billing-grade usage into this Senti session (default: auto-detect active Senti session)")
58
58
  .option("--require-usage-ledger", "Fail if Investor-DD LLM planner calls cannot write session_usage")
59
59
  .option("--no-email", "Skip email dispatch")
60
60
  .option("--no-dashboard", "Skip dashboard card persistence")
package/src/legacy-cli.js CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  getCoordinationEtiquetteItems,
30
30
  renderCoordinationNumberedList,
31
31
  } from "./session/coordination-guidance.js";
32
+ import { resolveUsageSessionId } from "./session/usage-session-resolver.js";
32
33
 
33
34
  let DEFAULT_API_URL = process.env.SENTINELAYER_API_URL || "https://api.sentinelayer.com";
34
35
  let DEFAULT_WEB_URL = process.env.SENTINELAYER_WEB_URL || "https://sentinelayer.com";
@@ -1200,7 +1201,7 @@ async function runLocalOmarGateCommand(args) {
1200
1201
  const devTestBotBaseUrl = getCommandOptionValue(args, "--devtestbot-base-url") || "";
1201
1202
  const devTestBotScope = getCommandOptionValue(args, "--devtestbot-scope") || "";
1202
1203
  const emailOnComplete = getCommandOptionValue(args, "--email-on-complete") || "";
1203
- const notifySession = getCommandOptionValue(args, "--notify-session") || "";
1204
+ const notifySessionFlag = getCommandOptionValue(args, "--notify-session") || "";
1204
1205
  const requireUsageLedger = hasCommandOption(args, "--require-usage-ledger");
1205
1206
  const model = getCommandOptionValue(args, "--model") || "gpt-5.3-codex";
1206
1207
  const provider = getCommandOptionValue(args, "--provider") || "sentinelayer";
@@ -1211,6 +1212,12 @@ async function runLocalOmarGateCommand(args) {
1211
1212
  if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
1212
1213
  throw new Error(`Invalid --path target: ${targetPath}`);
1213
1214
  }
1215
+ const usageSession = await resolveUsageSessionId({
1216
+ explicitSessionId: notifySessionFlag,
1217
+ targetPath,
1218
+ env: process.env,
1219
+ });
1220
+ const notifySession = usageSession.sessionId || "";
1214
1221
 
1215
1222
  const { runInvestorDd } = await import("./review/investor-dd-orchestrator.js");
1216
1223
  const { parsePersonaCsv, resolveFilteredPersonas } = await import("./review/scan-modes.js");
@@ -1235,6 +1242,15 @@ async function runLocalOmarGateCommand(args) {
1235
1242
  printInfo(`Skipped personas: ${personaFilter.dropped.join(", ")}`);
1236
1243
  }
1237
1244
  if (dryRun) printInfo("Mode: dry-run (plan + stub report only)");
1245
+ if (notifySession) {
1246
+ printInfo(
1247
+ `Usage ledger: ${usageSession.source === "explicit" ? "using" : "auto-detected"} Senti session ${notifySession}`,
1248
+ );
1249
+ } else if (requireUsageLedger) {
1250
+ printInfo(
1251
+ `Usage ledger: required but no active Senti session was auto-detected (${usageSession.reason || "missing_session"}).`,
1252
+ );
1253
+ }
1238
1254
  }
1239
1255
  const result = await runInvestorDd({
1240
1256
  rootPath: targetPath,
@@ -1298,7 +1314,7 @@ async function runLocalOmarGateCommand(args) {
1298
1314
  const maxCostUsd = parseFloat(getCommandOptionValue(args, "--max-cost") || "5.0") || 5.0;
1299
1315
  const modelOverride = getCommandOptionValue(args, "--model") || "";
1300
1316
  const providerOverride = getCommandOptionValue(args, "--provider") || "";
1301
- const notifySession = getCommandOptionValue(args, "--notify-session") || "";
1317
+ const notifySessionFlag = getCommandOptionValue(args, "--notify-session") || "";
1302
1318
  const requireUsageLedger = hasCommandOption(args, "--require-usage-ledger");
1303
1319
  const scanMode = getCommandOptionValue(args, "--scan-mode") || "deep";
1304
1320
  const maxParallel = parseInt(getCommandOptionValue(args, "--max-parallel") || "4", 10) || 4;
@@ -1315,6 +1331,12 @@ async function runLocalOmarGateCommand(args) {
1315
1331
  if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
1316
1332
  throw new Error(`Invalid --path target: ${targetPath}`);
1317
1333
  }
1334
+ const usageSession = await resolveUsageSessionId({
1335
+ explicitSessionId: notifySessionFlag,
1336
+ targetPath,
1337
+ env: process.env,
1338
+ });
1339
+ const notifySession = usageSession.sessionId || "";
1318
1340
  // Default scope to changed-files when the target has a git delta (uncommitted
1319
1341
  // or staged), matching gh's PR-scoped Omar; fall back to full repo otherwise.
1320
1342
  // Explicit --scope-mode/--diff/--staged always override (see resolveScopeModeFromOptions).
@@ -1327,6 +1349,15 @@ async function runLocalOmarGateCommand(args) {
1327
1349
  printInfo(`Target: ${targetPath}`);
1328
1350
  printInfo(`Scope mode: ${scopeMode}`);
1329
1351
  printInfo(`Scan mode: ${scanMode} | AI: ${aiEnabled ? "enabled" : "disabled"}`);
1352
+ if (notifySession) {
1353
+ printInfo(
1354
+ `Usage ledger: ${usageSession.source === "explicit" ? "using" : "auto-detected"} Senti session ${notifySession}`,
1355
+ );
1356
+ } else if (requireUsageLedger) {
1357
+ printInfo(
1358
+ `Usage ledger: required but no active Senti session was auto-detected (${usageSession.reason || "missing_session"}).`,
1359
+ );
1360
+ }
1330
1361
  console.error("");
1331
1362
  console.error(pc.gray(` [${formatElapsed(Date.now() - commandStartedAt)}] Phase 1: Deterministic analysis (22 rules)...`));
1332
1363
  }
@@ -0,0 +1,132 @@
1
+ import process from "node:process";
2
+
3
+ import { listActiveSessions } from "./store.js";
4
+
5
+ const USAGE_SESSION_ENV_KEYS = [
6
+ "SENTINELAYER_NOTIFY_SESSION",
7
+ "SENTINELAYER_SESSION_ID",
8
+ "SL_SESSION_ID",
9
+ ];
10
+
11
+ function normalizeString(value) {
12
+ return String(value || "").trim();
13
+ }
14
+
15
+ function isSafeUsageSessionId(value) {
16
+ const normalized = normalizeString(value);
17
+ if (!/^[A-Za-z0-9._-]{1,200}$/.test(normalized)) return false;
18
+ if (normalized === "." || normalized === "..") return false;
19
+ if (normalized.includes("..")) return false;
20
+ return true;
21
+ }
22
+
23
+ function normalizeTrustedUsageSessionId(value, source) {
24
+ const normalized = normalizeString(value);
25
+ if (!normalized) return "";
26
+ if (!isSafeUsageSessionId(normalized)) {
27
+ throw new Error(
28
+ `Invalid ${source} usage session id: must be a single session identifier without path traversal, separators, or control characters.`,
29
+ );
30
+ }
31
+ return normalized;
32
+ }
33
+
34
+ function parseEpoch(value) {
35
+ const epoch = Date.parse(normalizeString(value));
36
+ return Number.isFinite(epoch) ? epoch : 0;
37
+ }
38
+
39
+ function sessionRecencyEpoch(session = {}) {
40
+ return Math.max(
41
+ parseEpoch(session.lastInteractionAt),
42
+ parseEpoch(session.updatedAt),
43
+ parseEpoch(session.createdAt),
44
+ );
45
+ }
46
+
47
+ function resolveEnvUsageSession(env = process.env) {
48
+ for (const key of USAGE_SESSION_ENV_KEYS) {
49
+ const sessionId = normalizeTrustedUsageSessionId(env?.[key], key);
50
+ if (sessionId) {
51
+ return { sessionId, source: "env", envKey: key };
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+
57
+ export function selectMostRecentUsageSession(sessions = []) {
58
+ const candidatesBySessionId = new Map();
59
+ for (const session of Array.isArray(sessions) ? sessions : []) {
60
+ const sessionId = normalizeString(session?.sessionId);
61
+ if (!isSafeUsageSessionId(sessionId)) continue;
62
+
63
+ const candidate = {
64
+ sessionId,
65
+ title: normalizeString(session?.title),
66
+ recencyEpoch: sessionRecencyEpoch(session),
67
+ };
68
+ const existing = candidatesBySessionId.get(sessionId);
69
+ if (!existing || candidate.recencyEpoch > existing.recencyEpoch) {
70
+ candidatesBySessionId.set(sessionId, candidate);
71
+ }
72
+ }
73
+
74
+ const candidates = Array.from(candidatesBySessionId.values());
75
+
76
+ if (candidates.length === 0) {
77
+ return { sessionId: "", source: "none", reason: "no_active_session" };
78
+ }
79
+
80
+ candidates.sort((left, right) => {
81
+ if (right.recencyEpoch !== left.recencyEpoch) {
82
+ return right.recencyEpoch - left.recencyEpoch;
83
+ }
84
+ return left.sessionId.localeCompare(right.sessionId);
85
+ });
86
+
87
+ const [winner, runnerUp] = candidates;
88
+ if (runnerUp && runnerUp.recencyEpoch === winner.recencyEpoch) {
89
+ return {
90
+ sessionId: "",
91
+ source: "ambiguous",
92
+ reason: "multiple_active_sessions_same_recency",
93
+ candidates: candidates.map((candidate) => candidate.sessionId),
94
+ };
95
+ }
96
+
97
+ return {
98
+ sessionId: winner.sessionId,
99
+ source: "local_active_session",
100
+ title: winner.title || null,
101
+ candidates: candidates.map((candidate) => candidate.sessionId),
102
+ };
103
+ }
104
+
105
+ export async function resolveUsageSessionId({
106
+ explicitSessionId = "",
107
+ targetPath = process.cwd(),
108
+ env = process.env,
109
+ listActiveSessionsFn = listActiveSessions,
110
+ } = {}) {
111
+ const explicit = normalizeTrustedUsageSessionId(explicitSessionId, "--notify-session");
112
+ if (explicit) {
113
+ return { sessionId: explicit, source: "explicit" };
114
+ }
115
+
116
+ const envSession = resolveEnvUsageSession(env);
117
+ if (envSession) {
118
+ return envSession;
119
+ }
120
+
121
+ try {
122
+ const sessions = await listActiveSessionsFn({ targetPath });
123
+ return selectMostRecentUsageSession(sessions);
124
+ } catch (error) {
125
+ return {
126
+ sessionId: "",
127
+ source: "error",
128
+ reason: "list_active_sessions_failed",
129
+ errorCode: error && typeof error === "object" ? normalizeString(error.code) || null : null,
130
+ };
131
+ }
132
+ }