sentinelayer-cli 0.46.5 → 0.48.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.
Files changed (133) hide show
  1. package/README.md +47 -3
  2. package/package.json +12 -7
  3. package/skills/omar-gate-setup/SKILL.md +103 -0
  4. package/skills/senti-agent-identity/SKILL.md +80 -0
  5. package/skills/senti-judge/SKILL.md +139 -0
  6. package/skills/senti-room-notifications/SKILL.md +73 -0
  7. package/skills/senti-room-notifications/listen-wrapper.cjs +150 -0
  8. package/skills/senti-room-notifications/package.json +1 -0
  9. package/skills/senti-room-notifications/test-classifier.js +109 -0
  10. package/skills/senti-room-notifications/test-receipt.js +54 -0
  11. package/skills/sentinelayer-cli/SKILL.md +222 -0
  12. package/src/agents/devtestbot/runner.js +22 -11
  13. package/src/agents/devtestbot/tool.js +24 -11
  14. package/src/agents/jules/fix-cycle.js +7 -1
  15. package/src/agents/jules/tools/runtime-audit.js +17 -12
  16. package/src/agents/shared-tools/dispatch-core.js +10 -5
  17. package/src/ai/aidenid.js +4 -2
  18. package/src/ai/domain-target-store.js +11 -5
  19. package/src/ai/identity-store.js +11 -5
  20. package/src/ai/site-store.js +8 -4
  21. package/src/audit/orchestrator.js +21 -14
  22. package/src/audit/package.js +6 -4
  23. package/src/audit/persona-loop.js +7 -0
  24. package/src/audit/replay.js +4 -2
  25. package/src/auth/gate.js +32 -1
  26. package/src/auth/session-store.js +37 -0
  27. package/src/board/evidence-resolvers.js +3 -4
  28. package/src/cli.js +23 -3
  29. package/src/commands/ai/identity-lifecycle.js +30 -26
  30. package/src/commands/ai/provision-governance.js +32 -24
  31. package/src/commands/ai/shared.js +6 -4
  32. package/src/commands/audit.js +14 -12
  33. package/src/commands/board.js +2 -2
  34. package/src/commands/chat.js +8 -6
  35. package/src/commands/judge.js +125 -0
  36. package/src/commands/legacy-args.js +1 -0
  37. package/src/commands/mcp.js +45 -52
  38. package/src/commands/omargate.js +2 -1
  39. package/src/commands/plugin.js +11 -3
  40. package/src/commands/review.js +9 -4
  41. package/src/commands/scan.js +7 -7
  42. package/src/commands/session.js +516 -30
  43. package/src/commands/skills.js +45 -0
  44. package/src/commands/watch.js +9 -7
  45. package/src/config/output-filesystem.js +156 -0
  46. package/src/config/schema.js +4 -2
  47. package/src/config/service.js +40 -4
  48. package/src/cost/history.js +15 -9
  49. package/src/daemon/artifact-lineage.js +15 -12
  50. package/src/daemon/assignment-ledger.js +31 -25
  51. package/src/daemon/ast-drift.js +7 -3
  52. package/src/daemon/budget-governor.js +26 -20
  53. package/src/daemon/error-worker.js +28 -22
  54. package/src/daemon/fix-cycle.js +5 -4
  55. package/src/daemon/hybrid-mapper.js +19 -16
  56. package/src/daemon/ingest-refresh.js +4 -0
  57. package/src/daemon/jira-lifecycle.js +21 -17
  58. package/src/daemon/operator-control.js +18 -15
  59. package/src/daemon/reliability-lane.js +18 -15
  60. package/src/daemon/scope-engine.js +12 -8
  61. package/src/daemon/watchdog.js +15 -12
  62. package/src/engram/build-state.js +24 -3
  63. package/src/engram/capability-index.js +367 -0
  64. package/src/engram/compile-graph.js +1063 -0
  65. package/src/engram/contracts/action-receipt.js +82 -0
  66. package/src/engram/contracts/capability-index.js +259 -0
  67. package/src/engram/contracts/capability.js +377 -0
  68. package/src/engram/contracts/content-hash.js +8 -0
  69. package/src/engram/contracts/evidence.js +258 -0
  70. package/src/engram/contracts/graph.js +594 -0
  71. package/src/engram/contracts/identity.js +8 -0
  72. package/src/engram/contracts/index.js +149 -0
  73. package/src/engram/contracts/media-asset.js +85 -0
  74. package/src/engram/contracts/money.js +249 -0
  75. package/src/engram/contracts/source-snapshot.js +57 -0
  76. package/src/engram/contracts/task-context-pack.js +114 -0
  77. package/src/engram/contracts/temporal-segment.js +72 -0
  78. package/src/engram/contracts/versions.js +96 -0
  79. package/src/engram/extract-html.js +692 -0
  80. package/src/engram/graph-store.js +877 -0
  81. package/src/engram/index.js +98 -14
  82. package/src/engram/ingest-site.js +132 -0
  83. package/src/engram/live-state.js +1180 -0
  84. package/src/engram/namespace.js +34 -1
  85. package/src/engram/retrieve-site.js +753 -0
  86. package/src/engram/safe-reader.js +597 -0
  87. package/src/engram/source-intake.js +251 -0
  88. package/src/engram/store.js +227 -9
  89. package/src/ingest/engine.js +21 -28
  90. package/src/judge/verdict.js +305 -0
  91. package/src/legacy-cli.js +96 -19
  92. package/src/mcp/bridge-inputs.js +25 -4
  93. package/src/mcp/cli-command-tools.js +70 -0
  94. package/src/mcp/registry.js +17 -13
  95. package/src/mcp/session-stdio-server.js +297 -19
  96. package/src/memory/blackboard.js +5 -4
  97. package/src/net/gh-api-path.js +35 -9
  98. package/src/net/test-egress-guard.js +16 -4
  99. package/src/plugin/manifest.js +12 -8
  100. package/src/review/ai-review.js +110 -10
  101. package/src/review/investor-dd-devtestbot.js +17 -6
  102. package/src/review/investor-dd-orchestrator.js +21 -13
  103. package/src/review/local-review.js +26 -16
  104. package/src/review/omargate-cache.js +5 -5
  105. package/src/review/omargate-orchestrator.js +89 -17
  106. package/src/review/replay.js +9 -4
  107. package/src/review/report.js +22 -13
  108. package/src/review/typesafe-severity.js +201 -0
  109. package/src/session/admission-auth.js +34 -7
  110. package/src/session/admission.js +110 -15
  111. package/src/session/attachment-summary.js +77 -0
  112. package/src/session/edit-summary.js +19 -0
  113. package/src/session/invitations.js +65 -1
  114. package/src/session/mentions.js +17 -9
  115. package/src/session/message-edits.js +5 -4
  116. package/src/session/preview.js +10 -2
  117. package/src/session/recap.js +3 -2
  118. package/src/session/room-files.js +519 -0
  119. package/src/session/route-classes.js +22 -8
  120. package/src/session/setup-guides.js +4 -0
  121. package/src/session/standup.js +192 -0
  122. package/src/session/sync.js +229 -10
  123. package/src/session/tickets.js +41 -0
  124. package/src/session/transcript.js +2 -1
  125. package/src/session/wake/resolve-target.js +7 -3
  126. package/src/skills/listener-runtime.cjs +92 -0
  127. package/src/skills/service.js +207 -0
  128. package/src/swarm/factory.js +5 -5
  129. package/src/swarm/pentest.js +9 -9
  130. package/src/swarm/report.js +4 -4
  131. package/src/swarm/runtime.js +15 -12
  132. package/src/swarm/scenario-dsl.js +7 -5
  133. package/src/telemetry/ledger.js +16 -7
