sentinelayer-cli 0.6.2 → 0.8.1

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 (280) hide show
  1. package/README.md +1009 -996
  2. package/bin/create-sentinelayer.js +5 -5
  3. package/bin/sentinelayer-cli.js +4 -4
  4. package/bin/sl.js +5 -5
  5. package/package.json +64 -63
  6. package/src/agents/ai-governance/index.js +12 -0
  7. package/src/agents/ai-governance/tools/base.js +171 -0
  8. package/src/agents/ai-governance/tools/eval-regression.js +47 -0
  9. package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
  10. package/src/agents/ai-governance/tools/index.js +52 -0
  11. package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
  12. package/src/agents/ai-governance/tools/provenance-check.js +69 -0
  13. package/src/agents/backend/index.js +12 -0
  14. package/src/agents/backend/tools/base.js +189 -0
  15. package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
  16. package/src/agents/backend/tools/idempotency-audit.js +105 -0
  17. package/src/agents/backend/tools/index.js +87 -0
  18. package/src/agents/backend/tools/retry-audit.js +132 -0
  19. package/src/agents/backend/tools/timeout-audit.js +144 -0
  20. package/src/agents/code-quality/index.js +12 -0
  21. package/src/agents/code-quality/tools/base.js +159 -0
  22. package/src/agents/code-quality/tools/complexity-measure.js +197 -0
  23. package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
  24. package/src/agents/code-quality/tools/cycle-detect.js +49 -0
  25. package/src/agents/code-quality/tools/dep-graph.js +196 -0
  26. package/src/agents/code-quality/tools/index.js +89 -0
  27. package/src/agents/data-layer/index.js +12 -0
  28. package/src/agents/data-layer/tools/base.js +181 -0
  29. package/src/agents/data-layer/tools/index-audit.js +165 -0
  30. package/src/agents/data-layer/tools/index.js +83 -0
  31. package/src/agents/data-layer/tools/migration-scan.js +135 -0
  32. package/src/agents/data-layer/tools/query-explain.js +120 -0
  33. package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
  34. package/src/agents/documentation/index.js +12 -0
  35. package/src/agents/documentation/tools/api-diff.js +91 -0
  36. package/src/agents/documentation/tools/base.js +151 -0
  37. package/src/agents/documentation/tools/dead-link-check.js +58 -0
  38. package/src/agents/documentation/tools/docstring-coverage.js +78 -0
  39. package/src/agents/documentation/tools/index.js +52 -0
  40. package/src/agents/documentation/tools/readme-freshness.js +61 -0
  41. package/src/agents/envelope/fix-cycle.js +45 -0
  42. package/src/agents/envelope/index.js +31 -0
  43. package/src/agents/envelope/loop.js +150 -0
  44. package/src/agents/envelope/pulse.js +18 -0
  45. package/src/agents/envelope/stream.js +40 -0
  46. package/src/agents/infrastructure/index.js +12 -0
  47. package/src/agents/infrastructure/tools/base.js +171 -0
  48. package/src/agents/infrastructure/tools/checkov-run.js +32 -0
  49. package/src/agents/infrastructure/tools/drift-detect.js +59 -0
  50. package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
  51. package/src/agents/infrastructure/tools/index.js +52 -0
  52. package/src/agents/infrastructure/tools/tflint-run.js +31 -0
  53. package/src/agents/jules/config/definition.js +160 -160
  54. package/src/agents/jules/config/system-prompt.js +182 -182
  55. package/src/agents/jules/error-intake.js +51 -51
  56. package/src/agents/jules/fix-cycle.js +17 -17
  57. package/src/agents/jules/loop.js +460 -450
  58. package/src/agents/jules/pulse.js +10 -10
  59. package/src/agents/jules/stream.js +187 -186
  60. package/src/agents/jules/swarm/file-scanner.js +74 -74
  61. package/src/agents/jules/swarm/index.js +11 -11
  62. package/src/agents/jules/swarm/orchestrator.js +362 -362
  63. package/src/agents/jules/swarm/pattern-hunter.js +123 -123
  64. package/src/agents/jules/swarm/sub-agent.js +315 -309
  65. package/src/agents/jules/tools/aidenid-email.js +189 -189
  66. package/src/agents/jules/tools/auth-audit.js +1708 -1691
  67. package/src/agents/jules/tools/dispatch.js +340 -335
  68. package/src/agents/jules/tools/file-edit.js +2 -2
  69. package/src/agents/jules/tools/file-read.js +2 -2
  70. package/src/agents/jules/tools/frontend-analyze.js +570 -570
  71. package/src/agents/jules/tools/glob.js +2 -2
  72. package/src/agents/jules/tools/grep.js +2 -2
  73. package/src/agents/jules/tools/index.js +29 -29
  74. package/src/agents/jules/tools/path-guards.js +2 -2
  75. package/src/agents/jules/tools/runtime-audit.js +507 -507
  76. package/src/agents/jules/tools/shell.js +2 -2
  77. package/src/agents/jules/tools/url-policy.js +100 -100
  78. package/src/agents/mode.js +113 -0
  79. package/src/agents/observability/index.js +12 -0
  80. package/src/agents/observability/tools/alert-audit.js +39 -0
  81. package/src/agents/observability/tools/base.js +181 -0
  82. package/src/agents/observability/tools/dashboard-gap.js +42 -0
  83. package/src/agents/observability/tools/index.js +54 -0
  84. package/src/agents/observability/tools/log-schema-check.js +74 -0
  85. package/src/agents/observability/tools/span-coverage.js +74 -0
  86. package/src/agents/persona-visuals.js +102 -61
  87. package/src/agents/release/index.js +12 -0
  88. package/src/agents/release/tools/base.js +181 -0
  89. package/src/agents/release/tools/changelog-diff.js +86 -0
  90. package/src/agents/release/tools/feature-flag-audit.js +126 -0
  91. package/src/agents/release/tools/index.js +61 -0
  92. package/src/agents/release/tools/rollback-verify.js +129 -0
  93. package/src/agents/release/tools/semver-check.js +109 -0
  94. package/src/agents/reliability/index.js +12 -0
  95. package/src/agents/reliability/tools/backpressure-check.js +129 -0
  96. package/src/agents/reliability/tools/base.js +181 -0
  97. package/src/agents/reliability/tools/chaos-probe.js +109 -0
  98. package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
  99. package/src/agents/reliability/tools/health-check-audit.js +111 -0
  100. package/src/agents/reliability/tools/index.js +87 -0
  101. package/src/agents/run-persona.js +109 -0
  102. package/src/agents/security/index.js +12 -0
  103. package/src/agents/security/tools/authz-audit.js +134 -0
  104. package/src/agents/security/tools/base.js +190 -0
  105. package/src/agents/security/tools/crypto-review.js +175 -0
  106. package/src/agents/security/tools/index.js +97 -0
  107. package/src/agents/security/tools/sast-scan.js +175 -0
  108. package/src/agents/security/tools/secrets-scan.js +216 -0
  109. package/src/agents/shared-tools/dispatch-core.js +320 -315
  110. package/src/agents/shared-tools/file-edit.js +180 -180
  111. package/src/agents/shared-tools/file-read.js +100 -100
  112. package/src/agents/shared-tools/glob.js +168 -168
  113. package/src/agents/shared-tools/grep.js +228 -228
  114. package/src/agents/shared-tools/index.js +46 -46
  115. package/src/agents/shared-tools/path-guards.js +161 -161
  116. package/src/agents/shared-tools/shell.js +383 -383
  117. package/src/agents/supply-chain/index.js +12 -0
  118. package/src/agents/supply-chain/tools/attestation-check.js +42 -0
  119. package/src/agents/supply-chain/tools/base.js +151 -0
  120. package/src/agents/supply-chain/tools/index.js +52 -0
  121. package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
  122. package/src/agents/supply-chain/tools/package-verify.js +56 -0
  123. package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
  124. package/src/agents/testing/index.js +12 -0
  125. package/src/agents/testing/tools/base.js +202 -0
  126. package/src/agents/testing/tools/coverage-gap.js +144 -0
  127. package/src/agents/testing/tools/flake-detect.js +125 -0
  128. package/src/agents/testing/tools/index.js +85 -0
  129. package/src/agents/testing/tools/mutation-test.js +143 -0
  130. package/src/agents/testing/tools/snapshot-diff.js +103 -0
  131. package/src/ai/aidenid.js +1021 -1009
  132. package/src/ai/client.js +553 -553
  133. package/src/ai/domain-target-store.js +268 -268
  134. package/src/ai/identity-store.js +270 -270
  135. package/src/ai/proxy.js +137 -137
  136. package/src/ai/site-store.js +145 -145
  137. package/src/audit/agents/architecture.js +180 -180
  138. package/src/audit/agents/compliance.js +179 -179
  139. package/src/audit/agents/documentation.js +165 -165
  140. package/src/audit/agents/performance.js +145 -145
  141. package/src/audit/agents/security.js +215 -215
  142. package/src/audit/agents/testing.js +172 -172
  143. package/src/audit/orchestrator.js +557 -557
  144. package/src/audit/package.js +204 -204
  145. package/src/audit/registry.js +284 -284
  146. package/src/audit/replay.js +103 -103
  147. package/src/auth/gate.js +428 -371
  148. package/src/auth/http.js +681 -611
  149. package/src/auth/service.js +1106 -1106
  150. package/src/auth/session-store.js +813 -813
  151. package/src/cli.js +257 -252
  152. package/src/commands/ai/identity-lifecycle.js +1338 -1338
  153. package/src/commands/ai/provision-governance.js +1272 -1272
  154. package/src/commands/ai/shared.js +147 -147
  155. package/src/commands/ai.js +11 -11
  156. package/src/commands/apply.js +12 -12
  157. package/src/commands/audit.js +1171 -1166
  158. package/src/commands/auth.js +419 -419
  159. package/src/commands/chat.js +184 -191
  160. package/src/commands/config.js +184 -184
  161. package/src/commands/cost.js +311 -311
  162. package/src/commands/daemon/core.js +850 -850
  163. package/src/commands/daemon/extended.js +1048 -1048
  164. package/src/commands/daemon/shared.js +213 -213
  165. package/src/commands/daemon.js +11 -11
  166. package/src/commands/guide.js +174 -174
  167. package/src/commands/ingest.js +58 -58
  168. package/src/commands/init.js +55 -55
  169. package/src/commands/legacy-args.js +20 -10
  170. package/src/commands/mcp.js +461 -461
  171. package/src/commands/omargate.js +63 -29
  172. package/src/commands/persona.js +65 -20
  173. package/src/commands/plugin.js +260 -260
  174. package/src/commands/policy.js +132 -132
  175. package/src/commands/prompt.js +238 -238
  176. package/src/commands/review.js +704 -704
  177. package/src/commands/scan.js +865 -872
  178. package/src/commands/session.js +1238 -0
  179. package/src/commands/spec.js +771 -716
  180. package/src/commands/swarm.js +651 -651
  181. package/src/commands/telemetry.js +202 -202
  182. package/src/commands/watch.js +511 -511
  183. package/src/config/agent-dictionary.js +182 -182
  184. package/src/config/io.js +56 -56
  185. package/src/config/paths.js +18 -18
  186. package/src/config/schema.js +55 -55
  187. package/src/config/service.js +184 -184
  188. package/src/coord/events-log.js +141 -0
  189. package/src/coord/handshake.js +719 -0
  190. package/src/coord/index.js +35 -0
  191. package/src/coord/paths.js +84 -0
  192. package/src/coord/priority.js +62 -0
  193. package/src/coord/tarjan.js +157 -0
  194. package/src/cost/budget.js +235 -235
  195. package/src/cost/history.js +188 -188
  196. package/src/cost/tokenizer.js +160 -0
  197. package/src/cost/tracker.js +232 -171
  198. package/src/daemon/artifact-lineage.js +896 -534
  199. package/src/daemon/assignment-ledger.js +1083 -770
  200. package/src/daemon/ast-drift.js +496 -0
  201. package/src/daemon/ast-parser-layer.js +258 -258
  202. package/src/daemon/budget-governor.js +633 -633
  203. package/src/daemon/callgraph-overlay.js +646 -646
  204. package/src/daemon/error-worker.js +1209 -626
  205. package/src/daemon/fix-cycle.js +384 -377
  206. package/src/daemon/hybrid-mapper.js +929 -929
  207. package/src/daemon/ingest-refresh.js +79 -11
  208. package/src/daemon/jira-lifecycle.js +767 -632
  209. package/src/daemon/operator-control.js +657 -657
  210. package/src/daemon/pulse.js +327 -327
  211. package/src/daemon/reliability-lane.js +471 -471
  212. package/src/daemon/scope-engine.js +1068 -0
  213. package/src/daemon/watchdog.js +971 -971
  214. package/src/events/schema.js +190 -0
  215. package/src/guide/generator.js +316 -316
  216. package/src/ingest/engine.js +933 -918
  217. package/src/ingest/ownership.js +380 -0
  218. package/src/interactive/index.js +97 -97
  219. package/src/legacy-cli.js +3228 -2994
  220. package/src/mcp/registry.js +695 -695
  221. package/src/memory/blackboard.js +301 -301
  222. package/src/memory/retrieval.js +581 -581
  223. package/src/orchestrator/kai-chen.js +126 -0
  224. package/src/plugin/manifest.js +553 -553
  225. package/src/policy/packs.js +144 -144
  226. package/src/prompt/generator.js +136 -118
  227. package/src/review/ai-review.js +672 -679
  228. package/src/review/compliance-pack.js +389 -0
  229. package/src/review/investor-dd-config.js +54 -0
  230. package/src/review/investor-dd-file-loop.js +303 -0
  231. package/src/review/investor-dd-file-router.js +406 -0
  232. package/src/review/investor-dd-html-report.js +233 -0
  233. package/src/review/investor-dd-notification.js +120 -0
  234. package/src/review/investor-dd-orchestrator.js +405 -0
  235. package/src/review/investor-dd-persona-runner.js +275 -0
  236. package/src/review/live-validator.js +253 -0
  237. package/src/review/local-review.js +1351 -1305
  238. package/src/review/omargate-interactive.js +68 -68
  239. package/src/review/omargate-orchestrator.js +492 -300
  240. package/src/review/persona-prompts.js +484 -296
  241. package/src/review/reconciliation-rules.js +329 -0
  242. package/src/review/replay.js +235 -235
  243. package/src/review/report.js +664 -664
  244. package/src/review/reproducibility-chain.js +136 -0
  245. package/src/review/scan-modes.js +147 -42
  246. package/src/review/spec-binding.js +487 -487
  247. package/src/scaffold/generator.js +67 -67
  248. package/src/scaffold/templates.js +150 -150
  249. package/src/scan/generator.js +418 -418
  250. package/src/scan/gh-secrets.js +107 -107
  251. package/src/session/agent-registry.js +359 -0
  252. package/src/session/analytics.js +479 -0
  253. package/src/session/daemon.js +1396 -0
  254. package/src/session/file-locks.js +666 -0
  255. package/src/session/paths.js +37 -0
  256. package/src/session/recap.js +567 -0
  257. package/src/session/redact.js +82 -0
  258. package/src/session/runtime-bridge.js +762 -0
  259. package/src/session/scoring.js +406 -0
  260. package/src/session/setup-guides.js +304 -0
  261. package/src/session/store.js +704 -0
  262. package/src/session/stream.js +333 -0
  263. package/src/session/sync.js +753 -0
  264. package/src/session/tasks.js +1054 -0
  265. package/src/session/templates.js +188 -0
  266. package/src/spec/generator.js +619 -519
  267. package/src/spec/regenerate.js +237 -237
  268. package/src/spec/templates.js +91 -91
  269. package/src/swarm/dashboard.js +247 -247
  270. package/src/swarm/factory.js +363 -363
  271. package/src/swarm/pentest.js +934 -934
  272. package/src/swarm/registry.js +419 -419
  273. package/src/swarm/report.js +158 -158
  274. package/src/swarm/runtime.js +569 -576
  275. package/src/swarm/scenario-dsl.js +272 -272
  276. package/src/telemetry/ledger.js +302 -302
  277. package/src/telemetry/session-tracker.js +234 -234
  278. package/src/telemetry/sync.js +203 -203
  279. package/src/ui/command-hints.js +13 -13
  280. package/src/ui/markdown.js +220 -220
