sentinelayer-cli 0.12.4 → 0.13.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.
package/README.md CHANGED
@@ -104,6 +104,7 @@ Inputs for non-interactive mode:
104
104
  Sentinelayer includes a deterministic session coordination surface for multi-agent coding loops:
105
105
 
106
106
  - session event stream and replay (`start`, `join`, `say`, `read`, `status`, `leave`, `list`, `kill`)
107
+ - low-noise message actions (`react ack|like|dislike`, `action working_on|disregard`, `reply`/`comment`, `view`, `actions`)
107
108
  - agent lifecycle controls (join/heartbeat/leave/kill)
108
109
  - recap and context briefing for late-joining agents
109
110
  - analytics + lineage artifacts at session closeout
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.12.4",
3
+ "version": "0.13.0",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -31,6 +31,7 @@ export async function recordCliLlmSessionUsage({
31
31
  sourceCommand = "",
32
32
  provider = "",
33
33
  metadata = {},
34
+ syncRemote = true,
34
35
  } = {}) {
35
36
  const normalizedSessionId = normalizeString(sessionId);
36
37
  const normalizedAgentId = normalizeString(agentId);
@@ -80,7 +81,7 @@ export async function recordCliLlmSessionUsage({
80
81
  ...metadata,
81
82
  },
82
83
  },
83
- { targetPath },
84
+ { targetPath, syncRemote },
84
85
  );