@@ -6,6 +6,8 @@ import { createHash, randomUUID } from "node:crypto";
6
6
  import { spawn as defaultSpawn } from "node:child_process";
7
7
 
8
8
  import pc from "picocolors";
9
+ import { appendAttachmentSummary, withAttachmentSummary } from "../session/attachment-summary.js";
10
+ import { formatEditSummary, withEditSummary } from "../session/edit-summary.js";
9
11
 
10
12
  import { credentialFor, isAuthenticated } from "../auth/credential-destinations.js";
11
13
  import { SentinelayerApiError, requestJsonMutation } from "../auth/http.js";
@@ -18,7 +20,7 @@ import {
18
20
  import { recordProvisionedIdentity } from "../ai/identity-store.js";
19
21
  import { fetchAidenIdCredentials } from "../auth/service.js";
20
22
  import { resolveActiveAuthSession } from "../auth/service.js";
21
- import { resolveOutputRoot } from "../config/service.js";
23
+ import { resolveOutputContext } from "../config/service.js";
22
24
  import {
23
25
  listAssignments,
24
26
  releaseLease,
@@ -62,6 +64,7 @@ import {
62
64
  setupSessionGuides,
63
65
  } from "../session/setup-guides.js";
64
66
  import { listSessionTasks } from "../session/tasks.js";
67
+ import { formatSessionStandup, sessionStandup } from "../session/standup.js";
65
68
  import {
66
69
  createSession,
67
70
  DEFAULT_TTL_SECONDS,
@@ -89,6 +92,8 @@ import {
89
92
  } from "../session/control-events.js";
90
93
  import {
91
94
  acceptSessionInvitation,
95
+ acceptPendingSessionInvitation,
96
+ listPendingSessionInvitations,
92
97
  normalizeSessionOnboarding,
93
98
  writeSessionOnboardingBrief,
94
99
  } from "../session/invitations.js";
@@ -119,6 +124,7 @@ import {
119
124
  } from "../session/reactions.js";
120
125
  import {
121
126
  claimTicket,
127
+ createTicket,
122
128
  listTickets,
123
129
  releaseTicketLease,
124
130
  renewTicketLease,
@@ -140,6 +146,7 @@ import {
140
146
  createSessionMessageAction,
141
147
  editSessionMessage,
142
148
  fetchJsonWithFullTimeout,
149
+ fetchSessionPresence,
143
150
  fetchSessionUsageLedger,
144
151
  fetchSessionPinnedMessages,
145
152
  listSessionMessageActions,
@@ -156,7 +163,15 @@ import {
156
163
  updateSessionReadCursor,
157
164
  } from "../session/sync.js";
158
165
  import { hydrateSessionFromRemote } from "../session/remote-hydrate.js";
159
- import { messageRevision, replyRevisionEvent, sessionMessageRoutingEvent } from "../session/message-edits.js";
166
+ import {
167
+ DEFAULT_ROOM_FILES_LIMIT,
168
+ DEFAULT_ROOM_FILES_MAX_PAGES,
169
+ MAX_ROOM_FILES_LIMIT,
170
+ RoomFileError,
171
+ downloadRoomFile,
172
+ listRoomFiles,
173
+ } from "../session/room-files.js";
174
+ import { messageRevision, replyRevisionEvent } from "../session/message-edits.js";
160
175
  import { runSessionRecall } from "../session/recall/index.js";
161
176
  import { mergeLiveSources } from "../session/live-source.js";
162
177
  import { listenSessionEvents } from "../session/listener.js";
@@ -854,7 +869,7 @@ function formatHumanAskLine(event = {}) {
854
869
  const message = humanAskMessage(event);
855
870
  const shortMessage = message.length > 220 ? `${message.slice(0, 217)}...` : message;
856
871
  const suffix = shortMessage ? `: ${shortMessage}` : "";
857
- return `${sequenceLabel} ${humanAskAgentId(event)}${ts ? ` ${ts}` : ""}${suffix}`;
872
+ return appendAttachmentSummary(`${sequenceLabel} ${humanAskAgentId(event)}${ts ? ` ${ts}` : ""}${suffix}`, event);
858
873
  }
859
874
 
860
875
  function formatRemoteHydrateAppendWarning(result = {}) {
@@ -1460,6 +1475,7 @@ async function fetchRemoteSessionDetail(endpoint, credential) {
1460
1475
  endpoint, { method: "GET", credential, redirect: "error" }, 2_000,
1461
1476
  ));
1462
1477
  } catch (err) {
1478
+ if (err?.code === "INVITATION_PENDING") throw err;
1463
1479
  return {
1464
1480
  ok: false,
1465
1481
  response: null,
@@ -1986,6 +2002,7 @@ async function publishListenerPresenceEvent({
1986
2002
  clientKind = "cli",
1987
2003
  listenerId,
1988
2004
  lifecycle = {},
2005
+ minimalPresence = false,
1989
2006
  } = {}) {
1990
2007
  const normalizedType = normalizeString(lifecycle.type) || "heartbeat";
1991
2008
  const identity = inferSessionAgentIdentity({
@@ -1998,6 +2015,7 @@ async function publishListenerPresenceEvent({
1998
2015
  return renewSessionPresence(sessionId, {
1999
2016
  targetPath,
2000
2017
  agentId,
2018
+ minimalPresence,
2001
2019
  state:
2002
2020
  normalizedType === "stopped"
2003
2021
  ? "stopped"
@@ -2440,14 +2458,29 @@ function resolveSessionIdOption(options = {}) {
2440
2458
  return sessionId;
2441
2459
  }
2442
2460
 
2461
+ function formatFileSize(bytes) {
2462
+ const value = Number(bytes) || 0;
2463
+ if (value < 1024) return `${value} B`;
2464
+ const units = ["KB", "MB", "GB"];
2465
+ let size = value / 1024;
2466
+ let unit = 0;
2467
+ while (size >= 1024 && unit < units.length - 1) {
2468
+ size /= 1024;
2469
+ unit += 1;
2470
+ }
2471
+ return `${size.toFixed(size < 10 ? 1 : 0)} ${units[unit]}`;
2472
+ }
2473
+
2443
2474
  function formatEventLine(event = {}) {
2475
+ return appendAttachmentSummary(formatEventText(event), event);
2476
+ }
2477
+
2478
+ function formatEventText(event = {}) {
2444
2479
  const ts = normalizeString(event.ts || event.timestamp);
2445
2480
  const type = normalizeString(event.event || event.type) || "event";
2446
2481
  const agentId = normalizeString(event.agent?.id || event.agentId || "unknown");
2447
2482
  if (type === "session_message_edited") {
2448
- const routed = sessionMessageRoutingEvent(event);
2449
- const target = event.payload?.targetActionId ? `reply ${event.payload.targetActionId}` : `#${event.payload?.targetSequenceId || "?"}`;
2450
- return `${ts} ${agentId} ${type} ${target} revision ${event.payload?.messageRevision || "?"}: ${normalizeString(routed.payload?.message || routed.payload?.text || "")}`;
2483
+ return `${ts} ${agentId} ${formatEditSummary(event)}`;
2451
2484
  }
2452
2485
  const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
2453
2486
  if (type === "session_action" || type === "session_reply" || type === "session_reaction") {
@@ -2904,11 +2937,69 @@ async function sessionListenAgent(options) {
2904
2937
  return { sessionId: resolveSessionIdOption(options), agentId: normalizeAgentId(options.agent, "cli-user") };
2905
2938
  }
2906
2939
 
2940
+
2941
+ /**
2942
+ * Build the admission-join options object from parsed Commander options.
2943
+ * Extracted so tests can assert the full derivation from CLI flag to
2944
+ * runAdmissionJoin input without network I/O.
2945
+ */
2946
+ export function buildAdmissionJoinOptions(options, overrides = {}) {
2947
+ const waitSeconds = Number.parseInt(String(options.waitSeconds ?? "900"), 10);
2948
+ return {
2949
+ agentId: overrides.agentId ?? "",
2950
+ displayName: overrides.displayName ?? "",
2951
+ model: options.model,
2952
+ provider: options.provider,
2953
+ goal: options.goal,
2954
+ deliverables: options.deliverable || [],
2955
+ stopConditions: options.stopWhen || [],
2956
+ actions: parseAdmissionScope(options.scope),
2957
+ ttlSeconds: parseAdmissionTtlSeconds(options.ttl),
2958
+ wait: options.wait !== false,
2959
+ recoverActive: Boolean(options.recoverAdmission),
2960
+ noAutoGrant: options.autoGrant === false,
2961
+ waitTimeoutMs: (Number.isFinite(waitSeconds) && waitSeconds >= 0 ? waitSeconds : 900) * 1000,
2962
+ targetPath: overrides.targetPath ?? process.cwd(),
2963
+ onPending: overrides.onPending ?? (() => {}),
2964
+ };
2965
+ }
2966
+
2907
2967
  export function registerSessionCommand(program) {
2908
2968
  const session = program
2909
2969
  .command("session")
2910
2970
  .description("Multi-agent ephemeral coordination sessions");
2911
2971
 
2972
+ const invitations = session.command("invitations").description("List and accept pending room invitations");
2973
+ invitations
2974
+ .command("list")
2975
+ .description("List your pending room invitations")
2976
+ .option("--path <path>", "Workspace path for authentication", ".")
2977
+ .option("--json", "Emit the raw API response")
2978
+ .action(async (options, command) => {
2979
+ const result = await listPendingSessionInvitations({ targetPath: path.resolve(options.path) });
2980
+ if (shouldEmitJson(options, command)) {
2981
+ console.log(JSON.stringify(result, null, 2));
2982
+ return;
2983
+ }
2984
+ const entries = Array.isArray(result) ? result : result?.invitations;
2985
+ if (!Array.isArray(entries) || entries.some((entry) => !normalizeString(entry?.invitationId))) {
2986
+ console.log("Pending invitations retrieved. Use `sl session invitations list --json` to inspect the response.");
2987
+ return;
2988
+ }
2989
+ if (entries.length === 0) console.log("No pending invitations.");
2990
+ for (const entry of entries) console.log(`Invitation: ${entry.invitationId}`);
2991
+ });
2992
+ invitations
2993
+ .command("accept <invitationId>")
2994
+ .description("Accept a pending invitation using its invitationId from invitations list")
2995
+ .option("--path <path>", "Workspace path for authentication", ".")
2996
+ .option("--json", "Emit the raw API response")
2997
+ .action(async (invitationId, options, command) => {
2998
+ const result = await acceptPendingSessionInvitation(invitationId, { targetPath: path.resolve(options.path) });
2999
+ if (shouldEmitJson(options, command)) console.log(JSON.stringify(result, null, 2));
3000
+ else console.log("Invitation accepted.");
3001
+ });
3002
+
2912
3003
  session
2913
3004
  .command("start")
2914
3005
  .description(
@@ -3439,6 +3530,7 @@ export function registerSessionCommand(program) {
3439
3530
  .option("--wait-seconds <n>", "How long to wait for approval before returning pending", "900")
3440
3531
  .option("--cancel-admission", "Cancel this workspace's pending admission request for the session")
3441
3532
  .option("--recover-admission", "Recover a live admission with the retained agent key; requires the unchanged --goal/scope/ttl")
3533
+ .option("--no-auto-grant", "Skip auto-grant on 403 IDENTITY_FORGERY; fail immediately instead")
3442
3534
  .option("--json", "Emit machine-readable output")
3443
3535
  .action(async (sessionId, options, command) => {
3444
3536
  const normalizedSessionId = normalizeString(sessionId);
@@ -3527,20 +3619,9 @@ export function registerSessionCommand(program) {
3527
3619
  if (!admissionAgent) {
3528
3620
  throw new Error("--goal requires --agent <id>: admission is requested for one named agent.");
3529
3621
  }
3530
- const waitSeconds = Number.parseInt(String(options.waitSeconds ?? "900"), 10);
3531
- admissionJoinOptions = {
3622
+ admissionJoinOptions = buildAdmissionJoinOptions(options, {
3532
3623
  agentId: admissionAgent,
3533
3624
  displayName: legacyName && explicitAgent ? legacyName : "",
3534
- model: options.model,
3535
- provider: options.provider,
3536
- goal: options.goal,
3537
- deliverables: options.deliverable || [],
3538
- stopConditions: options.stopWhen || [],
3539
- actions: parseAdmissionScope(options.scope),
3540
- ttlSeconds: parseAdmissionTtlSeconds(options.ttl),
3541
- wait: options.wait !== false,
3542
- recoverActive: Boolean(options.recoverAdmission),
3543
- waitTimeoutMs: (Number.isFinite(waitSeconds) && waitSeconds >= 0 ? waitSeconds : 900) * 1000,
3544
3625
  targetPath,
3545
3626
  onPending: ({ approveUrl, phase }) => {
3546
3627
  // stderr, so --json stdout stays a single parseable document.
@@ -3555,7 +3636,7 @@ export function registerSessionCommand(program) {
3555
3636
  else if (phase === "identity") console.log(pc.yellow(line));
3556
3637
  else console.log(line);
3557
3638
  },
3558
- };
3639
+ });
3559
3640
  admission = await runAdmissionJoin(normalizedSessionId, admissionJoinOptions);
3560
3641
  if (admission.status !== "active") {
3561
3642
  // Only an ACTIVE admission joins. Pending, denied, cancelled or expired never
@@ -3884,6 +3965,7 @@ export function registerSessionCommand(program) {
3884
3965
  .option("--stdin", "Read the message body from stdin")
3885
3966
  .option("--force-cli-user", "Allow fallback sends as cli-user when no agent identity can be resolved")
3886
3967
  .option("--local-only", "Append only to the local session cache without remote send confirmation")
3968
+ .option("--no-auto-grant", "Skip auto-grant on 403 IDENTITY_FORGERY; fail immediately instead")
3887
3969
  .option("--path <path>", "Workspace path for the session", ".")
3888
3970
  .option("--json", "Emit machine-readable output")
3889
3971
  .action(asAdmittedAgent(sessionSayAgent, async (sessionId, messageParts, options, command) => {
@@ -3962,6 +4044,7 @@ export function registerSessionCommand(program) {
3962
4044
  for (let attempt = 0; attempt < 2; attempt += 1) {
3963
4045
  remoteSync = await syncSessionEventToApi(normalizedSessionId, event, {
3964
4046
  targetPath,
4047
+ noAutoGrant: options.autoGrant === false,
3965
4048
  });
3966
4049
  if (remoteSync?.synced) break;
3967
4050
  }
@@ -4024,6 +4107,7 @@ export function registerSessionCommand(program) {
4024
4107
  .option("--display-name <name>", "Human-readable agent display name")
4025
4108
  .option("--role <role>", "Agent role metadata: coder, reviewer, tester, observer", "coder")
4026
4109
  .option("--to <agent>", "Direct the message to a specific agent id")
4110
+ .option("--no-auto-grant", "Skip auto-grant on 403 IDENTITY_FORGERY; fail immediately instead")
4027
4111
  .option("--path <path>", "Workspace path for the session", ".")
4028
4112
  .option("--json", "Emit machine-readable output")
4029
4113
  .action(async (sessionId, message, options, command) => {
@@ -4093,6 +4177,7 @@ export function registerSessionCommand(program) {
4093
4177
  }
4094
4178
  const remoteSync = await syncSessionEventToApi(normalizedSessionId, event, {
4095
4179
  targetPath,
4180
+ noAutoGrant: options.autoGrant === false,
4096
4181
  });
4097
4182
  if (!remoteSync?.synced) {
4098
4183
  throw new Error(
@@ -4160,6 +4245,7 @@ export function registerSessionCommand(program) {
4160
4245
  .option("--proposal <text>", "Suggested remediation or next step")
4161
4246
  .option("--message-file <path>", "Read the observation summary from a UTF-8 file")
4162
4247
  .option("--stdin", "Read the observation summary from stdin")
4248
+ .option("--no-auto-grant", "Skip auto-grant on 403 IDENTITY_FORGERY; fail immediately instead")
4163
4249
  .option("--path <path>", "Workspace path for the session", ".")
4164
4250
  .option("--json", "Emit machine-readable output")
4165
4251
  .action(async (sessionId, summaryParts, options, command) => {
@@ -4265,6 +4351,7 @@ export function registerSessionCommand(program) {
4265
4351
  }
4266
4352
  const remoteSync = await syncSessionEventToApi(normalizedSessionId, event, {
4267
4353
  targetPath,
4354
+ noAutoGrant: options.autoGrant === false,
4268
4355
  });
4269
4356
  if (!remoteSync?.synced) {
4270
4357
  throw new Error(
@@ -4749,6 +4836,132 @@ export function registerSessionCommand(program) {
4749
4836
  return payload;
4750
4837
  });
4751
4838
 
4839
+ const filesCommand = session
4840
+ .command("files <sessionId>")
4841
+ .description(
4842
+ "List the files published in a room: name, size, type, who uploaded it and when, and the attachment id to download it with",
4843
+ )
4844
+ .option("--limit <n>", `Most files to list (default ${DEFAULT_ROOM_FILES_LIMIT}, max ${MAX_ROOM_FILES_LIMIT})`)
4845
+ .option(
4846
+ "--max-pages <n>",
4847
+ `Most pages of room history to read, 200 events each (default ${DEFAULT_ROOM_FILES_MAX_PAGES})`,
4848
+ )
4849
+ .option("--path <path>", "Workspace path for the session", ".")
4850
+ .option("--json", "Emit machine-readable output")
4851
+ .addHelpText(
4852
+ "after",
4853
+ "\nDownload a file with: sl session files get <sessionId> <attachmentId> [--out <path>] [--force]",
4854
+ )
4855
+ .action(async (sessionId, options, command) => {
4856
+ const normalizedSessionId = normalizeString(sessionId);
4857
+ if (!normalizedSessionId) {
4858
+ throw new Error("session id is required.");
4859
+ }
4860
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
4861
+ const limit = parsePositiveInteger(options.limit, "limit", DEFAULT_ROOM_FILES_LIMIT);
4862
+ const maxPages = parsePositiveInteger(options.maxPages, "max-pages", DEFAULT_ROOM_FILES_MAX_PAGES);
4863
+ const result = await listRoomFiles(normalizedSessionId, { targetPath, limit, maxPages });
4864
+ if (!result.ok) {
4865
+ throw new Error(`Could not list room files (${result.reason || "unknown"}).`);
4866
+ }
4867
+ const payload = {
4868
+ command: "session files",
4869
+ sessionId: normalizedSessionId,
4870
+ count: result.count,
4871
+ complete: result.complete,
4872
+ pages: result.pages,
4873
+ scannedEvents: result.scannedEvents,
4874
+ files: result.files,
4875
+ };
4876
+ if (shouldEmitJson(options, command)) {
4877
+ console.log(JSON.stringify(payload, null, 2));
4878
+ return payload;
4879
+ }
4880
+ if (!result.count) {
4881
+ console.log(
4882
+ pc.gray(
4883
+ result.complete
4884
+ ? "No files have been published in this room."
4885
+ : `No files in the last ${result.scannedEvents} events; raise --max-pages to read further back.`,
4886
+ ),
4887
+ );
4888
+ return payload;
4889
+ }
4890
+ console.log(pc.bold(`Files published in ${normalizedSessionId} (${result.count}, newest first)`));
4891
+ for (const file of result.files) {
4892
+ const type = file.contentType || file.kind || "unknown type";
4893
+ const who = file.uploadedBy ? ` by ${file.uploadedBy}` : "";
4894
+ const when = file.uploadedAt || file.publishedAt;
4895
+ const message = file.publishedSequenceId ? ` in #${file.publishedSequenceId}` : "";
4896
+ console.log(`${file.filename || "(unnamed)"} ${formatFileSize(file.sizeBytes)} ${type}${who}${when ? ` ${when}` : ""}${message}`);
4897
+ console.log(pc.gray(` attachment id ${file.attachmentId}`));
4898
+ }
4899
+ if (!result.complete) {
4900
+ console.log(pc.gray("More files may exist further back; raise --limit or --max-pages."));
4901
+ }
4902
+ console.log(pc.gray(`Download one with: sl session files get ${normalizedSessionId} <attachmentId> [--out <path>]`));
4903
+ return payload;
4904
+ });
4905
+
4906
+ filesCommand
4907
+ .command("get <sessionId> <attachmentId>")
4908
+ .description(
4909
+ "Download one published room file: streamed to disk, checked against the server's sha256, and written atomically",
4910
+ )
4911
+ .option("--out <path>", "File or directory to write to (default: the server's file name in the workspace)")
4912
+ .option("--force", "Overwrite an existing file")
4913
+ .option("--path <path>", "Workspace path; a relative --out must stay inside it", ".")
4914
+ .option("--json", "Emit machine-readable output")
4915
+ .addHelpText(
4916
+ "after",
4917
+ "\nThe destination folder is checked when the download starts; this does not protect against another local process renaming or relinking folders during the download.",
4918
+ )
4919
+ .action(async (sessionId, attachmentId, options, command) => {
4920
+ const normalizedSessionId = normalizeString(sessionId);
4921
+ const normalizedAttachmentId = normalizeString(attachmentId);
4922
+ if (!normalizedSessionId) {
4923
+ throw new Error("session id is required.");
4924
+ }
4925
+ // `files` also defines --path and --json, so Commander gives them to it: read the merged values.
4926
+ const merged = typeof command?.optsWithGlobals === "function" ? command.optsWithGlobals() : options;
4927
+ const targetPath = path.resolve(process.cwd(), String(merged.path || options.path || "."));
4928
+ let result;
4929
+ try {
4930
+ result = await downloadRoomFile(normalizedSessionId, normalizedAttachmentId, {
4931
+ targetPath,
4932
+ out: options.out,
4933
+ force: Boolean(options.force),
4934
+ });
4935
+ } catch (error) {
4936
+ if (error instanceof RoomFileError) {
4937
+ throw new Error(error.message);
4938
+ }
4939
+ throw error;
4940
+ }
4941
+ const payload = {
4942
+ command: "session files get",
4943
+ sessionId: normalizedSessionId,
4944
+ attachmentId: normalizedAttachmentId,
4945
+ path: result.path,
4946
+ bytes: result.bytes,
4947
+ sha256: result.sha256,
4948
+ verified: result.verified,
4949
+ overwritten: result.overwritten,
4950
+ linkRefetched: result.linkRefetched,
4951
+ attachment: result.attachment,
4952
+ };
4953
+ if (shouldEmitJson(options, command)) {
4954
+ console.log(JSON.stringify(payload, null, 2));
4955
+ return payload;
4956
+ }
4957
+ console.log(
4958
+ pc.green(
4959
+ `Saved ${result.attachment.filename || normalizedAttachmentId} (${formatFileSize(result.bytes)}, sha256 verified) to ${result.path}`,
4960
+ ),
4961
+ );
4962
+ return payload;
4963
+ });
4964
+
4752
4965
  const runFileLockCommand = async (verb, sessionId, files, options, command) => {
4753
4966
  const normalizedSessionId = normalizeString(sessionId);
4754
4967
  if (!normalizedSessionId) {
@@ -5395,6 +5608,7 @@ export function registerSessionCommand(program) {
5395
5608
  "--no-presence",
5396
5609
  "Do not renew ephemeral remote presence",
5397
5610
  )
5611
+ .option("--presence-check", "Check once whether the server lists this agent after the first presence renewal")
5398
5612
  .option(
5399
5613
  "--model <model>",
5400
5614
  "Model/provider label to publish with listener presence",
@@ -5651,6 +5865,29 @@ export function registerSessionCommand(program) {
5651
5865
  let lastPresenceHeartbeatMs = 0;
5652
5866
  let lastPresenceHeartbeatFingerprint = "";
5653
5867
  let nextPresenceAttemptMs = 0;
5868
+ let minimalPresence = false;
5869
+ let presenceCheckPending = Boolean(options.presenceCheck);
5870
+ const checkPresence = async () => {
5871
+ if (!presenceCheckPending) return;
5872
+ presenceCheckPending = false;
5873
+ const roster = await fetchSessionPresence(normalizedSessionId, { targetPath });
5874
+ const present = roster.ok
5875
+ ? roster.present.some((entry) => entry.agentId === agentId)
5876
+ : null;
5877
+ if (emitFormat === "ndjson") {
5878
+ console.log(JSON.stringify(createAgentEvent({
5879
+ event: "session_presence_check",
5880
+ agentId,
5881
+ sessionId: normalizedSessionId,
5882
+ payload: { present, reason: roster.reason || null },
5883
+ })));
5884
+ } else if (present !== null) {
5885
+ console.log(`Presence check: ${agentId} is ${present ? "listed" : "not listed"} in session ${normalizedSessionId}.`);
5886
+ }
5887
+ if (present === null) {
5888
+ console.error(`Presence check: ${agentId} presence is unknown (${roster.reason || "presence_read_failed"}).`);
5889
+ }
5890
+ };
5654
5891
 
5655
5892
  // Periodic in-session success reminders (ack, claim work, reply
5656
5893
  // in-thread). Long-running interactive listeners only — skipped under
@@ -5764,7 +6001,7 @@ export function registerSessionCommand(program) {
5764
6001
  },
5765
6002
  onEvent: async (event) => {
5766
6003
  if (emitFormat === "ndjson") {
5767
- console.log(JSON.stringify(event));
6004
+ console.log(JSON.stringify(withEditSummary(withAttachmentSummary(event))));
5768
6005
  } else {
5769
6006
  console.log(formatEventLine(event));
5770
6007
  }
@@ -5826,7 +6063,10 @@ export function registerSessionCommand(program) {
5826
6063
  );
5827
6064
  }
5828
6065
  }
5829
- if (!publishPresence) return;
6066
+ if (!publishPresence) {
6067
+ await checkPresence();
6068
+ return;
6069
+ }
5830
6070
  const lifecycleType = normalizeString(lifecycle?.type);
5831
6071
  const presenceAttemptMs = Date.now();
5832
6072
  if (lifecycleType !== "stopped" && presenceAttemptMs < nextPresenceAttemptMs) {
@@ -5853,7 +6093,7 @@ export function registerSessionCommand(program) {
5853
6093
  lastPresenceHeartbeatFingerprint = heartbeatDecision.fingerprint;
5854
6094
  lastPresenceHeartbeatMs = nowMs;
5855
6095
  }
5856
- const presenceResult = await publishListenerPresenceEvent({
6096
+ const presenceOptions = {
5857
6097
  sessionId: normalizedSessionId,
5858
6098
  targetPath,
5859
6099
  agentId,
@@ -5863,7 +6103,18 @@ export function registerSessionCommand(program) {
5863
6103
  clientKind,
5864
6104
  listenerId,
5865
6105
  lifecycle: publishLifecycle,
5866
- });
6106
+ minimalPresence,
6107
+ };
6108
+ let presenceResult = await publishListenerPresenceEvent(presenceOptions);
6109
+ if (!minimalPresence && presenceResult?.reason === "api_403" && presenceResult?.code === "AGENT_IDENTITY_REQUIRED") {
6110
+ minimalPresence = true;
6111
+ console.error("Presence renewal refused (403 AGENT_IDENTITY_REQUIRED; runtime/working require scoped agent credentials); retrying with agentId and model only for this listener session.");
6112
+ presenceResult = await publishListenerPresenceEvent({ ...presenceOptions, minimalPresence });
6113
+ }
6114
+ if (!presenceResult?.ok) {
6115
+ console.error(`Presence renewal failed for ${agentId} (${presenceResult?.reason || "not_recorded"}); the server may not list this listener.`);
6116
+ }
6117
+ await checkPresence();
5867
6118
  if (presenceResult?.ok) {
5868
6119
  nextPresenceAttemptMs = 0;
5869
6120
  return;
@@ -6664,11 +6915,11 @@ export function registerSessionCommand(program) {
6664
6915
  tail,
6665
6916
  beforeSequence,
6666
6917
  count: events.length,
6667
- events,
6918
+ events: events.map(withAttachmentSummary).map(withEditSummary),
6668
6919
  includeControlEvents,
6669
6920
  hiddenControlEventCount,
6670
6921
  unacknowledgedHumanMessageCount: unacknowledgedHumanMessages.length,
6671
- unacknowledgedHumanMessages,
6922
+ unacknowledgedHumanMessages: unacknowledgedHumanMessages.map(withAttachmentSummary).map(withEditSummary),
6672
6923
  recentHumanActivityCount: recentHumanActivity.length,
6673
6924
  recentHumanActivity,
6674
6925
  autoView,
@@ -6770,7 +7021,7 @@ export function registerSessionCommand(program) {
6770
7021
  if (item.event) {
6771
7022
  if (!includeControlEvents && isSessionControlEvent(item.event)) continue;
6772
7023
  if (emitJson) {
6773
- console.log(JSON.stringify({ source: item.source, event: item.event }));
7024
+ console.log(JSON.stringify({ source: item.source, event: withEditSummary(withAttachmentSummary(item.event)) }));
6774
7025
  } else {
6775
7026
  const sourceTag = item.source === "sse" ? pc.cyan("[sse]") : pc.gray("[fs] ");
6776
7027
  console.log(`${sourceTag} ${formatEventLine(item.event)}`);
@@ -6801,7 +7052,7 @@ export function registerSessionCommand(program) {
6801
7052
  continue;
6802
7053
  }
6803
7054
  if (emitJson) {
6804
- console.log(JSON.stringify(event));
7055
+ console.log(JSON.stringify(withEditSummary(withAttachmentSummary(event))));
6805
7056
  } else {
6806
7057
  console.log(formatEventLine(event));
6807
7058
  }
@@ -6859,6 +7110,170 @@ export function registerSessionCommand(program) {
6859
7110
  }
6860
7111
  });
6861
7112
 
7113
+ session
7114
+ .command("events <sessionId>")
7115
+ .description("Return one raw, verifiable page of session events from the remote API (ascending by sequence, no local merge). Use --before-sequence to walk backwards.")
7116
+ .option("--remote", "Fetch from the SentinelLayer API (required; this command is remote-only)")
7117
+ .option("--before-sequence <n>", "Return events older than this sequence id (omit for newest)")
7118
+ .option("--limit <n>", "Maximum events (1-200, default 50)", "50")
7119
+ .option("--path <path>", "Workspace path for the session", ".")
7120
+ .option("--no-auto-grant", "Accepted, has no effect (session events is read-only; reads never auto-grant)")
7121
+ .option("--json", "Emit machine-readable output")
7122
+ .action(async (sessionId, options, command) => {
7123
+ const normalizedSessionId = normalizeString(sessionId);
7124
+ if (!normalizedSessionId) {
7125
+ throw new Error("session id is required.");
7126
+ }
7127
+ if (!options.remote) {
7128
+ throw new Error("session events requires --remote (this command is remote-only; use 'session read' for local streams).");
7129
+ }
7130
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
7131
+ const beforeSequence = parseOptionalPositiveInteger(options.beforeSequence, "before-sequence");
7132
+ const rawLimit = parsePositiveInteger(options.limit, "limit", 50);
7133
+ if (rawLimit > 200) {
7134
+ throw new Error("limit must be between 1 and 200.");
7135
+ }
7136
+ const limit = Math.max(1, rawLimit);
7137
+ const emitJson = shouldEmitJson(options, command);
7138
+
7139
+ const authSession = await resolveActiveAuthSession({
7140
+ cwd: targetPath,
7141
+ env: process.env,
7142
+ autoRotate: false,
7143
+ });
7144
+ if (!isAuthenticated(authSession)) {
7145
+ throw new Error(`Remote session events requires authentication. Run \`${authLoginHint()}\` first.`);
7146
+ }
7147
+
7148
+ const result = await pollSessionEventsBefore(normalizedSessionId, {
7149
+ targetPath,
7150
+ beforeSequence,
7151
+ limit,
7152
+ timeoutMs: 15_000,
7153
+ });
7154
+
7155
+ const contractViolation = pageContractViolation(normalizedSessionId, beforeSequence, limit, result);
7156
+ const rawNext = result?.rawNextBeforeSequence;
7157
+
7158
+ const payload = {
7159
+ command: "session events",
7160
+ targetPath,
7161
+ sessionId: normalizedSessionId,
7162
+ ok: result.ok && !contractViolation,
7163
+ beforeSequence: beforeSequence || null,
7164
+ count: Array.isArray(result.events) ? result.events.length : 0,
7165
+ nextBeforeSequence: rawNext ?? null,
7166
+ hasMore: Boolean(result.hasMore),
7167
+ partial: Boolean(result.partial),
7168
+ events: Array.isArray(result.events) ? result.events : [],
7169
+ reason: contractViolation ? "page_contract_violation" : (result.reason || ""),
7170
+ contractViolation: contractViolation || undefined,
7171
+ };
7172
+
7173
+ // C3: non-zero exit on failure in --json mode
7174
+ if (emitJson && !payload.ok) {
7175
+ process.exitCode = 1;
7176
+ console.log(JSON.stringify(payload, null, 2));
7177
+ return;
7178
+ }
7179
+
7180
+ if (emitJson) {
7181
+ console.log(JSON.stringify(payload, null, 2));
7182
+ return;
7183
+ }
7184
+ if (!payload.ok) {
7185
+ if (contractViolation) {
7186
+ throw new Error(`Session events page contract violated: ${contractViolation}`);
7187
+ }
7188
+ throw new Error(`Session events fetch failed (${result.reason || "unknown"}).`);
7189
+ }
7190
+ for (const event of result.events || []) {
7191
+ const seq = Number(event.sequenceId || event.sequence_id || 0);
7192
+ const seqLabel = Number.isFinite(seq) && seq > 0 ? `#${Math.floor(seq)}` : "";
7193
+ console.log(`${seqLabel} ${formatEventLine(event)}`);
7194
+ }
7195
+ if (result.beforeSequence && result.events && result.events.length >= limit) {
7196
+ console.log(pc.gray(`More events before sequence ${result.beforeSequence}.`));
7197
+ }
7198
+ });
7199
+
7200
+
7201
+ function pageContractViolation(requestedSessionId, beforeSequence, limit, result) {
7202
+ if (!result || !result.ok) return null;
7203
+
7204
+ const rawEvents = result.rawEvents;
7205
+ const rawNextBeforeSequence = result.rawNextBeforeSequence;
7206
+ const hasMore = result.hasMore;
7207
+ const partial = result.partial;
7208
+
7209
+ // 1. rawEvents is not an Array (missing, or an object)
7210
+ if (!Array.isArray(rawEvents)) {
7211
+ return "page_events_not_array";
7212
+ }
7213
+
7214
+ // C6: coverage fields must be present booleans; absence is fail-closed
7215
+ if (typeof hasMore !== "boolean") {
7216
+ return "page_coverage_fields_missing";
7217
+ }
7218
+ if (typeof partial !== "boolean") {
7219
+ return "page_coverage_fields_missing";
7220
+ }
7221
+
7222
+ // C6: the bridge cannot use a partial page
7223
+ if (partial === true) {
7224
+ return "page_partial";
7225
+ }
7226
+
7227
+ // 5. hasMore === true and rawNextBeforeSequence is null or missing
7228
+ if (hasMore === true && (rawNextBeforeSequence === null || rawNextBeforeSequence === undefined)) {
7229
+ return "page_has_more_without_cursor";
7230
+ }
7231
+
7232
+ // 4. rawNextBeforeSequence is not null and is not a safe positive integer
7233
+ if (rawNextBeforeSequence !== null && rawNextBeforeSequence !== undefined) {
7234
+ if (!Number.isSafeInteger(rawNextBeforeSequence) || rawNextBeforeSequence <= 0) {
7235
+ return "page_next_before_not_safe_positive_integer";
7236
+ }
7237
+ if (Number.isFinite(beforeSequence) && rawNextBeforeSequence >= beforeSequence) {
7238
+ return "next_before_sequence_not_less_than_requested";
7239
+ }
7240
+ }
7241
+
7242
+ // 3. rawEvents.length > the requested limit
7243
+ if (rawEvents.length > limit) {
7244
+ return "page_too_many_events";
7245
+ }
7246
+
7247
+ // 2. Validate each raw event's sequenceId
7248
+ const seen = new Set();
7249
+ for (const event of rawEvents) {
7250
+ const rawSeq = event?.sequenceId;
7251
+ if (typeof rawSeq !== "number" || !Number.isSafeInteger(rawSeq) || rawSeq <= 0) {
7252
+ return "event_sequence_not_safe_positive_integer";
7253
+ }
7254
+
7255
+ // event's sequenceId >= beforeSequence
7256
+ if (Number.isFinite(beforeSequence) && rawSeq >= beforeSequence) {
7257
+ return "event_sequence_not_before_requested";
7258
+ }
7259
+
7260
+ // event carries a sessionId other than the one requested
7261
+ const eventSessionId = normalizeString(event?.sessionId ?? event?.session_id);
7262
+ if (eventSessionId && eventSessionId !== requestedSessionId) {
7263
+ return "event_session_id_mismatch";
7264
+ }
7265
+
7266
+ // duplicate sequence ids
7267
+ if (seen.has(rawSeq)) {
7268
+ return "duplicate_event_sequence";
7269
+ }
7270
+ seen.add(rawSeq);
7271
+ }
7272
+
7273
+ return null;
7274
+ }
7275
+
7276
+
6862
7277
  session
6863
7278
  .command("recall <need>")
6864
7279
  .description(
@@ -7294,12 +7709,38 @@ export function registerSessionCommand(program) {
7294
7709
  );
7295
7710
  });
7296
7711
 
7712
+ session
7713
+ .command("standup <sessionId>")
7714
+ .description("Summarize returned ticket records and their evidence without a model call")
7715
+ .option("--since <24h|ISO>", "Include records updated since this time (inclusive)")
7716
+ .option("--agent <id>", "Agent reading the tickets (defaults to the joined session agent)")
7717
+ .option("--path <path>", "Workspace path for the session", ".")
7718
+ .option("--json", "Emit machine-readable output")
7719
+ .action(async (sessionId, options, command) => {
7720
+ const result = await sessionStandup(normalizeString(sessionId), {
7721
+ since: options.since, targetPath: path.resolve(process.cwd(), String(options.path || ".")),
7722
+ });
7723
+ console.log(shouldEmitJson(options, command) ? JSON.stringify(result, null, 2) : formatSessionStandup(result));
7724
+ });
7725
+
7297
7726
  // ---------------------------------------------------------------- tickets
7298
7727
  // The room's shared backlog (API "Ticket Lease Contract (T2)"). Every subcommand is
7299
7728
  // an ACTOR command: the runCli choke point runs it on the agent's admission.
7300
7729
  const ticket = session
7301
7730
  .command("ticket")
7302
- .description("The room's shared backlog: list, claim, renew, release and submit tickets");
7731
+ .description("The room's shared backlog: list, claim, renew, release and submit tickets")
7732
+ .addHelpText("after", `
7733
+ Workflow:
7734
+ sl session ticket list <sessionId> --available --json
7735
+ sl session ticket claim <sessionId> <ticketId> --expected-version <version>
7736
+ sl session ticket renew <sessionId> <ticketId>
7737
+ sl session ticket submit <sessionId> <ticketId> --sha <commit> --evidence-file evidence.json
7738
+
7739
+ The evidence file is a JSON array of re-checkable claims. Submit means in_review,
7740
+ not done. Use release --reason "handoff" when you cannot continue.
7741
+ Agent work requires that agent's live admission; --agent is not authorization.
7742
+ Guide: https://sentinelayer.com/docs/senti/overview/
7743
+ `);
7303
7744
 
7304
7745
  // Ticket work is attributed by the CREDENTIAL, never by a label: the lease routes take the
7305
7746
  // holder from authentication alone. So an agent identity is allowed only with that agent's
@@ -7328,6 +7769,48 @@ export function registerSessionCommand(program) {
7328
7769
  console.log(line);
7329
7770
  }
7330
7771
 
7772
+ ticket
7773
+ .command("create <sessionId>")
7774
+ .description("Create a ticket in the room. Agent and source attribution are recorded only by servers that support them; the output says whether they were")
7775
+ .requiredOption("--title <text>", "Ticket title")
7776
+ .option("--body <text>", "Ticket description", "")
7777
+ .option("--agent <id>", "Acting agent identity, authorized by the server")
7778
+ .option("--assignee <id>", "Who should own the work")
7779
+ .option("--source-sequence <n>", "Source message in this room")
7780
+ .option("--idempotency-key <key>", "Reuse the key after an uncertain response")
7781
+ .option("--path <path>", "Workspace path for the session", ".")
7782
+ .option("--json", "Emit machine-readable output")
7783
+ .action(async (sessionId, options, command) => {
7784
+ const sid = normalizeString(sessionId), targetPath = path.resolve(process.cwd(), String(options.path || "."));
7785
+ const identity = await resolveSessionSayIdentity({ sessionId: sid, agentId: options.agent, targetPath });
7786
+ const agentId = normalizeString(identity.agentId);
7787
+ const result = await createTicket(sid, { title: options.title, body: options.body, assignee: options.assignee,
7788
+ agentId: agentId === "cli-user" ? "" : agentId, targetPath,
7789
+ sourceSequenceId: options.sourceSequence === undefined ? null : parsePositiveInteger(options.sourceSequence, "source-sequence", 1),
7790
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
7791
+ });
7792
+ const { agentRequested, agentRecorded, sourceRequested, sourceRecorded } = result.provenance;
7793
+ const creator = result.ticket?.creator;
7794
+ let creatorNote = " Creator: not recorded.";
7795
+ if (typeof creator?.id === "string" && creator.id.trim()) {
7796
+ if (creator.kind === "agent") {
7797
+ const assurance = creator.credentialBound === true && ["session_admission", "agent_token"].includes(creator.identitySource)
7798
+ ? "credential-bound"
7799
+ : creator.credentialBound === false && creator.identitySource === "authorized_agent_grant"
7800
+ ? "authorized account grant; not a separate agent credential"
7801
+ : "identity assurance unavailable";
7802
+ creatorNote = ` Creator: agent ${creator.id} (${assurance}).`;
7803
+ } else if (creator.kind === "account") {
7804
+ creatorNote = ` Creator: account ${creator.id} (the credential holder's physical identity is not established).`;
7805
+ }
7806
+ }
7807
+ const notes = [
7808
+ agentRequested && !agentRecorded ? ` The response did not confirm requested agent ${agentRequested}.` : "",
7809
+ sourceRequested !== null && !sourceRecorded ? ` The response did not confirm source #${sourceRequested}.` : "",
7810
+ ].join("");
7811
+ ticketOutput(options, command, { command: "session ticket create", ...result }, `Created ${result.ticket.id}.${creatorNote}${notes}`);
7812
+ });
7813
+
7331
7814
  ticket
7332
7815
  .command("list <sessionId>")
7333
7816
  .description("List the room's tickets (--available: only those claimable now)")
@@ -7691,7 +8174,7 @@ export function registerSessionCommand(program) {
7691
8174
 
7692
8175
  session
7693
8176
  .command("status <sessionId>")
7694
- .description("Show session status, agents, and health")
8177
+ .description("Show workspace session status, agents, and file-lock health")
7695
8178
  .option("--path <path>", "Workspace path for the session", ".")
7696
8179
  .option("--json", "Emit machine-readable output")
7697
8180
  .action(async (sessionId, options, command) => {
@@ -8557,6 +9040,7 @@ export function registerSessionCommand(program) {
8557
9040
  });
8558
9041
  const payload = {
8559
9042
  command: "session setup-guides",
9043
+ skillsInstallCommand: "sl skills install",
8560
9044
  targetPath,
8561
9045
  sessionId: normalizedSessionId,
8562
9046
  sectionHeading: result.sectionHeading,
@@ -8570,6 +9054,7 @@ export function registerSessionCommand(program) {
8570
9054
  }
8571
9055
 
8572
9056
  console.log(pc.bold(`Session guide sync complete for ${normalizedSessionId}`));
9057
+ console.log("Install bundled Senti agent skills: sl skills install");
8573
9058
  console.log(pc.gray(`AGENTS.md: changed=${result.agents.changed} path=${result.agents.path}`));
8574
9059
  console.log(pc.gray(`CLAUDE.md: changed=${result.claude.changed} path=${result.claude.path}`));
8575
9060
  console.log(
@@ -8650,7 +9135,7 @@ export function registerSessionCommand(program) {
8650
9135
  const concurrency = Math.max(1, Math.min(10, requestedConcurrency, count));
8651
9136
  const tags = parseCsvTokens(options.tags, ["session", "swarm"]);
8652
9137
  const apiUrl = normalizeAidenIdApiUrl(options.apiUrl);
8653
- const outputRoot = await resolveOutputRoot({
9138
+ const { outputRoot, outputFs } = await resolveOutputContext({
8654
9139
  cwd: targetPath,
8655
9140
  outputDirOverride: options.outputDir,
8656
9141
  env: process.env,
@@ -8743,6 +9228,7 @@ export function registerSessionCommand(program) {
8743
9228
  for (const identity of provisioned) {
8744
9229
  await recordProvisionedIdentity({
8745
9230
  outputRoot,
9231
+ outputFs,
8746
9232
  response: identity.response || {},
8747
9233
  context: {
8748
9234
  source: "session-provision-emails",