sentinelayer-cli 0.39.1 → 0.40.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.
@@ -1,151 +1,22 @@
1
- // Shared helpers for Nora's (supply-chain) domain tools (#A22).
1
+ // Shared helpers for Dima's (supply-chain) domain tools (#A22).
2
2
 
3
- import fsp from "node:fs/promises";
4
- import path from "node:path";
5
- import process from "node:process";
3
+ import {
4
+ createFinding as createDomainFinding,
5
+ toPosix,
6
+ walkRepoFiles as walkDomainRepoFiles,
7
+ } from "../../shared-tools/domain-base.js";
6
8
 
7
- import ignore from "ignore";
9
+ const DEFAULT_PERSONA = "supply-chain";
8
10
 
9
- const DEFAULT_IGNORED_DIRS = new Set([
10
- ".git",
11
- "node_modules",
12
- ".venv",
13
- ".next",
14
- "dist",
15
- "build",
16
- "coverage",
17
- ".sentinelayer",
18
- ".sentinel",
19
- ".turbo",
20
- ".idea",
21
- ".vscode",
22
- "__pycache__",
23
- ".cache",
24
- ]);
25
- const MAX_FILE_SIZE_BYTES = 1024 * 1024;
26
- const SEVERITIES = Object.freeze(["P0", "P1", "P2", "P3"]);
11
+ export { toPosix };
27
12
 
28
- export function toPosix(value) {
29
- return String(value || "").replace(/\\/g, "/");
13
+ export function createFinding(options = {}) {
14
+ return createDomainFinding(options, {
15
+ defaultPersona: DEFAULT_PERSONA,
16
+ defaultKind: DEFAULT_PERSONA,
17
+ });
30
18
  }
31
19
 
32
- export function createFinding({
33
- severity,
34
- kind,
35
- file,
36
- line = 0,
37
- evidence = "",
38
- rootCause = "",
39
- recommendedFix = "",
40
- confidence = null,
41
- tool = "",
42
- persona = "supply-chain",
43
- } = {}) {
44
- const normalizedSeverity = SEVERITIES.includes(String(severity || "").toUpperCase())
45
- ? String(severity).toUpperCase()
46
- : "P2";
47
- return {
48
- persona,
49
- tool: String(tool || "").trim(),
50
- kind: String(kind || "").trim() || "supply-chain",
51
- severity: normalizedSeverity,
52
- file: toPosix(file || ""),
53
- line: Number.isFinite(Number(line)) ? Math.max(0, Math.floor(Number(line))) : 0,
54
- evidence: String(evidence || "").trim().slice(0, 400),
55
- rootCause: String(rootCause || "").trim(),
56
- recommendedFix: String(recommendedFix || "").trim(),
57
- confidence:
58
- confidence === null || confidence === undefined
59
- ? null
60
- : Math.max(0, Math.min(1, Number(confidence) || 0)),
61
- };
62
- }
63
-
64
- async function readIgnorePatterns(filePath) {
65
- try {
66
- const raw = await fsp.readFile(filePath, "utf-8");
67
- return String(raw || "")
68
- .split(/\r?\n/)
69
- .map((line) => line.trim())
70
- .filter((line) => line && !line.startsWith("#"));
71
- } catch (err) {
72
- if (err && typeof err === "object" && err.code === "ENOENT") {
73
- return [];
74
- }
75
- throw err;
76
- }
77
- }
78
-
79
- async function createIgnoreMatcher(rootPath) {
80
- const matcher = ignore();
81
- const gitignore = await readIgnorePatterns(path.join(rootPath, ".gitignore"));
82
- const sentinel = await readIgnorePatterns(
83
- path.join(rootPath, ".sentinelayerignore")
84
- );
85
- matcher.add([...gitignore, ...sentinel]);
86
- return (relativePath, isDirectory) => {
87
- const normalized = toPosix(relativePath);
88
- if (!normalized) {
89
- return false;
90
- }
91
- const candidate = isDirectory ? `${normalized}/` : normalized;
92
- return matcher.ignores(candidate);
93
- };
94
- }
95
-
96
- export async function* walkRepoFiles({
97
- rootPath = process.cwd(),
98
- extensions = new Set(),
99
- maxFileSize = MAX_FILE_SIZE_BYTES,
100
- } = {}) {
101
- const resolvedRoot = path.resolve(rootPath);
102
- const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
103
- const wantedExtensions =
104
- extensions instanceof Set
105
- ? extensions
106
- : new Set(Array.isArray(extensions) ? extensions : []);
107
- const stack = [resolvedRoot];
108
- while (stack.length > 0) {
109
- const current = stack.pop();
110
- let entries = [];
111
- try {
112
- entries = await fsp.readdir(current, { withFileTypes: true });
113
- } catch {
114
- continue;
115
- }
116
- for (const entry of entries) {
117
- const fullPath = path.join(current, entry.name);
118
- const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
119
- if (entry.isDirectory()) {
120
- if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
121
- continue;
122
- }
123
- if (ignoreMatcher(relativePath, true)) {
124
- continue;
125
- }
126
- stack.push(fullPath);
127
- continue;
128
- }
129
- if (!entry.isFile()) {
130
- continue;
131
- }
132
- if (ignoreMatcher(relativePath, false)) {
133
- continue;
134
- }
135
- const ext = path.extname(entry.name).toLowerCase();
136
- if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
137
- continue;
138
- }
139
- let stat = null;
140
- try {
141
- stat = await fsp.stat(fullPath);
142
- } catch {
143
- stat = null;
144
- }
145
- if (!stat || stat.size > maxFileSize) {
146
- continue;
147
- }
148
- yield { fullPath, relativePath };
149
- }
150
- }
20
+ export async function* walkRepoFiles(options = {}) {
21
+ yield* walkDomainRepoFiles(options);
151
22
  }