85
86
  } catch (error) {
86
87
  return {
@@ -107,13 +107,58 @@ const SESSION_MESSAGE_ACTION_TYPES = new Set([
107
107
  "like",
108
108
  "dislike",
109
109
  "disregard",
110
+ "view",
111
+ ]);
112
+
113
+ const SESSION_MESSAGE_ACTION_ALIASES = new Map([
114
+ ["comment", "reply"],
115
+ ]);
116
+
117
+ const SESSION_MESSAGE_ACTION_DESCRIPTIONS = Object.freeze([
118
+ {
119
+ type: "ack",
120
+ command: "sl session react <id> ack --target-sequence <n>",
121
+ description: "Acknowledge that you read a message without adding a top-level post.",
122
+ },
123
+ {
124
+ type: "working_on",
125
+ command: "sl session action <id> working_on --target-sequence <n> --note \"scope\"",
126
+ description: "Claim active ownership of a target message or task.",
127
+ },
128
+ {
129
+ type: "reply",
130
+ alias: "comment",
131
+ command: "sl session reply <id> <sequence> \"message\"",
132
+ description: "Thread a substantive response under a specific message.",
133
+ },
134
+ {
135
+ type: "like",
136
+ command: "sl session react <id> like --target-sequence <n>",
137
+ description: "Positive lightweight feedback. Use --target-action-id <uuid> to react to a threaded reply.",
138
+ },
139
+ {
140
+ type: "dislike",
141
+ command: "sl session react <id> dislike --target-sequence <n>",
142
+ description: "Negative lightweight feedback. Use --target-action-id <uuid> to react to a threaded reply.",
143
+ },
144
+ {
145
+ type: "disregard",
146
+ command: "sl session action <id> disregard --target-sequence <n>",
147
+ description: "Mark a message as intentionally ignored or superseded.",
148
+ },
149
+ {
150
+ type: "view",
151
+ command: "sl session view <id> <sequence>",
152
+ description: "Record a read receipt for a target message.",
153
+ },
110
154
  ]);
111
155
 
112
156
  function normalizeSessionMessageActionType(value) {
113
- const normalized = normalizeString(value).toLowerCase();
157
+ const raw = normalizeString(value).toLowerCase();
158
+ const normalized = SESSION_MESSAGE_ACTION_ALIASES.get(raw) || raw;
114
159
  if (!SESSION_MESSAGE_ACTION_TYPES.has(normalized)) {
115
160
  throw new Error(
116
- `action type must be one of: ${[...SESSION_MESSAGE_ACTION_TYPES].join(", ")}.`,
161
+ `action type must be one of: ${[...SESSION_MESSAGE_ACTION_TYPES].join(", ")}; aliases: comment=reply.`,
117
162
  );
118
163
  }
119
164
  return normalized;
@@ -145,6 +190,10 @@ function actionTargetCursor(action = {}) {
145
190
  return normalizeString(action.targetCursor ?? action.target_cursor);
146
191
  }
147
192
 
193
+ function actionTargetActionId(action = {}) {
194
+ return normalizeString(action.targetActionId ?? action.target_action_id);
195
+ }
196
+
148
197
  function actionActorId(action = {}) {
149
198
  return normalizeString(action.actorId ?? action.actor_id) || "unknown";
150
199
  }
@@ -156,7 +205,11 @@ function actionCreatedAt(action = {}) {
156
205
  function actionDisplayMessage(action = {}) {
157
206
  const actionType = normalizeString(action.actionType ?? action.action_type).toLowerCase();
158
207
  const targetSequence = actionTargetSequence(action);
159
- const targetLabel = targetSequence ? `#${targetSequence}` : actionTargetCursor(action) || "target";
208
+ const targetActionId = actionTargetActionId(action);
209
+ const parentLabel = targetSequence ? `#${targetSequence}` : actionTargetCursor(action) || "";
210
+ const targetLabel = targetActionId
211
+ ? `action:${targetActionId}${parentLabel ? ` (${parentLabel})` : ""}`
212
+ : parentLabel || "target";
160
213
  const note = normalizeString(action.note);
161
214
  if (note) return `${actionType} ${targetLabel}: ${note}`;
162
215
  return `${actionType} ${targetLabel}`;
@@ -172,6 +225,7 @@ function buildSessionActionEvent(sessionId, action = {}) {
172
225
  actionType,
173
226
  targetSequenceId: actionTargetSequence(action),
174
227
  targetCursor: actionTargetCursor(action),
228
+ targetActionId: actionTargetActionId(action),
175
229
  actorId: actionActorId(action),
176
230
  note: normalizeString(action.note),
177
231
  createdAt: actionCreatedAt(action),
@@ -192,6 +246,7 @@ function buildSessionActionEvent(sessionId, action = {}) {
192
246
  actionType,
193
247
  targetSequenceId: actionTargetSequence(action),
194
248
  targetCursor: actionTargetCursor(action) || null,
249
+ targetActionId: actionTargetActionId(action) || null,
195
250
  note: normalizeString(action.note) || null,
196
251
  message: actionDisplayMessage(action),
197
252
  source: "session_action",
@@ -259,10 +314,15 @@ function defaultActionIdempotencyKey({
259
314
  actionType,
260
315
  targetSequenceId,
261
316
  targetCursor,
317
+ targetActionId,
262
318
  note,
263
319
  agentId,
264
320
  } = {}) {
265
- const target = targetSequenceId ? `seq:${targetSequenceId}` : `cursor:${normalizeString(targetCursor)}`;
321
+ const target = normalizeString(targetActionId)
322
+ ? `action:${normalizeString(targetActionId)}`
323
+ : targetSequenceId
324
+ ? `seq:${targetSequenceId}`
325
+ : `cursor:${normalizeString(targetCursor)}`;
266
326
  const noteHash = note ? shortSha256(note) : "none";
267
327
  const actor = normalizeString(agentId) || "user";
268
328
  return `cli:${normalizeString(actionType).toLowerCase()}:${target}:${actor}:${noteHash}`;
@@ -1768,6 +1828,7 @@ export function registerSessionCommand(program) {
1768
1828
  commandName = "session action",
1769
1829
  targetSequenceId: targetSequenceIdOverride = null,
1770
1830
  targetCursor: targetCursorOverride = "",
1831
+ targetActionId: targetActionIdOverride = "",
1771
1832
  note: noteOverride = "",
1772
1833
  } = {}) {
1773
1834
  const normalizedSessionId = normalizeString(sessionId);
@@ -1780,8 +1841,9 @@ export function registerSessionCommand(program) {
1780
1841
  targetSequenceIdOverride ||
1781
1842
  parseOptionalPositiveInteger(options.targetSequence, "target-sequence");
1782
1843
  const targetCursor = normalizeString(targetCursorOverride) || normalizeString(options.targetCursor);
1783
- if (!targetSequenceId && !targetCursor) {
1784
- throw new Error("Provide --target-sequence or --target-cursor.");
1844
+ const targetActionId = normalizeString(targetActionIdOverride) || normalizeString(options.targetActionId);
1845
+ if (!targetSequenceId && !targetCursor && !targetActionId) {
1846
+ throw new Error("Provide --target-sequence, --target-cursor, or --target-action-id.");
1785
1847
  }
1786
1848
  await ensureLocalSessionForRemoteCommand(normalizedSessionId, { targetPath });
1787
1849
  const note = normalizeString(noteOverride) || normalizeString(options.note);
@@ -1792,6 +1854,7 @@ export function registerSessionCommand(program) {
1792
1854
  actionType: normalizedActionType,
1793
1855
  targetSequenceId,
1794
1856
  targetCursor,
1857
+ targetActionId,
1795
1858
  note,
1796
1859
  agentId,
1797
1860
  });
@@ -1801,6 +1864,7 @@ export function registerSessionCommand(program) {
1801
1864
  targetPath,
1802
1865
  targetSequenceId,
1803
1866
  targetCursor,
1867
+ targetActionId,
1804
1868
  note,
1805
1869
  metadata: {
1806
1870
  source: "cli",
@@ -1837,11 +1901,37 @@ export function registerSessionCommand(program) {
1837
1901
  return payload;
1838
1902
  }
1839
1903
 
1904
+ session
1905
+ .command("actions")
1906
+ .description("List supported low-noise message actions with examples")
1907
+ .option("--json", "Emit machine-readable output")
1908
+ .action((options, command) => {
1909
+ const payload = {
1910
+ command: "session actions",
1911
+ actions: SESSION_MESSAGE_ACTION_DESCRIPTIONS,
1912
+ };
1913
+ if (shouldEmitJson(options, command)) {
1914
+ console.log(JSON.stringify(payload, null, 2));
1915
+ return payload;
1916
+ }
1917
+ console.log(pc.bold("Supported session message actions"));
1918
+ for (const action of SESSION_MESSAGE_ACTION_DESCRIPTIONS) {
1919
+ const alias = action.alias ? ` (alias: ${action.alias})` : "";
1920
+ console.log(`${pc.cyan(action.type)}${alias}`);
1921
+ console.log(` ${action.description}`);
1922
+ console.log(pc.gray(` ${action.command}`));
1923
+ }
1924
+ return payload;
1925
+ });
1926
+
1840
1927
  session
1841
1928
  .command("action <sessionId> <actionType>")
1842
- .description("Create a message action for a target session event")
1929
+ .description(
1930
+ "Create a message action for a target session event (ack, working_on, reply/comment, like, dislike, disregard, view)",
1931
+ )
1843
1932
  .option("--target-sequence <n>", "Target event sequence id")
1844
1933
  .option("--target-cursor <cursor>", "Target event cursor")
1934
+ .option("--target-action-id <uuid>", "Target a threaded reply/action by action UUID")
1845
1935
  .option("--note <text>", "Optional action note or reply body")
1846
1936
  .option("--agent <id>", "Agent id for local idempotency metadata", "cli-user")
1847
1937
  .option("--idempotency-key <key>", "Explicit idempotency key")
@@ -1856,6 +1946,7 @@ export function registerSessionCommand(program) {
1856
1946
  .description("React to or acknowledge a target session event with ack, like, or dislike")
1857
1947
  .option("--target-sequence <n>", "Target event sequence id")
1858
1948
  .option("--target-cursor <cursor>", "Target event cursor")
1949
+ .option("--target-action-id <uuid>", "Target a threaded reply/action by action UUID")
1859
1950
  .option("--agent <id>", "Agent id for local idempotency metadata", "cli-user")
1860
1951
  .option("--idempotency-key <key>", "Explicit idempotency key")
1861
1952
  .option("--path <path>", "Workspace path for the session", ".")
@@ -1894,6 +1985,44 @@ export function registerSessionCommand(program) {
1894
1985
  });
1895
1986
  });
1896
1987
 
1988
+ session
1989
+ .command("comment <sessionId> <targetSequenceId> <message...>")
1990
+ .description("Alias for `session reply`; add a threaded comment to a target event")
1991
+ .option("--agent <id>", "Agent id for local idempotency metadata", "cli-user")
1992
+ .option("--idempotency-key <key>", "Explicit idempotency key")
1993
+ .option("--path <path>", "Workspace path for the session", ".")
1994
+ .option("--json", "Emit machine-readable output")
1995
+ .action(async (sessionId, targetSequenceId, messageParts, options, command) => {
1996
+ const message = Array.isArray(messageParts) ? messageParts.join(" ") : messageParts;
1997
+ await runMessageActionCommand({
1998
+ sessionId,
1999
+ actionType: "reply",
2000
+ options,
2001
+ command,
2002
+ commandName: "session comment",
2003
+ targetSequenceId: parsePositiveInteger(targetSequenceId, "targetSequenceId", 0),
2004
+ note: message,
2005
+ });
2006
+ });
2007
+
2008
+ session
2009
+ .command("view <sessionId> <targetSequenceId>")
2010
+ .description("Record a read receipt for a target session event")
2011
+ .option("--agent <id>", "Agent id for local idempotency metadata", "cli-user")
2012
+ .option("--idempotency-key <key>", "Explicit idempotency key")
2013
+ .option("--path <path>", "Workspace path for the session", ".")
2014
+ .option("--json", "Emit machine-readable output")
2015
+ .action(async (sessionId, targetSequenceId, options, command) => {
2016
+ await runMessageActionCommand({
2017
+ sessionId,
2018
+ actionType: "view",
2019
+ options,
2020
+ command,
2021
+ commandName: "session view",
2022
+ targetSequenceId: parsePositiveInteger(targetSequenceId, "targetSequenceId", 0),
2023
+ });
2024
+ });
2025
+
1897
2026
  session
1898
2027
  .command("listen")
1899
2028
  .description("Background-poll a session for events addressed to this agent or broadcast")
package/src/legacy-cli.js CHANGED
@@ -218,6 +218,12 @@ function printUsage() {
218
218
  console.log(" sl session say <id> \"assign: @agent <task>\" Create task assignment + lease");
219
219
  console.log(" sl session say <id> \"assign: @*:reviewer <task>\" Wildcard route to least-busy role");
220
220
  console.log(" sl session say <id> \"accepted: task <task-id>\" / \"done: task <task-id>\" Task transitions");
221
+ console.log(" sl session actions List low-noise actions and examples");
222
+ console.log(" sl session react <id> ack --target-sequence <n> ACK/like/dislike without a new post");
223
+ console.log(" sl session action <id> working_on --target-sequence <n> Claim work on a message");
224
+ console.log(" sl session reply <id> <seq> \"msg\" Thread a response under a message");
225
+ console.log(" sl session comment <id> <seq> \"msg\" Alias for threaded reply");
226
+ console.log(" sl session view <id> <seq> Record a read receipt");
221
227
  console.log(" sl session read <id> --tail 20 Read session stream events");
222
228
  console.log(" sl session status <id> --json Show session health, agents, runs, leases");
223
229
  console.log(" sl session leave <id> Leave a session");
@@ -6,7 +6,7 @@ export const COORDINATION_ETIQUETTE_ITEMS = Object.freeze([
6
6
  "Before implementation, post a short plan and file claims with `sl session say <id> \"plan: <scope>; files: <paths>\"`.",
7
7
  "Claim shared files before editing with `lock: <file> - <intent>` and release them with `unlock: <file> - done`.",
8
8
  "Run a background listener for replies: `sl session listen --session <id> --agent <your-name> --interval 60 --active-interval 5 --emit ndjson`; this idles at 60s and switches to 5s after human activity. If background polling is unavailable, fall back to `sl session sync <id> --json` then `sl session read <id> --tail 20 --json` every 5 minutes.",
9
- "Use message actions for low-noise coordination: `sl session react <id> ack --target-sequence <n>` for ACKs, `sl session action <id> working_on --target-sequence <n>` for ownership, and `sl session reply <id> <sequence> \"<message>\"` or `sl session say <id> \"<message>\" --reply-to <sequence>` for threaded responses.",
9
+ "Use message actions for low-noise coordination before posting a new top-level message: `sl session react <id> ack --target-sequence <n>` for ACKs, `sl session action <id> working_on --target-sequence <n>` for ownership, `sl session view <id> <sequence>` for read receipts, and `sl session reply <id> <sequence> \"<message>\"` / `sl session comment <id> <sequence> \"<message>\"` for threaded responses. Run `sl session actions` to list all action types.",
10
10
  "Search before asking peers to restate context: `sl session search <id> \"<topic>\" --limit 10`.",
11
11
  "Run `sl review --diff` after each finished file or PR-ready diff and post the result summary back to the session.",
12
12
  "Post findings through `sl session say <id> \"finding: [P2] <title> in <file>:<line>\"` with enough context for a peer to act.",
@@ -418,7 +418,7 @@ const AGENT_JOIN_RULES = [
418
418
  "",
419
419
  "**Writing back** — You can use **markdown**: bold, italic, lists, fenced code, and `inline code`. The web dashboard renders it. Plain text also works. Keep posts terse and technical — link to the work, don't recap it.",
420
420
  "",
421
- "**Actions and threading** — ACK or claim work with message actions instead of top-level chatter: `sl session react <id> ack --target-sequence <n>` or `sl session action <id> working_on --target-sequence <n>`. Reply to a specific message with `sl session reply <id> <sequence> \"<message>\"` or `sl session say <id> \"<message>\" --reply-to <sequence>`; only start a new top-level post for a new topic.",
421
+ "**Actions and threading** — ACK, view, react, or claim work with message actions instead of top-level chatter: `sl session react <id> ack --target-sequence <n>`, `sl session view <id> <sequence>`, or `sl session action <id> working_on --target-sequence <n>`. Reply to a specific message with `sl session reply <id> <sequence> \"<message>\"`, `sl session comment <id> <sequence> \"<message>\"`, or `sl session say <id> \"<message>\" --reply-to <sequence>`; only start a new top-level post for a new topic. Run `sl session actions` for the full list.",
422
422
  "",
423
423
  "**Search before asking** — Use `sl session search <id> \"<topic>\" --limit 10` to recover old context before asking another agent to re-paste or summarize what is already in the transcript.",
424
424
  "",
@@ -1245,6 +1245,7 @@ export async function listSessionMessageActions(
1245
1245
  {
1246
1246
  targetPath = process.cwd(),
1247
1247
  targetSequenceId = null,
1248
+ targetActionId = "",
1248
1249
  limit = SESSION_ACTION_FETCH_LIMIT,
1249
1250
  timeoutMs = DEFAULT_SYNC_TIMEOUT_MS,
1250
1251
  forceCircuitProbe = false,
@@ -1294,6 +1295,10 @@ export async function listSessionMessageActions(
1294
1295
  if (Number.isFinite(normalizedTargetSequence) && normalizedTargetSequence > 0) {
1295
1296
  query.set("targetSequenceId", String(Math.floor(normalizedTargetSequence)));
1296
1297
  }
1298
+ const normalizedTargetActionId = normalizeString(targetActionId);
1299
+ if (normalizedTargetActionId) {
1300
+ query.set("targetActionId", normalizedTargetActionId);
1301
+ }
1297
1302
  query.set(
1298
1303
  "limit",
1299
1304
  String(Math.max(1, Math.min(SESSION_ACTION_FETCH_LIMIT, normalizePositiveInteger(limit, 200))))
@@ -1351,6 +1356,7 @@ export async function createSessionMessageAction(
1351
1356
  targetPath = process.cwd(),
1352
1357
  targetSequenceId = null,
1353
1358
  targetCursor = "",
1359
+ targetActionId = "",
1354
1360
  note = "",
1355
1361
  metadata = {},
1356
1362
  idempotencyKey = "",
@@ -1402,6 +1408,10 @@ export async function createSessionMessageAction(
1402
1408
  if (normalizedTargetCursor) {
1403
1409
  body.targetCursor = normalizedTargetCursor;
1404
1410
  }
1411
+ const normalizedTargetActionId = normalizeString(targetActionId);
1412
+ if (normalizedTargetActionId) {
1413
+ body.targetActionId = normalizedTargetActionId;
1414
+ }
1405
1415
  const normalizedNote = normalizeString(note);
1406
1416
  if (normalizedNote) {
1407
1417
  body.note = normalizedNote;
@@ -155,7 +155,63 @@ function eventTimestamp(event) {
155
155
  return normalize(event?.ts || event?.timestamp);
156
156
  }
157
157
 
158
+ function actionTargetLabel(payload = {}) {
159
+ const targetActionId = normalize(payload.targetActionId || payload.target_action_id);
160
+ const targetSequence = Number(payload.targetSequenceId || payload.target_sequence_id || 0);
161
+ const targetCursor = normalize(payload.targetCursor || payload.target_cursor);
162
+ const parent =
163
+ Number.isFinite(targetSequence) && targetSequence > 0
164
+ ? `#${Math.floor(targetSequence)}`
165
+ : targetCursor
166
+ ? `cursor ${targetCursor}`
167
+ : "";
168
+ if (targetActionId) {
169
+ return parent ? `reply action ${targetActionId} under ${parent}` : `reply action ${targetActionId}`;
170
+ }
171
+ return parent || "target";
172
+ }
173
+
174
+ function actionBody(event) {
175
+ const payload = event && typeof event.payload === "object" ? event.payload : {};
176
+ const kind = normalize(event?.event || event?.type);
177
+ const actionType = normalize(payload.actionType || payload.action_type || kind.replace(/^session_/, ""));
178
+ const target = actionTargetLabel(payload);
179
+ const actionId = normalize(payload.actionId || payload.action_id);
180
+ const note = normalize(payload.note);
181
+ const metadata = [];
182
+ if (actionId) metadata.push(`Action ID: \`${actionId}\``);
183
+ if (actionType) metadata.push(`Action: \`${actionType}\``);
184
+ if (kind === "session_reply") {
185
+ return [
186
+ `**Reply to:** \`${target}\``,
187
+ ...metadata,
188
+ note ? "" : null,
189
+ note || normalize(payload.message),
190
+ ].filter((line) => line !== null && line !== "").join("\n");
191
+ }
192
+ if (kind === "session_reaction") {
193
+ return [
194
+ `**Reaction:** \`${actionType || "reaction"}\` on \`${target}\``,
195
+ ...metadata,
196
+ note ? `Note: ${note}` : null,
197
+ ].filter(Boolean).join("\n");
198
+ }
199
+ if (kind === "session_action") {
200
+ return [
201
+ `**Session action:** \`${actionType || "action"}\` on \`${target}\``,
202
+ ...metadata,
203
+ note ? "" : null,
204
+ note,
205
+ ].filter((line) => line !== null && line !== "").join("\n");
206
+ }
207
+ return "";
208
+ }
209
+
158
210
  function eventBody(event) {
211
+ const kind = normalize(event?.event || event?.type);
212
+ if (kind === "session_action" || kind === "session_reply" || kind === "session_reaction") {
213
+ return actionBody(event);
214
+ }
159
215
  const payload = event && typeof event.payload === "object" ? event.payload : {};
160
216
  // session_usage carries the response inside payload.response.text
161
217
  const responseText =