sentinelayer-cli 0.8.0 → 0.8.2

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 (153) hide show
  1. package/README.md +23 -2
  2. package/package.json +4 -4
  3. package/src/agents/ai-governance/index.js +12 -0
  4. package/src/agents/ai-governance/tools/base.js +171 -0
  5. package/src/agents/ai-governance/tools/eval-regression.js +47 -0
  6. package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
  7. package/src/agents/ai-governance/tools/index.js +52 -0
  8. package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
  9. package/src/agents/ai-governance/tools/provenance-check.js +69 -0
  10. package/src/agents/backend/index.js +12 -0
  11. package/src/agents/backend/tools/base.js +189 -0
  12. package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
  13. package/src/agents/backend/tools/idempotency-audit.js +105 -0
  14. package/src/agents/backend/tools/index.js +87 -0
  15. package/src/agents/backend/tools/retry-audit.js +132 -0
  16. package/src/agents/backend/tools/timeout-audit.js +144 -0
  17. package/src/agents/code-quality/index.js +12 -0
  18. package/src/agents/code-quality/tools/base.js +159 -0
  19. package/src/agents/code-quality/tools/complexity-measure.js +197 -0
  20. package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
  21. package/src/agents/code-quality/tools/cycle-detect.js +49 -0
  22. package/src/agents/code-quality/tools/dep-graph.js +196 -0
  23. package/src/agents/code-quality/tools/index.js +89 -0
  24. package/src/agents/data-layer/index.js +12 -0
  25. package/src/agents/data-layer/tools/base.js +181 -0
  26. package/src/agents/data-layer/tools/index-audit.js +165 -0
  27. package/src/agents/data-layer/tools/index.js +83 -0
  28. package/src/agents/data-layer/tools/migration-scan.js +135 -0
  29. package/src/agents/data-layer/tools/query-explain.js +120 -0
  30. package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
  31. package/src/agents/documentation/index.js +12 -0
  32. package/src/agents/documentation/tools/api-diff.js +91 -0
  33. package/src/agents/documentation/tools/base.js +151 -0
  34. package/src/agents/documentation/tools/dead-link-check.js +58 -0
  35. package/src/agents/documentation/tools/docstring-coverage.js +78 -0
  36. package/src/agents/documentation/tools/index.js +52 -0
  37. package/src/agents/documentation/tools/readme-freshness.js +61 -0
  38. package/src/agents/envelope/fix-cycle.js +45 -0
  39. package/src/agents/envelope/index.js +31 -0
  40. package/src/agents/envelope/loop.js +150 -0
  41. package/src/agents/envelope/pulse.js +18 -0
  42. package/src/agents/envelope/stream.js +40 -0
  43. package/src/agents/infrastructure/index.js +12 -0
  44. package/src/agents/infrastructure/tools/base.js +171 -0
  45. package/src/agents/infrastructure/tools/checkov-run.js +32 -0
  46. package/src/agents/infrastructure/tools/drift-detect.js +59 -0
  47. package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
  48. package/src/agents/infrastructure/tools/index.js +52 -0
  49. package/src/agents/infrastructure/tools/tflint-run.js +31 -0
  50. package/src/agents/jules/loop.js +7 -4
  51. package/src/agents/jules/swarm/sub-agent.js +5 -1
  52. package/src/agents/jules/tools/auth-audit.js +10 -1
  53. package/src/agents/mode.js +113 -0
  54. package/src/agents/observability/index.js +12 -0
  55. package/src/agents/observability/tools/alert-audit.js +39 -0
  56. package/src/agents/observability/tools/base.js +181 -0
  57. package/src/agents/observability/tools/dashboard-gap.js +42 -0
  58. package/src/agents/observability/tools/index.js +54 -0
  59. package/src/agents/observability/tools/log-schema-check.js +74 -0
  60. package/src/agents/observability/tools/span-coverage.js +74 -0
  61. package/src/agents/persona-visuals.js +38 -0
  62. package/src/agents/release/index.js +12 -0
  63. package/src/agents/release/tools/base.js +181 -0
  64. package/src/agents/release/tools/changelog-diff.js +86 -0
  65. package/src/agents/release/tools/feature-flag-audit.js +126 -0
  66. package/src/agents/release/tools/index.js +61 -0
  67. package/src/agents/release/tools/rollback-verify.js +129 -0
  68. package/src/agents/release/tools/semver-check.js +109 -0
  69. package/src/agents/reliability/index.js +12 -0
  70. package/src/agents/reliability/tools/backpressure-check.js +129 -0
  71. package/src/agents/reliability/tools/base.js +181 -0
  72. package/src/agents/reliability/tools/chaos-probe.js +109 -0
  73. package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
  74. package/src/agents/reliability/tools/health-check-audit.js +111 -0
  75. package/src/agents/reliability/tools/index.js +87 -0
  76. package/src/agents/run-persona.js +109 -0
  77. package/src/agents/security/index.js +12 -0
  78. package/src/agents/security/tools/authz-audit.js +134 -0
  79. package/src/agents/security/tools/base.js +190 -0
  80. package/src/agents/security/tools/crypto-review.js +175 -0
  81. package/src/agents/security/tools/index.js +97 -0
  82. package/src/agents/security/tools/sast-scan.js +175 -0
  83. package/src/agents/security/tools/secrets-scan.js +216 -0
  84. package/src/agents/supply-chain/index.js +12 -0
  85. package/src/agents/supply-chain/tools/attestation-check.js +42 -0
  86. package/src/agents/supply-chain/tools/base.js +151 -0
  87. package/src/agents/supply-chain/tools/index.js +52 -0
  88. package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
  89. package/src/agents/supply-chain/tools/package-verify.js +56 -0
  90. package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
  91. package/src/agents/testing/index.js +12 -0
  92. package/src/agents/testing/tools/base.js +202 -0
  93. package/src/agents/testing/tools/coverage-gap.js +144 -0
  94. package/src/agents/testing/tools/flake-detect.js +125 -0
  95. package/src/agents/testing/tools/index.js +85 -0
  96. package/src/agents/testing/tools/mutation-test.js +143 -0
  97. package/src/agents/testing/tools/snapshot-diff.js +103 -0
  98. package/src/auth/gate.js +65 -37
  99. package/src/cli.js +1 -1
  100. package/src/commands/chat.js +3 -10
  101. package/src/commands/legacy-args.js +10 -0
  102. package/src/commands/omargate.js +36 -2
  103. package/src/commands/persona.js +46 -1
  104. package/src/commands/scan.js +3 -10
  105. package/src/commands/session.js +654 -6
  106. package/src/commands/spec.js +3 -10
  107. package/src/coord/events-log.js +141 -0
  108. package/src/coord/handshake.js +719 -0
  109. package/src/coord/index.js +35 -0
  110. package/src/coord/paths.js +84 -0
  111. package/src/coord/priority.js +62 -0
  112. package/src/coord/tarjan.js +157 -0
  113. package/src/cost/tokenizer.js +160 -0
  114. package/src/cost/tracker.js +61 -0
  115. package/src/daemon/artifact-lineage.js +362 -0
  116. package/src/daemon/assignment-ledger.js +117 -0
  117. package/src/daemon/ast-drift.js +496 -0
  118. package/src/daemon/ingest-refresh.js +69 -2
  119. package/src/ingest/engine.js +15 -0
  120. package/src/ingest/ownership.js +380 -0
  121. package/src/legacy-cli.js +68 -1
  122. package/src/orchestrator/kai-chen.js +126 -0
  123. package/src/review/ai-review.js +3 -10
  124. package/src/review/compliance-pack.js +389 -0
  125. package/src/review/investor-dd-config.js +54 -0
  126. package/src/review/investor-dd-file-loop.js +303 -0
  127. package/src/review/investor-dd-file-router.js +406 -0
  128. package/src/review/investor-dd-html-report.js +233 -0
  129. package/src/review/investor-dd-notification.js +120 -0
  130. package/src/review/investor-dd-orchestrator.js +405 -0
  131. package/src/review/investor-dd-persona-runner.js +275 -0
  132. package/src/review/live-validator.js +253 -0
  133. package/src/review/omargate-orchestrator.js +90 -2
  134. package/src/review/persona-prompts.js +244 -56
  135. package/src/review/reconciliation-rules.js +329 -0
  136. package/src/review/reproducibility-chain.js +136 -0
  137. package/src/review/scan-modes.js +102 -3
  138. package/src/session/agent-registry.js +7 -0
  139. package/src/session/analytics.js +479 -0
  140. package/src/session/daemon.js +609 -14
  141. package/src/session/file-locks.js +666 -0
  142. package/src/session/paths.js +4 -0
  143. package/src/session/recap.js +567 -0
  144. package/src/session/redact.js +82 -0
  145. package/src/session/runtime-bridge.js +24 -1
  146. package/src/session/scoring.js +406 -0
  147. package/src/session/setup-guides.js +304 -0
  148. package/src/session/store.js +318 -2
  149. package/src/session/stream.js +9 -1
  150. package/src/session/sync.js +753 -0
  151. package/src/session/tasks.js +1054 -0
  152. package/src/session/templates.js +188 -0
  153. package/src/swarm/runtime.js +1 -8