@@ -1,188 +1,18 @@
1
1
  // Shared helpers for Priya's (testing) domain tools (#A15).
2
- // Same pattern as security/tools/base.js and backend/tools/base.js — default
3
- // persona is "testing". Later refactor (#A25+) can hoist to shared-tools/.
4
2
 
5
- import fsp from "node:fs/promises";
6
- import path from "node:path";
7
- import process from "node:process";
3
+ import {
4
+ DEFAULT_IGNORED_DIRS,
5
+ MAX_FILE_SIZE_BYTES,
6
+ SEVERITIES,
7
+ createFinding as createDomainFinding,
8
+ findLineMatches,
9
+ getLineContent,
10
+ normalizeSeverity,
11
+ toPosix,
12
+ walkRepoFiles as walkDomainRepoFiles,
13
+ } from "../../shared-tools/domain-base.js";
8
14
 
9
- import ignore from "ignore";
10
-
11
- const DEFAULT_IGNORED_DIRS = new Set([
12
- ".git",
13
- "node_modules",
14
- ".venv",
15
- ".next",
16
- "dist",
17
- "build",
18
- "coverage",
19
- ".sentinelayer",
20
- ".sentinel",
21
- ".turbo",
22
- ".idea",
23
- ".vscode",
24
- "__pycache__",
25
- ".cache",
26
- ]);
27
- const MAX_FILE_SIZE_BYTES = 1024 * 1024;
28
- const SEVERITIES = Object.freeze(["P0", "P1", "P2", "P3"]);
29
-
30
- export function toPosix(value) {
31
- return String(value || "").replace(/\\/g, "/");
32
- }
33
-
34
- export function normalizeSeverity(value) {
35
- const normalized = String(value || "").trim().toUpperCase();
36
- if (SEVERITIES.includes(normalized)) {
37
- return normalized;
38
- }
39
- return "P2";
40
- }
41
-
42
- export function createFinding({
43
- severity,
44
- kind,
45
- file,
46
- line = 0,
47
- evidence = "",
48
- rootCause = "",
49
- recommendedFix = "",
50
- confidence = null,
51
- tool = "",
52
- persona = "testing",
53
- } = {}) {
54
- return {
55
- persona,
56
- tool: String(tool || "").trim(),
57
- kind: String(kind || "").trim() || "testing",
58
- severity: normalizeSeverity(severity),
59
- file: toPosix(file || ""),
60
- line: Number.isFinite(Number(line)) ? Math.max(0, Math.floor(Number(line))) : 0,
61
- evidence: String(evidence || "").trim().slice(0, 400),
62
- rootCause: String(rootCause || "").trim(),
63
- recommendedFix: String(recommendedFix || "").trim(),
64
- confidence:
65
- confidence === null || confidence === undefined
66
- ? null
67
- : Math.max(0, Math.min(1, Number(confidence) || 0)),
68
- };
69
- }
70
-
71
- async function readIgnorePatterns(filePath) {
72
- try {
73
- const raw = await fsp.readFile(filePath, "utf-8");
74
- return String(raw || "")
75
- .split(/\r?\n/)
76
- .map((line) => line.trim())
77
- .filter((line) => line && !line.startsWith("#"));
78
- } catch (err) {
79
- if (err && typeof err === "object" && err.code === "ENOENT") {
80
- return [];
81
- }
82
- throw err;
83
- }
84
- }
85
-
86
- async function createIgnoreMatcher(rootPath) {
87
- const matcher = ignore();
88
- const gitignore = await readIgnorePatterns(path.join(rootPath, ".gitignore"));
89
- const sentinel = await readIgnorePatterns(
90
- path.join(rootPath, ".sentinelayerignore")
91
- );
92
- matcher.add([...gitignore, ...sentinel]);
93
- return (relativePath, isDirectory) => {
94
- const normalized = toPosix(relativePath);
95
- if (!normalized) {
96
- return false;
97
- }
98
- const candidate = isDirectory ? `${normalized}/` : normalized;
99
- return matcher.ignores(candidate);
100
- };
101
- }
102
-
103
- export async function* walkRepoFiles({
104
- rootPath = process.cwd(),
105
- extensions = new Set(),
106
- maxFileSize = MAX_FILE_SIZE_BYTES,
107
- } = {}) {
108
- const resolvedRoot = path.resolve(rootPath);
109
- const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
110
- const wantedExtensions =
111
- extensions instanceof Set
112
- ? extensions
113
- : new Set(Array.isArray(extensions) ? extensions : []);
114
- const stack = [resolvedRoot];
115
- while (stack.length > 0) {
116
- const current = stack.pop();
117
- let entries = [];
118
- try {
119
- entries = await fsp.readdir(current, { withFileTypes: true });
120
- } catch {
121
- continue;
122
- }
123
- for (const entry of entries) {
124
- const fullPath = path.join(current, entry.name);
125
- const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
126
- if (entry.isDirectory()) {
127
- if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
128
- continue;
129
- }
130
- if (ignoreMatcher(relativePath, true)) {
131
- continue;
132
- }
133
- stack.push(fullPath);
134
- continue;
135
- }
136
- if (!entry.isFile()) {
137
- continue;
138
- }
139
- if (ignoreMatcher(relativePath, false)) {
140
- continue;
141
- }
142
- const ext = path.extname(entry.name).toLowerCase();
143
- if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
144
- continue;
145
- }
146
- let stat = null;
147
- try {
148
- stat = await fsp.stat(fullPath);
149
- } catch {
150
- stat = null;
151
- }
152
- if (!stat || stat.size > maxFileSize) {
153
- continue;
154
- }
155
- yield { fullPath, relativePath, stat };
156
- }
157
- }
158
- }
159
-
160
- export function findLineMatches(content, pattern) {
161
- const text = String(content || "");
162
- if (!pattern) {
163
- return [];
164
- }
165
- const global = new RegExp(
166
- pattern.source,
167
- pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`
168
- );
169
- const matches = [];
170
- let match;
171
- while ((match = global.exec(text)) !== null) {
172
- const lineIndex = text.slice(0, match.index).split(/\r?\n/).length;
173
- matches.push({
174
- index: match.index,
175
- line: lineIndex,
176
- match: match[0],
177
- });
178
- }
179
- return matches;
180
- }
181
-
182
- export function getLineContent(content, line) {
183
- const lines = String(content || "").split(/\r?\n/);
184
- return (lines[Math.max(0, (Number(line) || 1) - 1)] || "").trim();
185
- }
15
+ const DEFAULT_PERSONA = "testing";
186
16
 
187
17
  const TEST_FILE_PATTERNS = [
188
18
  /\.test\.(js|jsx|ts|tsx|mjs|cjs)$/,
@@ -194,9 +24,28 @@ const TEST_FILE_PATTERNS = [
194
24
  /(^|\/)tests?\/[^/]*\.py$/,
195
25
  ];
196
26
 
27
+ export {
28
+ DEFAULT_IGNORED_DIRS,
29
+ MAX_FILE_SIZE_BYTES,
30
+ SEVERITIES,
31
+ findLineMatches,
32
+ getLineContent,
33
+ normalizeSeverity,
34
+ toPosix,
35
+ };
36
+
37
+ export function createFinding(options = {}) {
38
+ return createDomainFinding(options, {
39
+ defaultPersona: DEFAULT_PERSONA,
40
+ defaultKind: DEFAULT_PERSONA,
41
+ });
42
+ }
43
+
44
+ export async function* walkRepoFiles(options = {}) {
45
+ yield* walkDomainRepoFiles(options, { entryMetadata: "stat" });
46
+ }
47
+
197
48
  export function isTestFile(relativePath) {
198
49
  const normalized = toPosix(relativePath);
199
50
  return TEST_FILE_PATTERNS.some((pattern) => pattern.test(normalized));
200
51
  }
201
-
202
- export { DEFAULT_IGNORED_DIRS, MAX_FILE_SIZE_BYTES, SEVERITIES };
package/src/cli.js CHANGED
@@ -124,6 +124,11 @@ const COMMAND_REGISTRARS = {
124
124
  exportName: "registerSessionCommand",
125
125
  needsLegacy: false,
126
126
  },
127
+ "ring-owner": {
128
+ loader: () => import("./commands/ring-owner.js"),
129
+ exportName: "registerRingOwnerCommand",
130
+ needsLegacy: false,
131
+ },
127
132
  };
128
133
 
129
134
  const COMMAND_SET = new Set(Object.keys(COMMAND_REGISTRARS));
@@ -236,7 +236,7 @@ Docs: https://github.com/mrrCarter/create-sentinelayer/blob/main/docs/mcp.md`,
236
236
  .description(
237
237
  "Smoke the hosted MCP resource with an in-memory bearer; prints only redacted proof"
238
238
  )
239
- .option("--session <id>", "Optional Senti session id to read through sessions.events.list")
239
+ .option("--session <id>", "Optional Senti session id to read through sessions_events_list")
240
240
  .option("--limit <n>", "Maximum session events to read when --session is set", "5")
241
241
  .option(
242
242
  "--scope <scopes>",
@@ -0,0 +1,58 @@
1
+ import process from "node:process";
2
+
3
+ import { ringOwner } from "../pocket/ring-owner.js";
4
+
5
+ function shouldEmitJson(options, command) {
6
+ if (options && options.json) return true;
7
+ return typeof command?.optsWithGlobals === "function" ? Boolean(command.optsWithGlobals().json) : false;
8
+ }
9
+
10
+ export function registerRingOwnerCommand(program) {
11
+ program
12
+ .command("ring-owner")
13
+ .description("Ring the owner's phone about a decision that needs them (Senti Pocket dial)")
14
+ .argument("<question>", "The question / decision to put to the owner")
15
+ .requiredOption("--session <id>", "The session this decision belongs to")
16
+ .option("--kind <kind>", "decisionYours | pickOption | go | info | checkpointReady", "decisionYours")
17
+ .option(
18
+ "--option <label>",
19
+ "A pickOption choice (repeat for multiple)",
20
+ (value, acc) => { acc.push(value); return acc; },
21
+ [],
22
+ )
23
+ .option("--what-we-need <text>", "A short 'what we need' lead-in for the ring")
24
+ .option("--checkpoint <id>", "An optional checkpoint id for jump-to context")
25
+ .option("--idempotency-key <key>", "Dedupe key so an accidental retry rings only once")
26
+ .option("--gateway-url <url>", "Pocket gateway base URL (else the SENTI_POCKET_URL env var)")
27
+ .option("--json", "Emit machine-readable output")
28
+ .action(async (question, options, command) => {
29
+ const emitJson = shouldEmitJson(options, command);
30
+ try {
31
+ const result = await ringOwner(question, {
32
+ kind: options.kind,
33
+ sessionId: options.session,
34
+ options: options.option || [],
35
+ whatWeNeed: options.whatWeNeed,
36
+ checkpointId: options.checkpoint,
37
+ idempotencyKey: options.idempotencyKey,
38
+ gatewayUrl: options.gatewayUrl,
39
+ env: process.env,
40
+ });
41
+ if (emitJson) {
42
+ process.stdout.write(`${JSON.stringify(result)}\n`);
43
+ return;
44
+ }
45
+ if (result && result.dispatched) {
46
+ const replay = result.idempotent ? " (idempotent replay — already ringing)" : "";
47
+ process.stdout.write(`Ringing the owner — dialId ${result.dialId}${replay}\n`);
48
+ } else {
49
+ const reason = result && result.reason ? `: ${result.reason}` : "";
50
+ process.stdout.write(`Not dispatched${reason} (dialId ${(result && result.dialId) || "n/a"})\n`);
51
+ process.exitCode = 1;
52
+ }
53
+ } catch (err) {
54
+ process.stderr.write(`ring-owner failed: ${err && err.message ? err.message : String(err)}\n`);
55
+ process.exitCode = 1;
56
+ }
57
+ });
58
+ }
@@ -138,6 +138,7 @@ import {
138
138
  generateSessionCheckpoint,
139
139
  generateSessionCheckpointBatch,
140
140
  listSessionCheckpoints,
141
+ showSessionCheckpoint,
141
142
  } from "../session/checkpoints.js";
142
143
  import {
143
144
  buildCodexExecResumeInvocation,
@@ -2317,7 +2318,7 @@ export async function resolveMessageActionIdentity({
2317
2318
  return resolveSessionSayIdentity({ sessionId, agentId: agentSeed, targetPath, env });
2318
2319
  }
2319
2320
 
2320
- async function ensureSessionSayAgentRegistered(
2321
+ async function recordPublishedSessionAgentActivity(
2321
2322
  sessionId,
2322
2323
  agent = {},
2323
2324
  { targetPath = process.cwd() } = {},
@@ -2328,33 +2329,27 @@ async function ensureSessionSayAgentRegistered(
2328
2329
  }
2329
2330
 
2330
2331
  try {
2331
- const activeAgents = await listAgents(sessionId, { targetPath, includeInactive: false });
2332
- if (
2333
- activeAgents.some(
2334
- (existing) => normalizeString(existing.agentId).toLowerCase() === agentId.toLowerCase(),
2335
- )
2336
- ) {
2337
- return { persisted: false, reason: "already_registered" };
2338
- }
2339
- } catch {
2340
- // If the local registry is unreadable, let rememberAgentIdentity surface the
2341
- // filesystem problem with its normal error message.
2332
+ const snapshot = await rememberAgentIdentity(sessionId, {
2333
+ agentId,
2334
+ model: normalizeString(agent.model) || "cli",
2335
+ displayName: normalizeString(agent.displayName),
2336
+ provider: normalizeString(agent.provider),
2337
+ clientKind: normalizeString(agent.clientKind) || "cli",
2338
+ role: sessionSayRegistryRole(agent.role),
2339
+ targetPath,
2340
+ });
2341
+ return {
2342
+ persisted: true,
2343
+ agentId: snapshot.agentId,
2344
+ lastActivityAt: snapshot.lastActivityAt,
2345
+ };
2346
+ } catch (error) {
2347
+ return {
2348
+ persisted: false,
2349
+ reason: "activity_update_failed",
2350
+ detail: normalizeString(error?.message),
2351
+ };
2342
2352
  }
2343
-
2344
- const registered = await rememberAgentIdentity(sessionId, {
2345
- agentId,
2346
- model: normalizeString(agent.model) || "cli",
2347
- displayName: normalizeString(agent.displayName),
2348
- provider: normalizeString(agent.provider),
2349
- clientKind: normalizeString(agent.clientKind) || "cli",
2350
- role: sessionSayRegistryRole(agent.role),
2351
- targetPath,
2352
- });
2353
-
2354
- return {
2355
- persisted: true,
2356
- agentId: registered.agentId,
2357
- };
2358
2353
  }
2359
2354
 
2360
2355
  async function resolveSessionAgentEnvelope(
@@ -3645,9 +3640,6 @@ export function registerSessionCommand(program) {
3645
3640
  role: options.role,
3646
3641
  displayName: options.displayName,
3647
3642
  });
3648
- const agentRegistration = await ensureSessionSayAgentRegistered(normalizedSessionId, agent, {
3649
- targetPath,
3650
- });
3651
3643
  const event = createAgentEvent({
3652
3644
  event: "session_message",
3653
3645
  agent,
@@ -3699,6 +3691,10 @@ export function registerSessionCommand(program) {
3699
3691
  targetPath,
3700
3692
  syncRemote: false,
3701
3693
  });
3694
+ const agentActivity = await recordPublishedSessionAgentActivity(normalizedSessionId, agent, {
3695
+ targetPath,
3696
+ });
3697
+ const agentRegistration = agentActivity;
3702
3698
  const payload = {
3703
3699
  command: "session say",
3704
3700
  targetPath,
@@ -3710,6 +3706,7 @@ export function registerSessionCommand(program) {
3710
3706
  identitySource: identity.source,
3711
3707
  identityWarning: identity.identityWarning || undefined,
3712
3708
  agentRegistration,
3709
+ agentActivity,
3713
3710
  remoteSync: remoteSync || undefined,
3714
3711
  remoteConfirmationAnchor: remoteConfirmationAnchor || undefined,
3715
3712
  remoteConfirmation: remoteConfirmation || undefined,
@@ -3822,6 +3819,9 @@ export function registerSessionCommand(program) {
3822
3819
  targetPath,
3823
3820
  syncRemote: false,
3824
3821
  });
3822
+ const agentActivity = await recordPublishedSessionAgentActivity(normalizedSessionId, agent, {
3823
+ targetPath,
3824
+ });
3825
3825
  const payload = {
3826
3826
  command: "session post-agent",
3827
3827
  targetPath,
@@ -3830,6 +3830,7 @@ export function registerSessionCommand(program) {
3830
3830
  event: persisted,
3831
3831
  materializedLocalSession: localSession.materialized,
3832
3832
  refreshedLocalSession: Boolean(localSession.refreshed),
3833
+ agentActivity,
3833
3834
  remoteSync,
3834
3835
  remoteConfirmationAnchor,
3835
3836
  remoteConfirmation,
@@ -6151,7 +6152,7 @@ export function registerSessionCommand(program) {
6151
6152
 
6152
6153
  const checkpoint = session
6153
6154
  .command("checkpoint")
6154
- .description("List, create, and generate durable session checkpoints");
6155
+ .description("List, show, create, and generate durable session checkpoints");
6155
6156
 
6156
6157
  checkpoint
6157
6158
  .command("list <sessionId>")
@@ -6187,6 +6188,66 @@ export function registerSessionCommand(program) {
6187
6188
  }
6188
6189
  });
6189
6190
 
6191
+ checkpoint
6192
+ .command("show <sessionId> <checkpointId>")
6193
+ .description("Show a checkpoint and a bounded read-only source event window")
6194
+ .option("--context-events <n>", "Adjacent events to include before/after the checkpoint range (default 3)", "3")
6195
+ .option("--max-events <n>", "Maximum events to fetch for restore context (default 120, max 200)", "120")
6196
+ .option("--path <path>", "Workspace path for auth/session context", ".")
6197
+ .option("--json", "Emit machine-readable output")
6198
+ .action(async (sessionId, checkpointId, options, command) => {
6199
+ const normalizedSessionId = normalizeString(sessionId);
6200
+ const normalizedCheckpointId = normalizeString(checkpointId);
6201
+ if (!normalizedSessionId) {
6202
+ throw new Error("session id is required.");
6203
+ }
6204
+ if (!normalizedCheckpointId) {
6205
+ throw new Error("checkpoint id is required.");
6206
+ }
6207
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
6208
+ const result = await showSessionCheckpoint(normalizedSessionId, normalizedCheckpointId, {
6209
+ targetPath,
6210
+ contextEvents: options.contextEvents,
6211
+ maxEvents: options.maxEvents,
6212
+ });
6213
+ const payload = {
6214
+ command: "session checkpoint show",
6215
+ targetPath,
6216
+ ...result,
6217
+ };
6218
+ if (shouldEmitJson(options, command)) {
6219
+ console.log(JSON.stringify(payload, null, 2));
6220
+ return;
6221
+ }
6222
+
6223
+ console.log(pc.bold(`Checkpoint ${result.checkpointId}`));
6224
+ console.log(formatCheckpointLine(result.checkpoint));
6225
+ const sourceRange = `#${result.window.startSequence}-${result.window.endSequence}`;
6226
+ const contextSummary =
6227
+ `${result.window.sourceEventCount}/${result.window.sourceEventCountExpected} source` +
6228
+ `, ${result.window.beforeContextCount} before` +
6229
+ `, ${result.window.afterContextCount} after`;
6230
+ console.log(pc.gray(`Read-only restore window ${sourceRange}: ${contextSummary}.`));
6231
+ if (!result.ok) {
6232
+ console.log(pc.yellow(`Remote event window unavailable: ${result.reason || "unknown"}`));
6233
+ return;
6234
+ }
6235
+ if (result.window.partial) {
6236
+ const missing = result.window.missingSourceEvents
6237
+ ? ` missing=${result.window.missingSourceEvents}`
6238
+ : "";
6239
+ const truncated = result.window.truncatedByLimit ? " truncated-by-limit" : "";
6240
+ console.log(pc.yellow(`Partial checkpoint source range:${missing}${truncated}`.trim()));
6241
+ }
6242
+ for (const event of result.window.events) {
6243
+ const sequence = eventSequenceNumber(event);
6244
+ const inSource =
6245
+ sequence >= result.window.startSequence &&
6246
+ sequence <= result.window.endSequence;
6247
+ console.log(`${inSource ? "*" : " "} ${formatEventLine(event)}`);
6248
+ }
6249
+ });
6250
+
6190
6251
  checkpoint
6191
6252
  .command("create <sessionId>")
6192
6253
  .description("Create a durable checkpoint anchored to a canonical sequence range")
package/src/legacy-cli.js CHANGED
@@ -2517,7 +2517,7 @@ jobs:
2517
2517
  fi
2518
2518
  - name: Run Omar Gate
2519
2519
  id: omar
2520
- uses: mrrCarter/sentinelayer-v1-action@a496be33a466c0cc3f8616d66bbd7d78f7d3c31d
2520
+ uses: mrrCarter/sentinelayer-v1-action@8afb7dbb42a5e1c25233d422c4cabe401ba02380
2521
2521
  with:
2522
2522
  github_token: \${{ github.token }}
2523
2523
  openai_api_key: \${{ secrets.OPENAI_API_KEY }}
@@ -2626,7 +2626,7 @@ jobs:
2626
2626
  },
2627
2627
  "scan": {
2628
2628
  "mode": env("OMAR_SCAN_MODE", "deep"),
2629
- "action_ref": "mrrCarter/sentinelayer-v1-action@a496be33a466c0cc3f8616d66bbd7d78f7d3c31d",
2629
+ "action_ref": "mrrCarter/sentinelayer-v1-action@8afb7dbb42a5e1c25233d422c4cabe401ba02380",
2630
2630
  "llm_provider": env("OMAR_LLM_PROVIDER", "openai"),
2631
2631
  "model": env("OMAR_MODEL", "gpt-5.3-codex"),
2632
2632
  "model_fallback": env("OMAR_MODEL_FALLBACK", "gpt-4.1-mini"),