sentinelayer-cli 0.12.3 → 0.12.5

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.3",
3
+ "version": "0.12.5",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -7,7 +7,10 @@ export const PRICED_ACTIONS = Object.freeze([
7
7
  "audit_run",
8
8
  "audit_security",
9
9
  "audit_frontend",
10
+ "chat_ask",
10
11
  "omargate_deep",
12
+ "scan_precheck",
13
+ "spec_generate_ai",
11
14
  ]);
12
15
 
13
16
  const OMIT_METADATA_KEYS = new Set([
@@ -0,0 +1,91 @@
1
+ import { buildBillingRunId, buildCallIdempotencyKey, stableHash } from "./ledger-entry.js";
2
+ import { recordSessionUsage } from "./session-usage.js";
3
+
4
+ function normalizeString(value) {
5
+ return String(value || "").trim();
6
+ }
7
+
8
+ function nonNegativeInteger(value) {
9
+ const parsed = Number(value || 0);
10
+ if (!Number.isFinite(parsed) || parsed < 0) return 0;
11
+ return Math.floor(parsed);
12
+ }
13
+
14
+ export function usageNumber(value, fallback = 0) {
15
+ const parsed = Number(value);
16
+ if (Number.isFinite(parsed) && parsed >= 0) return parsed;
17
+ const parsedFallback = Number(fallback || 0);
18
+ return Number.isFinite(parsedFallback) && parsedFallback >= 0 ? parsedFallback : 0;
19
+ }
20
+
21
+ export async function recordCliLlmSessionUsage({
22
+ sessionId,
23
+ agentId,
24
+ action,
25
+ model,
26
+ inputTokens = 0,
27
+ outputTokens = 0,
28
+ startedAtIso = "",
29
+ targetPath,
30
+ billingTier = "internal",
31
+ sourceCommand = "",
32
+ provider = "",
33
+ metadata = {},
34
+ } = {}) {
35
+ const normalizedSessionId = normalizeString(sessionId);
36
+ const normalizedAgentId = normalizeString(agentId);
37
+ const normalizedAction = normalizeString(action);
38
+ const normalizedModel = normalizeString(model);
39
+ const createdAt = normalizeString(startedAtIso) || new Date().toISOString();
40
+ const safeInputTokens = nonNegativeInteger(inputTokens);
41
+ const safeOutputTokens = nonNegativeInteger(outputTokens);
42
+
43
+ if (!normalizedSessionId || !normalizedAgentId || !normalizedAction || !normalizedModel) {
44
+ return { ok: false, reason: "missing_session_usage_fields" };
45
+ }
46
+ if (safeInputTokens + safeOutputTokens <= 0) {
47
+ return { ok: false, reason: "zero_tokens" };
48
+ }
49
+
50
+ try {
51
+ const configHash = stableHash(
52
+ JSON.stringify({
53
+ action: normalizedAction,
54
+ agentId: normalizedAgentId,
55
+ model: normalizedModel,
56
+ provider: normalizeString(provider),
57
+ sourceCommand: normalizeString(sourceCommand),
58
+ metadata,
59
+ }),
60
+ );
61
+ const billingRunId = buildBillingRunId({
62
+ sessionId: normalizedSessionId,
63
+ invocationTimestamp: createdAt,
64
+ configHash,
65
+ });
66
+ return await recordSessionUsage(
67
+ normalizedSessionId,
68
+ {
69
+ agentId: normalizedAgentId,
70
+ action: normalizedAction,
71
+ model: normalizedModel,
72
+ inputTokens: safeInputTokens,
73
+ outputTokens: safeOutputTokens,
74
+ idempotencyKey: buildCallIdempotencyKey({ runId: billingRunId, callIndex: 0 }),
75
+ billingTier,
76
+ createdAt,
77
+ metadata: {
78
+ sourceCommand,
79
+ provider,
80
+ ...metadata,
81
+ },
82
+ },
83
+ { targetPath },
84
+ );
85
+ } catch (error) {
86
+ return {
87
+ ok: false,
88
+ reason: error instanceof Error ? error.message : String(error || "session_usage_failed"),
89
+ };
90
+ }
91
+ }
@@ -9,7 +9,9 @@ import {
9
9
  resolveModel,
10
10
  resolveProvider,
11
11
  } from "../ai/client.js";
12
+ import { recordCliLlmSessionUsage, usageNumber } from "../billing/llm-session-usage.js";
12
13
  import { resolveOutputRoot } from "../config/service.js";
14
+ import { estimateModelCost } from "../cost/tracker.js";
13
15
  import { estimateTokens } from "../cost/tokenizer.js";
14
16
 
15
17
  function shouldEmitJson(options, command) {
@@ -92,7 +94,9 @@ export function registerChatCommand(program) {
92
94
  });
93
95
 
94
96
  const startedAt = Date.now();
97
+ const startedAtIso = new Date(startedAt).toISOString();
95
98
  let responseText = "";
99
+ let invocation = null;
96
100
 
97
101
  if (options.dryRun) {
98
102
  responseText = `DRY_RUN_RESPONSE: ${prompt.slice(0, 240)}`;
@@ -100,7 +104,7 @@ export function registerChatCommand(program) {
100
104
  const streamEnabled = Boolean(options.stream);
101
105
  let streamedText = "";
102
106
  const client = createMultiProviderApiClient();
103
- const invocation = await client.invoke({
107
+ invocation = await client.invoke({
104
108
  provider,
105
109
  model,
106
110
  prompt,
@@ -125,8 +129,31 @@ export function registerChatCommand(program) {
125
129
 
126
130
  const durationMs = Date.now() - startedAt;
127
131
  const generatedAt = new Date().toISOString();
128
- const inputTokens = estimateTokens(prompt, { model });
129
- const outputTokens = estimateTokens(responseText, { model });
132
+ const estimatedInputTokens = estimateTokens(prompt, { model });
133
+ const estimatedOutputTokens = estimateTokens(responseText, { model });
134
+ const inputTokens = usageNumber(invocation?.usage?.inputTokens, estimatedInputTokens);
135
+ const outputTokens = usageNumber(invocation?.usage?.outputTokens, estimatedOutputTokens);
136
+ const modelCost = estimateModelCost({ modelId: invocation?.model || model, inputTokens, outputTokens });
137
+ const costUsd = usageNumber(invocation?.usage?.costUsd, modelCost.costUsd);
138
+
139
+ const sessionUsageLedger = options.dryRun
140
+ ? { ok: false, reason: "dry_run" }
141
+ : await recordCliLlmSessionUsage({
142
+ sessionId,
143
+ agentId: "chat-cli",
144
+ action: "chat_ask",
145
+ model: invocation?.model || model,
146
+ inputTokens,
147
+ outputTokens,
148
+ startedAtIso,
149
+ targetPath,
150
+ sourceCommand: "chat ask",
151
+ provider: invocation?.provider || provider,
152
+ metadata: {
153
+ streamed: Boolean(options.stream),
154
+ pricingFound: modelCost.pricingFound,
155
+ },
156
+ });
130
157
 
131
158
  await appendTranscriptEntries({
132
159
  transcriptPath,
@@ -154,8 +181,8 @@ export function registerChatCommand(program) {
154
181
  const payload = {
155
182
  command: "chat ask",
156
183
  sessionId,
157
- provider,
158
- model,
184
+ provider: invocation?.provider || provider,
185
+ model: invocation?.model || model,
159
186
  dryRun: Boolean(options.dryRun),
160
187
  streamed: Boolean(options.stream),
161
188
  transcriptPath,
@@ -165,8 +192,10 @@ export function registerChatCommand(program) {
165
192
  inputTokens,
166
193
  outputTokens,
167
194
  totalTokens: inputTokens + outputTokens,
195
+ costUsd,
168
196
  durationMs,
169
197
  },
198
+ billing: sessionUsageLedger,
170
199
  };
171
200
 
172
201
  if (emitJson) {
@@ -179,6 +208,6 @@ export function registerChatCommand(program) {
179
208
  }
180
209
  console.log(pc.gray(`session: ${sessionId}`));
181
210
  console.log(pc.gray(`transcript: ${transcriptPath}`));
182
- console.log(pc.gray(`usage: input=${inputTokens} output=${outputTokens} duration_ms=${durationMs}`));
211
+ console.log(pc.gray(`usage: input=${inputTokens} output=${outputTokens} cost_usd=${costUsd.toFixed(6)} duration_ms=${durationMs}`));
183
212
  });
184
213
  }
@@ -11,6 +11,7 @@ import {
11
11
  resolveModel,
12
12
  resolveProvider,
13
13
  } from "../ai/client.js";
14
+ import { recordCliLlmSessionUsage, usageNumber } from "../billing/llm-session-usage.js";
14
15
  import { loadConfig, resolveOutputRoot } from "../config/service.js";
15
16
  import { evaluateBudget } from "../cost/budget.js";
16
17
  import { appendCostEntry, summarizeCostHistory } from "../cost/history.js";
@@ -585,6 +586,7 @@ export function registerScanCommand(program) {
585
586
  profile,
586
587
  });
587
588
 
589
+ const startedAtIso = new Date().toISOString();
588
590
  const startedAtMs = Date.now();
589
591
  const client = createMultiProviderApiClient();
590
592
  const response = await client.invoke({
@@ -615,13 +617,16 @@ export function registerScanCommand(program) {
615
617
  await fsp.mkdir(path.dirname(reportPath), { recursive: true });
616
618
  await fsp.writeFile(reportPath, reportMarkdown, "utf-8");
617
619
 
618
- const inputTokens = estimateTokens(prompt, { model: response.model });
619
- const outputTokens = estimateTokens(aiMarkdown, { model: response.model });
620
+ const estimatedInputTokens = estimateTokens(prompt, { model: response.model });
621
+ const estimatedOutputTokens = estimateTokens(aiMarkdown, { model: response.model });
622
+ const inputTokens = usageNumber(response.usage?.inputTokens, estimatedInputTokens);
623
+ const outputTokens = usageNumber(response.usage?.outputTokens, estimatedOutputTokens);
620
624
  const modelCost = maybeEstimateModelCost({
621
625
  modelId: response.model,
622
626
  inputTokens,
623
627
  outputTokens,
624
628
  });
629
+ const costUsd = usageNumber(response.usage?.costUsd, modelCost.costUsd);
625
630
  const sessionId = String(options.sessionId || "scan-ai-precheck").trim() || "scan-ai-precheck";
626
631
 
627
632
  const appendedCost = await appendCostEntry(
@@ -639,7 +644,7 @@ export function registerScanCommand(program) {
639
644
  cacheWriteTokens: 0,
640
645
  durationMs,
641
646
  toolCalls: 1,
642
- costUsd: modelCost.costUsd,
647
+ costUsd,
643
648
  progressScore: aiMarkdown ? 1 : 0,
644
649
  }
645
650
  );
@@ -682,7 +687,7 @@ export function registerScanCommand(program) {
682
687
  outputTokens,
683
688
  cacheReadTokens: 0,
684
689
  cacheWriteTokens: 0,
685
- costUsd: modelCost.costUsd,
690
+ costUsd,
686
691
  durationMs,
687
692
  toolCalls: 1,
688
693
  },
@@ -727,9 +732,28 @@ export function registerScanCommand(program) {
727
732
  invocationId: appendedCost.entry.invocationId,
728
733
  },
729
734
  }
730
- );
735
+ );
731
736
  }
732
737
 
738
+ const sessionUsageLedger = await recordCliLlmSessionUsage({
739
+ sessionId,
740
+ agentId: "scan-precheck",
741
+ action: "scan_precheck",
742
+ model: response.model,
743
+ inputTokens,
744
+ outputTokens,
745
+ startedAtIso,
746
+ targetPath,
747
+ sourceCommand: "scan precheck",
748
+ provider: response.provider,
749
+ metadata: {
750
+ specPath,
751
+ profile,
752
+ policyPackId: activePolicy.selected?.id || "",
753
+ pricingFound: modelCost.pricingFound,
754
+ },
755
+ });
756
+
733
757
  const payload = {
734
758
  command: "scan precheck",
735
759
  targetPath,
@@ -749,10 +773,11 @@ export function registerScanCommand(program) {
749
773
  usage: {
750
774
  inputTokens,
751
775
  outputTokens,
752
- costUsd: modelCost.costUsd,
776
+ costUsd,
753
777
  durationMs,
754
778
  toolCalls: 1,
755
779
  },
780
+ billing: sessionUsageLedger,
756
781
  budget,
757
782
  cost: {
758
783
  filePath: appendedCost.filePath,
@@ -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.",
138
+ },
139
+ {
140
+ type: "dislike",
141
+ command: "sl session react <id> dislike --target-sequence <n>",
142
+ description: "Negative lightweight feedback.",
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;
@@ -1837,9 +1882,34 @@ export function registerSessionCommand(program) {
1837
1882
  return payload;
1838
1883
  }
1839
1884
 
1885
+ session
1886
+ .command("actions")
1887
+ .description("List supported low-noise message actions with examples")
1888
+ .option("--json", "Emit machine-readable output")
1889
+ .action((options, command) => {
1890
+ const payload = {
1891
+ command: "session actions",
1892
+ actions: SESSION_MESSAGE_ACTION_DESCRIPTIONS,
1893
+ };
1894
+ if (shouldEmitJson(options, command)) {
1895
+ console.log(JSON.stringify(payload, null, 2));
1896
+ return payload;
1897
+ }
1898
+ console.log(pc.bold("Supported session message actions"));
1899
+ for (const action of SESSION_MESSAGE_ACTION_DESCRIPTIONS) {
1900
+ const alias = action.alias ? ` (alias: ${action.alias})` : "";
1901
+ console.log(`${pc.cyan(action.type)}${alias}`);
1902
+ console.log(` ${action.description}`);
1903
+ console.log(pc.gray(` ${action.command}`));
1904
+ }
1905
+ return payload;
1906
+ });
1907
+
1840
1908
  session
1841
1909
  .command("action <sessionId> <actionType>")
1842
- .description("Create a message action for a target session event")
1910
+ .description(
1911
+ "Create a message action for a target session event (ack, working_on, reply/comment, like, dislike, disregard, view)",
1912
+ )
1843
1913
  .option("--target-sequence <n>", "Target event sequence id")
1844
1914
  .option("--target-cursor <cursor>", "Target event cursor")
1845
1915
  .option("--note <text>", "Optional action note or reply body")
@@ -1894,6 +1964,44 @@ export function registerSessionCommand(program) {
1894
1964
  });
1895
1965
  });
1896
1966
 
1967
+ session
1968
+ .command("comment <sessionId> <targetSequenceId> <message...>")
1969
+ .description("Alias for `session reply`; add a threaded comment to a target event")
1970
+ .option("--agent <id>", "Agent id for local idempotency metadata", "cli-user")
1971
+ .option("--idempotency-key <key>", "Explicit idempotency key")
1972
+ .option("--path <path>", "Workspace path for the session", ".")
1973
+ .option("--json", "Emit machine-readable output")
1974
+ .action(async (sessionId, targetSequenceId, messageParts, options, command) => {
1975
+ const message = Array.isArray(messageParts) ? messageParts.join(" ") : messageParts;
1976
+ await runMessageActionCommand({
1977
+ sessionId,
1978
+ actionType: "reply",
1979
+ options,
1980
+ command,
1981
+ commandName: "session comment",
1982
+ targetSequenceId: parsePositiveInteger(targetSequenceId, "targetSequenceId", 0),
1983
+ note: message,
1984
+ });
1985
+ });
1986
+
1987
+ session
1988
+ .command("view <sessionId> <targetSequenceId>")
1989
+ .description("Record a read receipt for a target session event")
1990
+ .option("--agent <id>", "Agent id for local idempotency metadata", "cli-user")
1991
+ .option("--idempotency-key <key>", "Explicit idempotency key")
1992
+ .option("--path <path>", "Workspace path for the session", ".")
1993
+ .option("--json", "Emit machine-readable output")
1994
+ .action(async (sessionId, targetSequenceId, options, command) => {
1995
+ await runMessageActionCommand({
1996
+ sessionId,
1997
+ actionType: "view",
1998
+ options,
1999
+ command,
2000
+ commandName: "session view",
2001
+ targetSequenceId: parsePositiveInteger(targetSequenceId, "targetSequenceId", 0),
2002
+ });
2003
+ });
2004
+
1897
2005
  session
1898
2006
  .command("listen")
1899
2007
  .description("Background-poll a session for events addressed to this agent or broadcast")
@@ -9,6 +9,7 @@ import {
9
9
  resolveModel,
10
10
  resolveProvider,
11
11
  } from "../ai/client.js";
12
+ import { recordCliLlmSessionUsage, usageNumber } from "../billing/llm-session-usage.js";
12
13
  import { loadConfig } from "../config/service.js";
13
14
  import { evaluateBudget } from "../cost/budget.js";
14
15
  import { appendCostEntry, summarizeCostHistory } from "../cost/history.js";
@@ -273,6 +274,7 @@ async function maybeEnhanceSpecWithAi({
273
274
  ingest,
274
275
  });
275
276
 
277
+ const startedAtIso = new Date().toISOString();
276
278
  const startedAtMs = Date.now();
277
279
  const client = createMultiProviderApiClient();
278
280
  const result = await client.invoke({
@@ -288,13 +290,16 @@ async function maybeEnhanceSpecWithAi({
288
290
  const normalizedText = String(result.text || "").trim();
289
291
  const enhancedMarkdown = normalizedText || baseSpecMarkdown;
290
292
 
291
- const inputTokens = estimateTokens(prompt, { model: result.model });
292
- const outputTokens = estimateTokens(enhancedMarkdown, { model: result.model });
293
+ const estimatedInputTokens = estimateTokens(prompt, { model: result.model });
294
+ const estimatedOutputTokens = estimateTokens(enhancedMarkdown, { model: result.model });
295
+ const inputTokens = usageNumber(result.usage?.inputTokens, estimatedInputTokens);
296
+ const outputTokens = usageNumber(result.usage?.outputTokens, estimatedOutputTokens);
293
297
  const modelCost = maybeEstimateModelCost({
294
298
  modelId: result.model,
295
299
  inputTokens,
296
300
  outputTokens,
297
301
  });
302
+ const costUsd = usageNumber(result.usage?.costUsd, modelCost.costUsd);
298
303
 
299
304
  const sessionId = String(options.sessionId || "spec-generate-ai").trim() || "spec-generate-ai";
300
305
  const appendedCost = await appendCostEntry(
@@ -312,7 +317,7 @@ async function maybeEnhanceSpecWithAi({
312
317
  cacheWriteTokens: 0,
313
318
  durationMs,
314
319
  toolCalls: 1,
315
- costUsd: modelCost.costUsd,
320
+ costUsd,
316
321
  progressScore: normalizedText ? 1 : 0,
317
322
  }
318
323
  );
@@ -355,7 +360,7 @@ async function maybeEnhanceSpecWithAi({
355
360
  outputTokens,
356
361
  cacheReadTokens: 0,
357
362
  cacheWriteTokens: 0,
358
- costUsd: modelCost.costUsd,
363
+ costUsd,
359
364
  durationMs,
360
365
  toolCalls: 1,
361
366
  },
@@ -403,6 +408,24 @@ async function maybeEnhanceSpecWithAi({
403
408
  );
404
409
  }
405
410
 
411
+ const sessionUsageLedger = await recordCliLlmSessionUsage({
412
+ sessionId,
413
+ agentId: "spec-generator",
414
+ action: "spec_generate_ai",
415
+ model: result.model,
416
+ inputTokens,
417
+ outputTokens,
418
+ startedAtIso,
419
+ targetPath,
420
+ sourceCommand: "spec generate --ai",
421
+ provider: result.provider,
422
+ metadata: {
423
+ template: template?.id || template?.name || "",
424
+ projectType: template?.projectType || "",
425
+ pricingFound: modelCost.pricingFound,
426
+ },
427
+ });
428
+
406
429
  return {
407
430
  markdown: enhancedMarkdown,
408
431
  ai: {
@@ -413,10 +436,11 @@ async function maybeEnhanceSpecWithAi({
413
436
  usage: {
414
437
  inputTokens,
415
438
  outputTokens,
416
- costUsd: modelCost.costUsd,
439
+ costUsd,
417
440
  durationMs,
418
441
  toolCalls: 1,
419
442
  },
443
+ billing: sessionUsageLedger,
420
444
  budget,
421
445
  cost: {
422
446
  filePath: appendedCost.filePath,
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
  "",