@@ -1,8 +1,21 @@
1
1
  import path from "node:path";
2
2
  import process from "node:process";
3
+ import { randomUUID } from "node:crypto";
3
4
 
4
5
  import pc from "picocolors";
5
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";
6
19
  import {
7
20
  listAssignments,
8
21
  releaseLease,
@@ -17,13 +30,32 @@ import {
17
30
  } from "../session/agent-registry.js";
18
31
  import { stopSenti } from "../session/daemon.js";
19
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";
20
42
  import {
21
43
  createSession,
22
44
  DEFAULT_TTL_SECONDS,
23
45
  getSession,
24
46
  listActiveSessions,
47
+ recordSessionProvisionedIdentities,
25
48
  } from "../session/store.js";
26
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";
27
59
 
28
60
  function shouldEmitJson(options, command) {
29
61
  const local = Boolean(options && options.json);
@@ -55,6 +87,29 @@ function normalizeAgentId(value, fallbackValue = "cli-user") {
55
87
  return normalized || fallbackValue;
56
88
  }
57
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
+
58
113
  function resolveSessionIdOption(options = {}) {
59
114
  const sessionId = normalizeString(options.session || options.id);
60
115
  if (!sessionId) {
@@ -75,6 +130,78 @@ function formatEventLine(event = {}) {
75
130
  return `${ts} ${agentId} ${type}`;
76
131
  }
77
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
+
78
205
  async function revokeAgentLeases(sessionId, agentId, { targetPath, reason } = {}) {
79
206
  const active = await listAssignments({
80
207
  targetPath,
@@ -127,21 +254,36 @@ export function registerSessionCommand(program) {
127
254
  .command("start")
128
255
  .description("Create a new persistent session with metadata + NDJSON stream")
129
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
+ )
130
261
  .option(
131
262
  "--ttl-seconds <seconds>",
132
- `Session time-to-live in seconds (default ${DEFAULT_TTL_SECONDS})`,
133
- String(DEFAULT_TTL_SECONDS)
263
+ `Session time-to-live in seconds (default ${DEFAULT_TTL_SECONDS}; template defaults override when omitted)`
134
264
  )
135
265
  .option("--json", "Emit machine-readable output")
136
266
  .action(async (options, command) => {
137
267
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
138
- const ttlSeconds = parsePositiveInteger(options.ttlSeconds, "ttl-seconds", DEFAULT_TTL_SECONDS);
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
+ );
139
278
  const startedAt = Date.now();
140
279
  const created = await createSession({
141
280
  targetPath,
142
281
  ttlSeconds,
282
+ template,
143
283
  });
144
284
  const durationMs = Date.now() - startedAt;
285
+ const launchPlan = template ? buildTemplateLaunchPlan(created.sessionId, template) : [];
286
+ const dashboardUrl = buildDashboardUrl(created.sessionId);
145
287
 
146
288
  const payload = {
147
289
  command: "session start",
@@ -153,16 +295,46 @@ export function registerSessionCommand(program) {
153
295
  streamPath: created.streamPath,
154
296
  createdAt: created.createdAt,
155
297
  expiresAt: created.expiresAt,
298
+ ttlSeconds,
156
299
  elapsedTimer: created.elapsedTimer,
157
300
  renewalCount: created.renewalCount,
158
301
  status: created.status,
302
+ template: created.template,
303
+ launchPlan,
304
+ dashboardUrl,
159
305
  };
160
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
+
161
319
  if (shouldEmitJson(options, command)) {
162
320
  console.log(JSON.stringify(payload, null, 2));
163
321
  return;
164
322
  }
165
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
+
166
338
  console.log(pc.bold("Session created"));
167
339
  console.log(pc.gray(`Session: ${created.sessionId}`));
168
340
  console.log(pc.gray(`Stream: ${created.streamPath}`));
@@ -172,6 +344,26 @@ export function registerSessionCommand(program) {
172
344
  );
173
345
  });
174
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
+
175
367
  session
176
368
  .command("join <sessionId>")
177
369
  .description("Join an active session")
@@ -325,7 +517,7 @@ export function registerSessionCommand(program) {
325
517
  throw new Error(`Session '${normalizedSessionId}' was not found.`);
326
518
  }
327
519
 
328
- const [agents, runtimeRuns, leases, recentEvents] = await Promise.all([
520
+ const [agents, runtimeRuns, leases, fileLocks, activeTasks, recentEvents] = await Promise.all([
329
521
  listAgents(normalizedSessionId, {
330
522
  targetPath,
331
523
  includeInactive: false,
@@ -344,6 +536,15 @@ export function registerSessionCommand(program) {
344
536
  includeExpired: true,
345
537
  limit: 100,
346
538
  }),
539
+ listFileLocks(normalizedSessionId, {
540
+ targetPath,
541
+ emitExpiredEvents: false,
542
+ }),
543
+ listSessionTasks(normalizedSessionId, {
544
+ targetPath,
545
+ statuses: ["PENDING", "ACCEPTED"],
546
+ limit: 100,
547
+ }),
347
548
  readStream(normalizedSessionId, {
348
549
  targetPath,
349
550
  tail: 10,
@@ -360,6 +561,8 @@ export function registerSessionCommand(program) {
360
561
  staleAgents,
361
562
  runtimeRuns,
362
563
  activeLeases: leases.assignments,
564
+ activeFileLocks: fileLocks,
565
+ activeTasks: activeTasks.tasks,
363
566
  recentEvents,
364
567
  };
365
568
  if (shouldEmitJson(options, command)) {
@@ -370,7 +573,7 @@ export function registerSessionCommand(program) {
370
573
  console.log(pc.bold(`Session ${normalizedSessionId}`));
371
574
  console.log(
372
575
  pc.gray(
373
- `status=${sessionPayload.status} agents=${agents.length} stale=${staleAgents.length} runs=${runtimeRuns.length} leases=${leases.assignments.length}`
576
+ `status=${sessionPayload.status} agents=${agents.length} stale=${staleAgents.length} runs=${runtimeRuns.length} leases=${leases.assignments.length} locks=${fileLocks.length} tasks=${activeTasks.tasks.length}`
374
577
  )
375
578
  );
376
579
  for (const event of recentEvents) {
@@ -443,6 +646,441 @@ export function registerSessionCommand(program) {
443
646
  }
444
647
  });
445
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
+
446
1084
  session
447
1085
  .command("kill")
448
1086
  .description("Kill a single agent or all agents in a session")
@@ -486,6 +1124,7 @@ export function registerSessionCommand(program) {
486
1124
  let runtimeStops = 0;
487
1125
  let scopeStops = 0;
488
1126
  let leaseRevocations = 0;
1127
+ let lockRevocations = 0;
489
1128
  let anyStopped = false;
490
1129
 
491
1130
  for (const agentId of agentsToKill) {
@@ -542,6 +1181,13 @@ export function registerSessionCommand(program) {
542
1181
  reason: `agent_killed:${reason}`,
543
1182
  });
544
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);
545
1191
  anyStopped = anyStopped || stopped;
546
1192
 
547
1193
  results.push({
@@ -550,6 +1196,7 @@ export function registerSessionCommand(program) {
550
1196
  runtimeStops: stopDetails.runtimeStops,
551
1197
  scopeStops: stopDetails.scopeStops,
552
1198
  leaseRevocations: releasedCount,
1199
+ lockRevocations: Number(releasedLocks.releasedCount || 0),
553
1200
  });
554
1201
  }
555
1202
 
@@ -567,6 +1214,7 @@ export function registerSessionCommand(program) {
567
1214
  runtimeStops,
568
1215
  scopeStops,
569
1216
  leaseRevocations,
1217
+ lockRevocations,
570
1218
  results,
571
1219
  };
572
1220
 
@@ -582,7 +1230,7 @@ export function registerSessionCommand(program) {
582
1230
  }
583
1231
  console.log(
584
1232
  pc.gray(
585
- `session=${sessionId} runtime_stops=${runtimeStops} scope_stops=${scopeStops} lease_revocations=${leaseRevocations}`
1233
+ `session=${sessionId} runtime_stops=${runtimeStops} scope_stops=${scopeStops} lease_revocations=${leaseRevocations} lock_revocations=${lockRevocations}`
586
1234
  )
587
1235
  );
588
1236
  console.log(`stopped=${payload.stopped} reason=${reason} duration_ms=${durationMs}`);