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 +17 -3
- package/package.json +1 -1
- package/src/commands/omargate.js +2 -2
- package/src/legacy-cli.js +33 -2
- package/src/session/usage-session-resolver.js +132 -0
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.
|
|
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
|
-
|
|
959
|
-
|
|
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
package/src/commands/omargate.js
CHANGED
|
@@ -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-
|
|
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
|
|
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
|
|
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
|
+
}
|