@@ -0,0 +1,1238 @@
1
+ import path from "node:path";
2
+ import process from "node:process";
3
+ import { randomUUID } from "node:crypto";
4
+
5
+ import pc from "picocolors";
6
+
7
+ import { SentinelayerApiError, requestJsonMutation } from "../auth/http.js";
8
+ import {
9
+ buildProvisionEmailPayload,
10
+ normalizeAidenIdApiUrl,
11
+ provisionEmailIdentity,
12
+ resolveAidenIdCredentials,
13
+ } from "../ai/aidenid.js";
14
+ import { recordProvisionedIdentity } from "../ai/identity-store.js";
15
+ import { readStoredSession } from "../auth/session-store.js";
16
+ import { fetchAidenIdCredentials } from "../auth/service.js";
17
+ import { resolveActiveAuthSession } from "../auth/service.js";
18
+ import { resolveOutputRoot } from "../config/service.js";
19
+ import {
20
+ listAssignments,
21
+ releaseLease,
22
+ } from "../daemon/assignment-ledger.js";
23
+ import { stopScopeEngine } from "../daemon/scope-engine.js";
24
+ import { createAgentEvent } from "../events/schema.js";
25
+ import {
26
+ detectStaleAgents,
27
+ listAgents,
28
+ registerAgent,
29
+ unregisterAgent,
30
+ } from "../session/agent-registry.js";
31
+ import { stopSenti } from "../session/daemon.js";
32
+ import { listRuntimeRuns } from "../session/runtime-bridge.js";
33
+ import {
34
+ listFileLocks,
35
+ releaseFileLocksForAgent,
36
+ } from "../session/file-locks.js";
37
+ import {
38
+ injectSessionGuides,
39
+ setupSessionGuides,
40
+ } from "../session/setup-guides.js";
41
+ import { listSessionTasks } from "../session/tasks.js";
42
+ import {
43
+ createSession,
44
+ DEFAULT_TTL_SECONDS,
45
+ getSession,
46
+ listActiveSessions,
47
+ recordSessionProvisionedIdentities,
48
+ } from "../session/store.js";
49
+ import { appendToStream, readStream, tailStream } from "../session/stream.js";
50
+ import { syncSessionMetadataToApi } from "../session/sync.js";
51
+ import {
52
+ buildDashboardUrl,
53
+ buildTemplateLaunchPlan,
54
+ getTemplateRegistry,
55
+ resolveSessionTemplate,
56
+ } from "../session/templates.js";
57
+ import { authLoginHint } from "../ui/command-hints.js";
58
+ import { parseCsvTokens } from "./ai/shared.js";
59
+
60
+ function shouldEmitJson(options, command) {
61
+ const local = Boolean(options && options.json);
62
+ const globalFromCommand =
63
+ command && command.optsWithGlobals ? Boolean(command.optsWithGlobals().json) : false;
64
+ return local || globalFromCommand;
65
+ }
66
+
67
+ function normalizeString(value) {
68
+ return String(value || "").trim();
69
+ }
70
+
71
+ function parsePositiveInteger(rawValue, field, fallbackValue) {
72
+ if (rawValue === undefined || rawValue === null || String(rawValue).trim() === "") {
73
+ return fallbackValue;
74
+ }
75
+ const normalized = Number(rawValue);
76
+ if (!Number.isFinite(normalized) || normalized <= 0) {
77
+ throw new Error(`${field} must be a positive integer.`);
78
+ }
79
+ return Math.floor(normalized);
80
+ }
81
+
82
+ function normalizeAgentId(value, fallbackValue = "cli-user") {
83
+ const normalized = normalizeString(value)
84
+ .toLowerCase()
85
+ .replace(/[^a-z0-9._-]+/g, "-")
86
+ .replace(/^-+|-+$/g, "");
87
+ return normalized || fallbackValue;
88
+ }
89
+
90
+ async function runWithConcurrency(items = [], concurrency = 1, worker = async () => null) {
91
+ const normalizedItems = Array.isArray(items) ? items : [];
92
+ const normalizedConcurrency = Math.max(
93
+ 1,
94
+ Math.min(
95
+ normalizedItems.length || 1,
96
+ Number.isFinite(Number(concurrency)) ? Math.floor(Number(concurrency)) : 1
97
+ )
98
+ );
99
+ const results = new Array(normalizedItems.length);
100
+ let cursor = 0;
101
+
102
+ const runners = Array.from({ length: normalizedConcurrency }, async () => {
103
+ while (cursor < normalizedItems.length) {
104
+ const index = cursor;
105
+ cursor += 1;
106
+ results[index] = await worker(normalizedItems[index], index);
107
+ }
108
+ });
109
+ await Promise.all(runners);
110
+ return results;
111
+ }
112
+
113
+ function resolveSessionIdOption(options = {}) {
114
+ const sessionId = normalizeString(options.session || options.id);
115
+ if (!sessionId) {
116
+ throw new Error("session id is required (use --session <id>).");
117
+ }
118
+ return sessionId;
119
+ }
120
+
121
+ function formatEventLine(event = {}) {
122
+ const ts = normalizeString(event.ts || event.timestamp);
123
+ const type = normalizeString(event.event || event.type) || "event";
124
+ const agentId = normalizeString(event.agent?.id || event.agentId || "unknown");
125
+ const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
126
+ const message = normalizeString(payload.message || payload.response || payload.alert || payload.reason || "");
127
+ if (message) {
128
+ return `${ts} ${agentId} ${type}: ${message}`;
129
+ }
130
+ return `${ts} ${agentId} ${type}`;
131
+ }
132
+
133
+ function formatTemplateLaunchLine(slot = {}) {
134
+ const terminal = Number(slot.terminal || 0);
135
+ const role = normalizeString(slot.role) || "agent";
136
+ const command = normalizeString(slot.command);
137
+ return `Terminal ${terminal} (${role}): ${command}`;
138
+ }
139
+
140
+ function formatApiError(error) {
141
+ if (!(error instanceof SentinelayerApiError)) {
142
+ return error instanceof Error ? error.message : String(error || "Unknown API error");
143
+ }
144
+ const requestId = error.requestId ? ` request_id=${error.requestId}` : "";
145
+ return `${error.message} [${error.code}] status=${error.status}${requestId}`;
146
+ }
147
+
148
+ async function resolveAdminApiSession({ targetPath, explicitApiUrl }) {
149
+ const session = await resolveActiveAuthSession({
150
+ cwd: targetPath,
151
+ env: process.env,
152
+ explicitApiUrl,
153
+ autoRotate: true,
154
+ });
155
+ if (!session || !session.token) {
156
+ throw new Error(`No active auth token found. Run \`${authLoginHint()}\` first.`);
157
+ }
158
+ return session;
159
+ }
160
+
161
+ async function postAdminSessionMutation({
162
+ session,
163
+ pathSuffix,
164
+ operationName,
165
+ body = {},
166
+ headers = {},
167
+ } = {}) {
168
+ const apiUrl = normalizeString(session?.apiUrl).replace(/\/+$/, "");
169
+ if (!apiUrl) {
170
+ throw new Error("Missing apiUrl for admin session mutation.");
171
+ }
172
+ return requestJsonMutation(`${apiUrl}${pathSuffix}`, {
173
+ method: "POST",
174
+ operationName,
175
+ headers: {
176
+ Authorization: `Bearer ${normalizeString(session.token)}`,
177
+ ...headers,
178
+ },
179
+ body,
180
+ });
181
+ }
182
+
183
+ async function emitLocalAdminKillEvent(
184
+ sessionId,
185
+ { targetPath, reason, scope, apiResult, actorId = "admin" } = {}
186
+ ) {
187
+ const session = await getSession(sessionId, { targetPath });
188
+ if (!session) {
189
+ return null;
190
+ }
191
+ const event = createAgentEvent({
192
+ event: "session_admin_kill",
193
+ agentId: actorId,
194
+ agentModel: "api-admin",
195
+ sessionId,
196
+ payload: {
197
+ scope: normalizeString(scope) || "session",
198
+ reason: normalizeString(reason) || "admin_kill",
199
+ result: apiResult && typeof apiResult === "object" ? apiResult : null,
200
+ },
201
+ });
202
+ return appendToStream(sessionId, event, { targetPath });
203
+ }
204
+
205
+ async function revokeAgentLeases(sessionId, agentId, { targetPath, reason } = {}) {
206
+ const active = await listAssignments({
207
+ targetPath,
208
+ sessionId,
209
+ agentIdentity: agentId,
210
+ statuses: ["CLAIMED", "IN_PROGRESS"],
211
+ includeExpired: true,
212
+ limit: 500,
213
+ });
214
+ let releasedCount = 0;
215
+ for (const assignment of active.assignments) {
216
+ await releaseLease({
217
+ targetPath,
218
+ sessionId,
219
+ workItemId: assignment.workItemId,
220
+ agentIdentity: agentId,
221
+ status: "QUEUED",
222
+ reason,
223
+ });
224
+ releasedCount += 1;
225
+ }
226
+ return releasedCount;
227
+ }
228
+
229
+ async function emitAgentKilledEvent(sessionId, agentId, {
230
+ targetPath,
231
+ reason,
232
+ leaseRevocations = 0,
233
+ } = {}) {
234
+ const event = createAgentEvent({
235
+ event: "agent_killed",
236
+ agentId,
237
+ sessionId,
238
+ payload: {
239
+ target: agentId,
240
+ reason: normalizeString(reason) || "manual_stop",
241
+ leaseRevocations: Number(leaseRevocations || 0),
242
+ },
243
+ });
244
+ await appendToStream(sessionId, event, { targetPath });
245
+ return event;
246
+ }
247
+
248
+ export function registerSessionCommand(program) {
249
+ const session = program
250
+ .command("session")
251
+ .description("Multi-agent ephemeral coordination sessions");
252
+
253
+ session
254
+ .command("start")
255
+ .description("Create a new persistent session with metadata + NDJSON stream")
256
+ .option("--path <path>", "Workspace path for the session", ".")
257
+ .option(
258
+ "--template <name>",
259
+ "Optional quick-start template (code-review, security-audit, e2e-test, incident-response, standup)"
260
+ )
261
+ .option(
262
+ "--ttl-seconds <seconds>",
263
+ `Session time-to-live in seconds (default ${DEFAULT_TTL_SECONDS}; template defaults override when omitted)`
264
+ )
265
+ .option("--json", "Emit machine-readable output")
266
+ .action(async (options, command) => {
267
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
268
+ const template = resolveSessionTemplate(options.template);
269
+ const templateDefaultTtlSeconds =
270
+ template && Number.isFinite(Number(template.ttlHours))
271
+ ? Math.max(1, Math.floor(Number(template.ttlHours))) * 60 * 60
272
+ : DEFAULT_TTL_SECONDS;
273
+ const ttlSeconds = parsePositiveInteger(
274
+ options.ttlSeconds,
275
+ "ttl-seconds",
276
+ templateDefaultTtlSeconds
277
+ );
278
+ const startedAt = Date.now();
279
+ const created = await createSession({
280
+ targetPath,
281
+ ttlSeconds,
282
+ template,
283
+ });
284
+ const durationMs = Date.now() - startedAt;
285
+ const launchPlan = template ? buildTemplateLaunchPlan(created.sessionId, template) : [];
286
+ const dashboardUrl = buildDashboardUrl(created.sessionId);
287
+
288
+ const payload = {
289
+ command: "session start",
290
+ targetPath,
291
+ durationMs,
292
+ sessionId: created.sessionId,
293
+ sessionDir: created.sessionDir,
294
+ metadataPath: created.metadataPath,
295
+ streamPath: created.streamPath,
296
+ createdAt: created.createdAt,
297
+ expiresAt: created.expiresAt,
298
+ ttlSeconds,
299
+ elapsedTimer: created.elapsedTimer,
300
+ renewalCount: created.renewalCount,
301
+ status: created.status,
302
+ template: created.template,
303
+ launchPlan,
304
+ dashboardUrl,
305
+ };
306
+
307
+ // Best-effort admin visibility sync. Session creation remains local-first.
308
+ void syncSessionMetadataToApi(created.sessionId, {
309
+ targetPath,
310
+ sessionId: created.sessionId,
311
+ status: created.status,
312
+ createdAt: created.createdAt,
313
+ expiresAt: created.expiresAt,
314
+ ttlSeconds,
315
+ template: created.template,
316
+ codebaseContext: created.codebaseContext,
317
+ }).catch(() => {});
318
+
319
+ if (shouldEmitJson(options, command)) {
320
+ console.log(JSON.stringify(payload, null, 2));
321
+ return;
322
+ }
323
+
324
+ if (template) {
325
+ console.log(`Session ${created.sessionId} created (template: ${template.id})`);
326
+ if (launchPlan.length > 0) {
327
+ console.log("");
328
+ console.log("Launch your agents:");
329
+ for (const slot of launchPlan) {
330
+ console.log(formatTemplateLaunchLine(slot));
331
+ }
332
+ }
333
+ console.log("");
334
+ console.log(`Dashboard: ${dashboardUrl}`);
335
+ return;
336
+ }
337
+
338
+ console.log(pc.bold("Session created"));
339
+ console.log(pc.gray(`Session: ${created.sessionId}`));
340
+ console.log(pc.gray(`Stream: ${created.streamPath}`));
341
+ console.log(pc.gray(`Created in ${durationMs}ms`));
342
+ console.log(
343
+ `status=${created.status} created_at=${created.createdAt} expires_at=${created.expiresAt} ttl_seconds=${ttlSeconds}`
344
+ );
345
+ });
346
+
347
+ session
348
+ .command("templates")
349
+ .description("List available session quick-start templates")
350
+ .option("--json", "Emit machine-readable output")
351
+ .action(async (options, command) => {
352
+ const registry = getTemplateRegistry();
353
+ const payload = {
354
+ command: "session templates",
355
+ ...registry,
356
+ };
357
+ if (shouldEmitJson(options, command)) {
358
+ console.log(JSON.stringify(payload, null, 2));
359
+ return;
360
+ }
361
+ console.log(`Session templates (registry ${registry.registryVersion}):`);
362
+ for (const template of registry.templates) {
363
+ console.log(`- ${template.id}: ${template.description}`);
364
+ }
365
+ });
366
+
367
+ session
368
+ .command("join <sessionId>")
369
+ .description("Join an active session")
370
+ .option("--name <name>", "Agent display name")
371
+ .option("--role <role>", "Agent role: coder, reviewer, tester, observer", "coder")
372
+ .option("--model <model>", "Agent model hint", "cli")
373
+ .option("--path <path>", "Workspace path for the session", ".")
374
+ .option("--json", "Emit machine-readable output")
375
+ .action(async (sessionId, options, command) => {
376
+ const normalizedSessionId = normalizeString(sessionId);
377
+ if (!normalizedSessionId) {
378
+ throw new Error("session id is required.");
379
+ }
380
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
381
+ const joined = await registerAgent(normalizedSessionId, {
382
+ targetPath,
383
+ agentId: normalizeAgentId(options.name, "cli-user"),
384
+ model: normalizeString(options.model) || "cli",
385
+ role: options.role || "coder",
386
+ });
387
+ const payload = {
388
+ command: "session join",
389
+ targetPath,
390
+ sessionId: normalizedSessionId,
391
+ agentId: joined.agentId,
392
+ role: joined.role,
393
+ model: joined.model,
394
+ status: joined.status,
395
+ joinedAt: joined.joinedAt,
396
+ };
397
+ if (shouldEmitJson(options, command)) {
398
+ console.log(JSON.stringify(payload, null, 2));
399
+ return;
400
+ }
401
+ console.log(pc.bold(`Joined session ${normalizedSessionId}`));
402
+ console.log(pc.gray(`agent=${joined.agentId} role=${joined.role} model=${joined.model}`));
403
+ });
404
+
405
+ session
406
+ .command("say <sessionId> <message>")
407
+ .description("Send a message to the session")
408
+ .option("--agent <id>", "Agent id to emit from", "cli-user")
409
+ .option("--path <path>", "Workspace path for the session", ".")
410
+ .option("--json", "Emit machine-readable output")
411
+ .action(async (sessionId, message, options, command) => {
412
+ const normalizedSessionId = normalizeString(sessionId);
413
+ if (!normalizedSessionId) {
414
+ throw new Error("session id is required.");
415
+ }
416
+ const normalizedMessage = normalizeString(message);
417
+ if (!normalizedMessage) {
418
+ throw new Error("message is required.");
419
+ }
420
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
421
+ const agentId = normalizeAgentId(options.agent, "cli-user");
422
+ const event = createAgentEvent({
423
+ event: "session_message",
424
+ agentId,
425
+ sessionId: normalizedSessionId,
426
+ payload: {
427
+ message: normalizedMessage,
428
+ channel: "session",
429
+ },
430
+ });
431
+ const persisted = await appendToStream(normalizedSessionId, event, {
432
+ targetPath,
433
+ });
434
+ const payload = {
435
+ command: "session say",
436
+ targetPath,
437
+ sessionId: normalizedSessionId,
438
+ agentId,
439
+ event: persisted,
440
+ };
441
+ if (shouldEmitJson(options, command)) {
442
+ console.log(JSON.stringify(payload, null, 2));
443
+ return;
444
+ }
445
+ console.log(formatEventLine(persisted));
446
+ });
447
+
448
+ session
449
+ .command("read <sessionId>")
450
+ .description("Read recent session messages")
451
+ .option("--tail <n>", "Number of recent events", "20")
452
+ .option("--follow", "Continuously follow new events")
453
+ .option("--path <path>", "Workspace path for the session", ".")
454
+ .option("--json", "Emit machine-readable output")
455
+ .action(async (sessionId, options, command) => {
456
+ const normalizedSessionId = normalizeString(sessionId);
457
+ if (!normalizedSessionId) {
458
+ throw new Error("session id is required.");
459
+ }
460
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
461
+ const tail = parsePositiveInteger(options.tail, "tail", 20);
462
+ const emitJson = shouldEmitJson(options, command);
463
+
464
+ if (!options.follow) {
465
+ const events = await readStream(normalizedSessionId, {
466
+ targetPath,
467
+ tail,
468
+ });
469
+ const payload = {
470
+ command: "session read",
471
+ targetPath,
472
+ sessionId: normalizedSessionId,
473
+ tail,
474
+ count: events.length,
475
+ events,
476
+ };
477
+ if (emitJson) {
478
+ console.log(JSON.stringify(payload, null, 2));
479
+ return;
480
+ }
481
+ for (const event of events) {
482
+ console.log(formatEventLine(event));
483
+ }
484
+ return;
485
+ }
486
+
487
+ if (!emitJson) {
488
+ console.log(pc.gray(`Following session ${normalizedSessionId}... Press Ctrl+C to stop.`));
489
+ }
490
+ for await (const event of tailStream(normalizedSessionId, {
491
+ targetPath,
492
+ replayTail: tail,
493
+ })) {
494
+ if (emitJson) {
495
+ console.log(JSON.stringify(event));
496
+ } else {
497
+ console.log(formatEventLine(event));
498
+ }
499
+ }
500
+ });
501
+
502
+ session
503
+ .command("status <sessionId>")
504
+ .description("Show session status, agents, and health")
505
+ .option("--path <path>", "Workspace path for the session", ".")
506
+ .option("--json", "Emit machine-readable output")
507
+ .action(async (sessionId, options, command) => {
508
+ const normalizedSessionId = normalizeString(sessionId);
509
+ if (!normalizedSessionId) {
510
+ throw new Error("session id is required.");
511
+ }
512
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
513
+ const sessionPayload = await getSession(normalizedSessionId, {
514
+ targetPath,
515
+ });
516
+ if (!sessionPayload) {
517
+ throw new Error(`Session '${normalizedSessionId}' was not found.`);
518
+ }
519
+
520
+ const [agents, runtimeRuns, leases, fileLocks, activeTasks, recentEvents] = await Promise.all([
521
+ listAgents(normalizedSessionId, {
522
+ targetPath,
523
+ includeInactive: false,
524
+ }),
525
+ Promise.resolve(
526
+ listRuntimeRuns({
527
+ sessionId: normalizedSessionId,
528
+ targetPath,
529
+ includeStopped: false,
530
+ })
531
+ ),
532
+ listAssignments({
533
+ targetPath,
534
+ sessionId: normalizedSessionId,
535
+ statuses: ["CLAIMED", "IN_PROGRESS"],
536
+ includeExpired: true,
537
+ limit: 100,
538
+ }),
539
+ listFileLocks(normalizedSessionId, {
540
+ targetPath,
541
+ emitExpiredEvents: false,
542
+ }),
543
+ listSessionTasks(normalizedSessionId, {
544
+ targetPath,
545
+ statuses: ["PENDING", "ACCEPTED"],
546
+ limit: 100,
547
+ }),
548
+ readStream(normalizedSessionId, {
549
+ targetPath,
550
+ tail: 10,
551
+ }),
552
+ ]);
553
+
554
+ const staleAgents = detectStaleAgents(agents, {});
555
+ const payload = {
556
+ command: "session status",
557
+ targetPath,
558
+ sessionId: normalizedSessionId,
559
+ session: sessionPayload,
560
+ activeAgents: agents,
561
+ staleAgents,
562
+ runtimeRuns,
563
+ activeLeases: leases.assignments,
564
+ activeFileLocks: fileLocks,
565
+ activeTasks: activeTasks.tasks,
566
+ recentEvents,
567
+ };
568
+ if (shouldEmitJson(options, command)) {
569
+ console.log(JSON.stringify(payload, null, 2));
570
+ return;
571
+ }
572
+
573
+ console.log(pc.bold(`Session ${normalizedSessionId}`));
574
+ console.log(
575
+ pc.gray(
576
+ `status=${sessionPayload.status} agents=${agents.length} stale=${staleAgents.length} runs=${runtimeRuns.length} leases=${leases.assignments.length} locks=${fileLocks.length} tasks=${activeTasks.tasks.length}`
577
+ )
578
+ );
579
+ for (const event of recentEvents) {
580
+ console.log(formatEventLine(event));
581
+ }
582
+ });
583
+
584
+ session
585
+ .command("leave <sessionId>")
586
+ .description("Leave a session")
587
+ .option("--agent <id>", "Agent id to unregister", "cli-user")
588
+ .option("--reason <reason>", "Leave reason", "manual")
589
+ .option("--path <path>", "Workspace path for the session", ".")
590
+ .option("--json", "Emit machine-readable output")
591
+ .action(async (sessionId, options, command) => {
592
+ const normalizedSessionId = normalizeString(sessionId);
593
+ if (!normalizedSessionId) {
594
+ throw new Error("session id is required.");
595
+ }
596
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
597
+ const agentId = normalizeAgentId(options.agent, "cli-user");
598
+ const left = await unregisterAgent(normalizedSessionId, agentId, {
599
+ reason: options.reason || "manual",
600
+ targetPath,
601
+ });
602
+ const payload = {
603
+ command: "session leave",
604
+ targetPath,
605
+ sessionId: normalizedSessionId,
606
+ agentId: left.agentId,
607
+ reason: left.leaveReason,
608
+ leftAt: left.leftAt,
609
+ };
610
+ if (shouldEmitJson(options, command)) {
611
+ console.log(JSON.stringify(payload, null, 2));
612
+ return;
613
+ }
614
+ console.log(pc.bold(`Left session ${normalizedSessionId}`));
615
+ console.log(pc.gray(`agent=${left.agentId} reason=${left.leaveReason}`));
616
+ });
617
+
618
+ session
619
+ .command("list")
620
+ .description("List active sessions")
621
+ .option("--path <path>", "Workspace path for sessions", ".")
622
+ .option("--json", "Emit machine-readable output")
623
+ .action(async (options, command) => {
624
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
625
+ const sessions = await listActiveSessions({
626
+ targetPath,
627
+ });
628
+ const payload = {
629
+ command: "session list",
630
+ targetPath,
631
+ count: sessions.length,
632
+ sessions,
633
+ };
634
+ if (shouldEmitJson(options, command)) {
635
+ console.log(JSON.stringify(payload, null, 2));
636
+ return;
637
+ }
638
+ if (sessions.length === 0) {
639
+ console.log(pc.yellow("No active sessions."));
640
+ return;
641
+ }
642
+ for (const item of sessions) {
643
+ console.log(
644
+ `${item.sessionId} status=${item.status} created_at=${item.createdAt} expires_at=${item.expiresAt}`
645
+ );
646
+ }
647
+ });
648
+
649
+ session
650
+ .command("setup-guides <sessionId>")
651
+ .description("Generate or update AGENTS.md and CLAUDE.md with session coordination rules")
652
+ .option("--path <path>", "Workspace path for the session", ".")
653
+ .option("--json", "Emit machine-readable output")
654
+ .action(async (sessionId, options, command) => {
655
+ const normalizedSessionId = normalizeString(sessionId);
656
+ if (!normalizedSessionId) {
657
+ throw new Error("session id is required.");
658
+ }
659
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
660
+ const result = await setupSessionGuides(normalizedSessionId, {
661
+ targetPath,
662
+ });
663
+ const payload = {
664
+ command: "session setup-guides",
665
+ targetPath,
666
+ sessionId: normalizedSessionId,
667
+ sectionHeading: result.sectionHeading,
668
+ agents: result.agents,
669
+ claude: result.claude,
670
+ sessionGuide: result.sessionGuide,
671
+ };
672
+ if (shouldEmitJson(options, command)) {
673
+ console.log(JSON.stringify(payload, null, 2));
674
+ return;
675
+ }
676
+
677
+ console.log(pc.bold(`Session guide sync complete for ${normalizedSessionId}`));
678
+ console.log(pc.gray(`AGENTS.md: changed=${result.agents.changed} path=${result.agents.path}`));
679
+ console.log(pc.gray(`CLAUDE.md: changed=${result.claude.changed} path=${result.claude.path}`));
680
+ console.log(
681
+ pc.gray(
682
+ `.sentinelayer/AGENTS_SESSION_GUIDE.md: changed=${result.sessionGuide.changed} path=${result.sessionGuide.path}`
683
+ )
684
+ );
685
+ });
686
+
687
+ session
688
+ .command("inject-guide <sessionId>")
689
+ .description("Append coordination section to existing AGENTS.md and CLAUDE.md files")
690
+ .option("--path <path>", "Workspace path for the session", ".")
691
+ .option("--json", "Emit machine-readable output")
692
+ .action(async (sessionId, options, command) => {
693
+ const normalizedSessionId = normalizeString(sessionId);
694
+ if (!normalizedSessionId) {
695
+ throw new Error("session id is required.");
696
+ }
697
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
698
+ const result = await injectSessionGuides(normalizedSessionId, {
699
+ targetPath,
700
+ });
701
+ const payload = {
702
+ command: "session inject-guide",
703
+ targetPath,
704
+ sessionId: normalizedSessionId,
705
+ sectionHeading: result.sectionHeading,
706
+ agents: result.agents,
707
+ claude: result.claude,
708
+ };
709
+ if (shouldEmitJson(options, command)) {
710
+ console.log(JSON.stringify(payload, null, 2));
711
+ return;
712
+ }
713
+
714
+ console.log(pc.bold(`Session guide section injected for ${normalizedSessionId}`));
715
+ console.log(pc.gray(`AGENTS.md: existed=${result.agents.existed} changed=${result.agents.changed}`));
716
+ console.log(pc.gray(`CLAUDE.md: existed=${result.claude.existed} changed=${result.claude.changed}`));
717
+ });
718
+
719
+ session
720
+ .command("provision-emails <sessionId>")
721
+ .description("Provision ephemeral AIdenID emails for swarm testing")
722
+ .option("--count <n>", "Number of emails to provision", "5")
723
+ .option("--tags <csv>", "Tags for provisioned identities", "session,swarm")
724
+ .option("--ttl-hours <hours>", "Identity TTL in hours", "24")
725
+ .option("--alias-template <value>", "Optional alias template override")
726
+ .option("--concurrency <n>", "Parallel provision requests (max 10)", "10")
727
+ .option("--path <path>", "Workspace path for the session", ".")
728
+ .option("--output-dir <path>", "Optional artifact output root override")
729
+ .option("--api-url <url>", "AIdenID API base URL", "https://api.aidenid.com")
730
+ .option("--api-key <key>", "AIdenID API key (or use AIDENID_API_KEY env)")
731
+ .option("--org-id <id>", "AIdenID org id (or use AIDENID_ORG_ID env)")
732
+ .option("--project-id <id>", "AIdenID project id (or use AIDENID_PROJECT_ID env)")
733
+ .option("--dry-run", "Plan provisioning without executing remote API calls")
734
+ .option("--json", "Emit machine-readable output")
735
+ .action(async (sessionId, options, command) => {
736
+ const normalizedSessionId = normalizeString(sessionId);
737
+ if (!normalizedSessionId) {
738
+ throw new Error("session id is required.");
739
+ }
740
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
741
+ const sessionPayload = await getSession(normalizedSessionId, { targetPath });
742
+ if (!sessionPayload) {
743
+ throw new Error(`Session '${normalizedSessionId}' was not found.`);
744
+ }
745
+
746
+ const count = parsePositiveInteger(options.count, "count", 5);
747
+ if (count > 50) {
748
+ throw new Error("count must be <= 50 for a single provisioning batch.");
749
+ }
750
+ const ttlHours = parsePositiveInteger(options.ttlHours, "ttl-hours", 24);
751
+ if (ttlHours > 24 * 30) {
752
+ throw new Error("ttl-hours must be between 1 and 720.");
753
+ }
754
+ const requestedConcurrency = parsePositiveInteger(options.concurrency, "concurrency", 10);
755
+ const concurrency = Math.max(1, Math.min(10, requestedConcurrency, count));
756
+ const tags = parseCsvTokens(options.tags, ["session", "swarm"]);
757
+ const apiUrl = normalizeAidenIdApiUrl(options.apiUrl);
758
+ const outputRoot = await resolveOutputRoot({
759
+ cwd: targetPath,
760
+ outputDirOverride: options.outputDir,
761
+ env: process.env,
762
+ });
763
+
764
+ const aliasBase =
765
+ normalizeString(options.aliasTemplate) ||
766
+ `session-${normalizedSessionId.slice(0, 8)}-identity`;
767
+
768
+ if (Boolean(options.dryRun)) {
769
+ const planned = Array.from({ length: count }, (_, index) => ({
770
+ index: index + 1,
771
+ aliasTemplate: `${aliasBase}-${index + 1}`,
772
+ tags,
773
+ ttlHours,
774
+ }));
775
+ const payload = {
776
+ command: "session provision-emails",
777
+ execute: false,
778
+ sessionId: normalizedSessionId,
779
+ targetPath,
780
+ apiUrl,
781
+ requestedCount: count,
782
+ concurrency,
783
+ tags,
784
+ planned,
785
+ };
786
+ if (shouldEmitJson(options, command)) {
787
+ console.log(JSON.stringify(payload, null, 2));
788
+ return;
789
+ }
790
+ console.log(pc.bold(`Provision plan ready for session ${normalizedSessionId}`));
791
+ console.log(pc.gray(`count=${count} concurrency=${concurrency} api=${apiUrl}`));
792
+ return;
793
+ }
794
+
795
+ let storedSession = null;
796
+ try {
797
+ storedSession = await readStoredSession();
798
+ } catch {
799
+ storedSession = null;
800
+ }
801
+
802
+ const fetchCredentials =
803
+ storedSession && storedSession.token
804
+ ? () =>
805
+ fetchAidenIdCredentials({
806
+ apiUrl: storedSession.apiUrl,
807
+ token: storedSession.token,
808
+ })
809
+ : null;
810
+ const credentials = await resolveAidenIdCredentials({
811
+ apiKey: options.apiKey,
812
+ orgId: options.orgId,
813
+ projectId: options.projectId,
814
+ env: process.env,
815
+ requireAll: true,
816
+ session: storedSession,
817
+ fetchCredentials,
818
+ });
819
+
820
+ const startedAt = Date.now();
821
+ const indices = Array.from({ length: count }, (_, index) => index);
822
+ const provisioned = await runWithConcurrency(indices, concurrency, async (index) => {
823
+ const idempotencyKey = `session-${normalizedSessionId}-${index + 1}-${randomUUID()}`;
824
+ const payload = buildProvisionEmailPayload({
825
+ aliasTemplate: `${aliasBase}-${index + 1}`,
826
+ ttlHours,
827
+ tags,
828
+ });
829
+ const execution = await provisionEmailIdentity({
830
+ apiUrl,
831
+ apiKey: credentials.apiKey,
832
+ orgId: credentials.orgId,
833
+ projectId: credentials.projectId,
834
+ idempotencyKey,
835
+ payload,
836
+ });
837
+
838
+ const responseIdentity = execution.response || {};
839
+ return {
840
+ index: index + 1,
841
+ idempotencyKey,
842
+ identityId: normalizeString(responseIdentity.id) || null,
843
+ emailAddress: normalizeString(responseIdentity.emailAddress) || null,
844
+ status: normalizeString(responseIdentity.status) || null,
845
+ expiresAt: responseIdentity.expiresAt || null,
846
+ response: responseIdentity,
847
+ };
848
+ });
849
+
850
+ for (const identity of provisioned) {
851
+ await recordProvisionedIdentity({
852
+ outputRoot,
853
+ response: identity.response || {},
854
+ context: {
855
+ source: "session-provision-emails",
856
+ apiUrl,
857
+ orgId: credentials.orgId,
858
+ projectId: credentials.projectId,
859
+ idempotencyKey: identity.idempotencyKey,
860
+ tags,
861
+ },
862
+ });
863
+ }
864
+
865
+ const identityIds = provisioned
866
+ .map((identity) => normalizeString(identity.identityId))
867
+ .filter(Boolean);
868
+ const updatedSession = await recordSessionProvisionedIdentities(normalizedSessionId, {
869
+ targetPath,
870
+ identityIds,
871
+ tags,
872
+ });
873
+ const streamEvent = await appendToStream(
874
+ normalizedSessionId,
875
+ createAgentEvent({
876
+ event: "session_provision_emails",
877
+ agentId: "senti",
878
+ agentModel: "gpt-5.4-mini",
879
+ sessionId: normalizedSessionId,
880
+ payload: {
881
+ requestedCount: count,
882
+ provisionedCount: provisioned.length,
883
+ identityIds,
884
+ tags,
885
+ ttlHours,
886
+ concurrency,
887
+ },
888
+ }),
889
+ { targetPath }
890
+ );
891
+
892
+ const durationMs = Date.now() - startedAt;
893
+ const payload = {
894
+ command: "session provision-emails",
895
+ execute: true,
896
+ targetPath,
897
+ outputRoot,
898
+ durationMs,
899
+ sessionId: normalizedSessionId,
900
+ apiUrl,
901
+ requestedCount: count,
902
+ provisionedCount: provisioned.length,
903
+ concurrency,
904
+ tags,
905
+ ttlHours,
906
+ identities: provisioned,
907
+ sharedResources: updatedSession.sharedResources,
908
+ event: streamEvent,
909
+ };
910
+
911
+ if (shouldEmitJson(options, command)) {
912
+ console.log(JSON.stringify(payload, null, 2));
913
+ return;
914
+ }
915
+ console.log(pc.bold(`Provisioned ${provisioned.length} identities for session ${normalizedSessionId}`));
916
+ console.log(pc.gray(`concurrency=${concurrency} duration_ms=${durationMs}`));
917
+ });
918
+
919
+ session
920
+ .command("admin-kill <sessionId>")
921
+ .description("Admin: kill a remote session through sentinelayer-api")
922
+ .option("--reason <reason>", "Kill reason", "admin_kill")
923
+ .option("--api-url <url>", "Override Sentinelayer API base URL")
924
+ .option("--path <path>", "Workspace path for local stream sync", ".")
925
+ .option("--json", "Emit machine-readable output")
926
+ .action(async (sessionId, options, command) => {
927
+ const normalizedSessionId = normalizeString(sessionId);
928
+ if (!normalizedSessionId) {
929
+ throw new Error("session id is required.");
930
+ }
931
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
932
+ const reason = normalizeString(options.reason) || "admin_kill";
933
+
934
+ let apiSession;
935
+ try {
936
+ apiSession = await resolveAdminApiSession({
937
+ targetPath,
938
+ explicitApiUrl: options.apiUrl,
939
+ });
940
+ } catch (error) {
941
+ throw new Error(formatApiError(error));
942
+ }
943
+
944
+ let result;
945
+ try {
946
+ result = await postAdminSessionMutation({
947
+ session: apiSession,
948
+ pathSuffix: `/api/v1/admin/sessions/${encodeURIComponent(normalizedSessionId)}/kill`,
949
+ operationName: "session-admin-kill",
950
+ body: { reason },
951
+ });
952
+ } catch (error) {
953
+ throw new Error(formatApiError(error));
954
+ }
955
+
956
+ let localEvent = null;
957
+ try {
958
+ localEvent = await emitLocalAdminKillEvent(normalizedSessionId, {
959
+ targetPath,
960
+ reason,
961
+ scope: "session",
962
+ apiResult: result,
963
+ });
964
+ } catch {
965
+ localEvent = null;
966
+ }
967
+
968
+ const payload = {
969
+ command: "session admin-kill",
970
+ targetPath,
971
+ sessionId: normalizedSessionId,
972
+ reason,
973
+ apiUrl: apiSession.apiUrl,
974
+ tokenSource: apiSession.source,
975
+ result,
976
+ localEventEmitted: Boolean(localEvent),
977
+ };
978
+ if (shouldEmitJson(options, command)) {
979
+ console.log(JSON.stringify(payload, null, 2));
980
+ return;
981
+ }
982
+ console.log(pc.bold(`Admin kill completed for session ${normalizedSessionId}`));
983
+ console.log(pc.gray(`api=${apiSession.apiUrl} source=${apiSession.source} reason=${reason}`));
984
+ if (payload.localEventEmitted) {
985
+ console.log(pc.gray("Local stream event emitted."));
986
+ }
987
+ });
988
+
989
+ session
990
+ .command("admin-kill-all")
991
+ .description("Admin: kill all active remote sessions (requires --confirm)")
992
+ .option("--confirm", "Required confirmation flag")
993
+ .option("--reason <reason>", "Kill reason", "admin_global_kill")
994
+ .option("--api-url <url>", "Override Sentinelayer API base URL")
995
+ .option("--path <path>", "Workspace path for local stream sync", ".")
996
+ .option("--json", "Emit machine-readable output")
997
+ .action(async (options, command) => {
998
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
999
+ const reason = normalizeString(options.reason) || "admin_global_kill";
1000
+ const emitJson = shouldEmitJson(options, command);
1001
+
1002
+ if (!options.confirm) {
1003
+ const confirmationMessage = "This will kill ALL active sessions. Pass --confirm to proceed.";
1004
+ const blockedPayload = {
1005
+ command: "session admin-kill-all",
1006
+ targetPath,
1007
+ blocked: true,
1008
+ reason,
1009
+ error: confirmationMessage,
1010
+ };
1011
+ if (emitJson) {
1012
+ console.log(JSON.stringify(blockedPayload, null, 2));
1013
+ } else {
1014
+ console.error(pc.red(confirmationMessage));
1015
+ }
1016
+ process.exitCode = 1;
1017
+ return;
1018
+ }
1019
+
1020
+ let apiSession;
1021
+ try {
1022
+ apiSession = await resolveAdminApiSession({
1023
+ targetPath,
1024
+ explicitApiUrl: options.apiUrl,
1025
+ });
1026
+ } catch (error) {
1027
+ throw new Error(formatApiError(error));
1028
+ }
1029
+
1030
+ let result;
1031
+ try {
1032
+ result = await postAdminSessionMutation({
1033
+ session: apiSession,
1034
+ pathSuffix: "/api/v1/admin/sessions/kill-all",
1035
+ operationName: "session-admin-kill-all",
1036
+ headers: {
1037
+ "X-Confirm-Kill-All": "true",
1038
+ },
1039
+ body: { reason },
1040
+ });
1041
+ } catch (error) {
1042
+ throw new Error(formatApiError(error));
1043
+ }
1044
+
1045
+ const localSessions = await listActiveSessions({ targetPath });
1046
+ const localSessionIds = [];
1047
+ for (const item of localSessions) {
1048
+ try {
1049
+ const event = await emitLocalAdminKillEvent(item.sessionId, {
1050
+ targetPath,
1051
+ reason,
1052
+ scope: "global",
1053
+ apiResult: result,
1054
+ });
1055
+ if (event) {
1056
+ localSessionIds.push(item.sessionId);
1057
+ }
1058
+ } catch {
1059
+ // Best effort local mirror only.
1060
+ }
1061
+ }
1062
+
1063
+ const payload = {
1064
+ command: "session admin-kill-all",
1065
+ targetPath,
1066
+ reason,
1067
+ apiUrl: apiSession.apiUrl,
1068
+ tokenSource: apiSession.source,
1069
+ result,
1070
+ localEventsEmitted: localSessionIds.length,
1071
+ localSessionIds,
1072
+ };
1073
+ if (emitJson) {
1074
+ console.log(JSON.stringify(payload, null, 2));
1075
+ return;
1076
+ }
1077
+ console.log(pc.bold("Admin kill-all completed"));
1078
+ console.log(pc.gray(`api=${apiSession.apiUrl} source=${apiSession.source} reason=${reason}`));
1079
+ if (localSessionIds.length > 0) {
1080
+ console.log(pc.gray(`local_events_emitted=${localSessionIds.length}`));
1081
+ }
1082
+ });
1083
+
1084
+ session
1085
+ .command("kill")
1086
+ .description("Kill a single agent or all agents in a session")
1087
+ .option("--agent <id>", "Specific agent id to stop")
1088
+ .option("--all", "Kill every known agent in the session")
1089
+ .option("--session <id>", "Session id")
1090
+ .option("--id <sessionId>", "Deprecated alias for --session")
1091
+ .option("--path <path>", "Workspace path for the session", ".")
1092
+ .option("--reason <reason>", "Kill reason code", "manual_stop")
1093
+ .option("--json", "Emit machine-readable output")
1094
+ .action(async (options, command) => {
1095
+ const sessionId = resolveSessionIdOption(options);
1096
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
1097
+ const reason = normalizeString(options.reason) || "manual_stop";
1098
+ const requestedAgent = normalizeString(options.agent).toLowerCase();
1099
+
1100
+ if (!options.all && !requestedAgent) {
1101
+ throw new Error("session kill requires --agent <id> or --all.");
1102
+ }
1103
+
1104
+ const startedAt = Date.now();
1105
+ const discoveredAgents = await listAgents(sessionId, {
1106
+ targetPath,
1107
+ includeInactive: false,
1108
+ });
1109
+ const agentsToKill = new Set();
1110
+ if (options.all) {
1111
+ agentsToKill.add("senti");
1112
+ agentsToKill.add("scope-engine");
1113
+ for (const agent of discoveredAgents) {
1114
+ const agentId = normalizeString(agent.agentId).toLowerCase();
1115
+ if (agentId) {
1116
+ agentsToKill.add(agentId);
1117
+ }
1118
+ }
1119
+ } else {
1120
+ agentsToKill.add(requestedAgent);
1121
+ }
1122
+
1123
+ const results = [];
1124
+ let runtimeStops = 0;
1125
+ let scopeStops = 0;
1126
+ let leaseRevocations = 0;
1127
+ let lockRevocations = 0;
1128
+ let anyStopped = false;
1129
+
1130
+ for (const agentId of agentsToKill) {
1131
+ let stopped = false;
1132
+ let stopDetails = {};
1133
+ if (agentId === "senti") {
1134
+ const stopResult = await stopSenti(sessionId, {
1135
+ targetPath,
1136
+ reason,
1137
+ });
1138
+ runtimeStops += Number(stopResult?.runtimeStopSummary?.stoppedCount || 0);
1139
+ stopped = Boolean(stopResult?.stopped);
1140
+ stopDetails = {
1141
+ runtimeStops: Number(stopResult?.runtimeStopSummary?.stoppedCount || 0),
1142
+ scopeStops: 0,
1143
+ };
1144
+ } else if (agentId === "scope-engine") {
1145
+ const stopResult = await stopScopeEngine({
1146
+ targetPath,
1147
+ sessionId,
1148
+ reason,
1149
+ });
1150
+ scopeStops += Number(stopResult?.count || 0);
1151
+ stopped = Boolean(stopResult?.stopped);
1152
+ stopDetails = {
1153
+ runtimeStops: 0,
1154
+ scopeStops: Number(stopResult?.count || 0),
1155
+ };
1156
+ } else {
1157
+ try {
1158
+ await unregisterAgent(sessionId, agentId, {
1159
+ reason: "killed",
1160
+ targetPath,
1161
+ });
1162
+ stopped = true;
1163
+ } catch {
1164
+ stopped = false;
1165
+ }
1166
+ if (stopped) {
1167
+ await emitAgentKilledEvent(sessionId, agentId, {
1168
+ targetPath,
1169
+ reason,
1170
+ leaseRevocations: 0,
1171
+ });
1172
+ }
1173
+ stopDetails = {
1174
+ runtimeStops: 0,
1175
+ scopeStops: 0,
1176
+ };
1177
+ }
1178
+
1179
+ const releasedCount = await revokeAgentLeases(sessionId, agentId, {
1180
+ targetPath,
1181
+ reason: `agent_killed:${reason}`,
1182
+ });
1183
+ leaseRevocations += releasedCount;
1184
+
1185
+ const releasedLocks = await releaseFileLocksForAgent(sessionId, agentId, {
1186
+ targetPath,
1187
+ reason: `agent_killed:${reason}`,
1188
+ actorAgentId: "senti",
1189
+ });
1190
+ lockRevocations += Number(releasedLocks.releasedCount || 0);
1191
+ anyStopped = anyStopped || stopped;
1192
+
1193
+ results.push({
1194
+ agentId,
1195
+ stopped,
1196
+ runtimeStops: stopDetails.runtimeStops,
1197
+ scopeStops: stopDetails.scopeStops,
1198
+ leaseRevocations: releasedCount,
1199
+ lockRevocations: Number(releasedLocks.releasedCount || 0),
1200
+ });
1201
+ }
1202
+
1203
+ const durationMs = Date.now() - startedAt;
1204
+ const primaryAgentId = !options.all ? requestedAgent : null;
1205
+ const payload = {
1206
+ command: "session kill",
1207
+ targetPath,
1208
+ durationMs,
1209
+ sessionId,
1210
+ agentId: primaryAgentId,
1211
+ all: Boolean(options.all),
1212
+ reason,
1213
+ stopped: anyStopped,
1214
+ runtimeStops,
1215
+ scopeStops,
1216
+ leaseRevocations,
1217
+ lockRevocations,
1218
+ results,
1219
+ };
1220
+
1221
+ if (shouldEmitJson(options, command)) {
1222
+ console.log(JSON.stringify(payload, null, 2));
1223
+ return;
1224
+ }
1225
+
1226
+ if (payload.stopped) {
1227
+ console.log(pc.bold("Kill complete"));
1228
+ } else {
1229
+ console.log(pc.yellow(`No active target found in session ${sessionId}.`));
1230
+ }
1231
+ console.log(
1232
+ pc.gray(
1233
+ `session=${sessionId} runtime_stops=${runtimeStops} scope_stops=${scopeStops} lease_revocations=${leaseRevocations} lock_revocations=${lockRevocations}`
1234
+ )
1235
+ );
1236
+ console.log(`stopped=${payload.stopped} reason=${reason} duration_ms=${durationMs}`);
1237
+ });
1238
+ }