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.
- package/README.md +1009 -996
- package/bin/create-sentinelayer.js +5 -5
- package/bin/sentinelayer-cli.js +4 -4
- package/bin/sl.js +5 -5
- package/package.json +64 -63
- package/src/agents/ai-governance/index.js +12 -0
- package/src/agents/ai-governance/tools/base.js +171 -0
- package/src/agents/ai-governance/tools/eval-regression.js +47 -0
- package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
- package/src/agents/ai-governance/tools/index.js +52 -0
- package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
- package/src/agents/ai-governance/tools/provenance-check.js +69 -0
- package/src/agents/backend/index.js +12 -0
- package/src/agents/backend/tools/base.js +189 -0
- package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
- package/src/agents/backend/tools/idempotency-audit.js +105 -0
- package/src/agents/backend/tools/index.js +87 -0
- package/src/agents/backend/tools/retry-audit.js +132 -0
- package/src/agents/backend/tools/timeout-audit.js +144 -0
- package/src/agents/code-quality/index.js +12 -0
- package/src/agents/code-quality/tools/base.js +159 -0
- package/src/agents/code-quality/tools/complexity-measure.js +197 -0
- package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
- package/src/agents/code-quality/tools/cycle-detect.js +49 -0
- package/src/agents/code-quality/tools/dep-graph.js +196 -0
- package/src/agents/code-quality/tools/index.js +89 -0
- package/src/agents/data-layer/index.js +12 -0
- package/src/agents/data-layer/tools/base.js +181 -0
- package/src/agents/data-layer/tools/index-audit.js +165 -0
- package/src/agents/data-layer/tools/index.js +83 -0
- package/src/agents/data-layer/tools/migration-scan.js +135 -0
- package/src/agents/data-layer/tools/query-explain.js +120 -0
- package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
- package/src/agents/documentation/index.js +12 -0
- package/src/agents/documentation/tools/api-diff.js +91 -0
- package/src/agents/documentation/tools/base.js +151 -0
- package/src/agents/documentation/tools/dead-link-check.js +58 -0
- package/src/agents/documentation/tools/docstring-coverage.js +78 -0
- package/src/agents/documentation/tools/index.js +52 -0
- package/src/agents/documentation/tools/readme-freshness.js +61 -0
- package/src/agents/envelope/fix-cycle.js +45 -0
- package/src/agents/envelope/index.js +31 -0
- package/src/agents/envelope/loop.js +150 -0
- package/src/agents/envelope/pulse.js +18 -0
- package/src/agents/envelope/stream.js +40 -0
- package/src/agents/infrastructure/index.js +12 -0
- package/src/agents/infrastructure/tools/base.js +171 -0
- package/src/agents/infrastructure/tools/checkov-run.js +32 -0
- package/src/agents/infrastructure/tools/drift-detect.js +59 -0
- package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
- package/src/agents/infrastructure/tools/index.js +52 -0
- package/src/agents/infrastructure/tools/tflint-run.js +31 -0
- package/src/agents/jules/config/definition.js +160 -160
- package/src/agents/jules/config/system-prompt.js +182 -182
- package/src/agents/jules/error-intake.js +51 -51
- package/src/agents/jules/fix-cycle.js +17 -17
- package/src/agents/jules/loop.js +460 -450
- package/src/agents/jules/pulse.js +10 -10
- package/src/agents/jules/stream.js +187 -186
- package/src/agents/jules/swarm/file-scanner.js +74 -74
- package/src/agents/jules/swarm/index.js +11 -11
- package/src/agents/jules/swarm/orchestrator.js +362 -362
- package/src/agents/jules/swarm/pattern-hunter.js +123 -123
- package/src/agents/jules/swarm/sub-agent.js +315 -309
- package/src/agents/jules/tools/aidenid-email.js +189 -189
- package/src/agents/jules/tools/auth-audit.js +1708 -1691
- package/src/agents/jules/tools/dispatch.js +340 -335
- package/src/agents/jules/tools/file-edit.js +2 -2
- package/src/agents/jules/tools/file-read.js +2 -2
- package/src/agents/jules/tools/frontend-analyze.js +570 -570
- package/src/agents/jules/tools/glob.js +2 -2
- package/src/agents/jules/tools/grep.js +2 -2
- package/src/agents/jules/tools/index.js +29 -29
- package/src/agents/jules/tools/path-guards.js +2 -2
- package/src/agents/jules/tools/runtime-audit.js +507 -507
- package/src/agents/jules/tools/shell.js +2 -2
- package/src/agents/jules/tools/url-policy.js +100 -100
- package/src/agents/mode.js +113 -0
- package/src/agents/observability/index.js +12 -0
- package/src/agents/observability/tools/alert-audit.js +39 -0
- package/src/agents/observability/tools/base.js +181 -0
- package/src/agents/observability/tools/dashboard-gap.js +42 -0
- package/src/agents/observability/tools/index.js +54 -0
- package/src/agents/observability/tools/log-schema-check.js +74 -0
- package/src/agents/observability/tools/span-coverage.js +74 -0
- package/src/agents/persona-visuals.js +102 -61
- package/src/agents/release/index.js +12 -0
- package/src/agents/release/tools/base.js +181 -0
- package/src/agents/release/tools/changelog-diff.js +86 -0
- package/src/agents/release/tools/feature-flag-audit.js +126 -0
- package/src/agents/release/tools/index.js +61 -0
- package/src/agents/release/tools/rollback-verify.js +129 -0
- package/src/agents/release/tools/semver-check.js +109 -0
- package/src/agents/reliability/index.js +12 -0
- package/src/agents/reliability/tools/backpressure-check.js +129 -0
- package/src/agents/reliability/tools/base.js +181 -0
- package/src/agents/reliability/tools/chaos-probe.js +109 -0
- package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
- package/src/agents/reliability/tools/health-check-audit.js +111 -0
- package/src/agents/reliability/tools/index.js +87 -0
- package/src/agents/run-persona.js +109 -0
- package/src/agents/security/index.js +12 -0
- package/src/agents/security/tools/authz-audit.js +134 -0
- package/src/agents/security/tools/base.js +190 -0
- package/src/agents/security/tools/crypto-review.js +175 -0
- package/src/agents/security/tools/index.js +97 -0
- package/src/agents/security/tools/sast-scan.js +175 -0
- package/src/agents/security/tools/secrets-scan.js +216 -0
- package/src/agents/shared-tools/dispatch-core.js +320 -315
- package/src/agents/shared-tools/file-edit.js +180 -180
- package/src/agents/shared-tools/file-read.js +100 -100
- package/src/agents/shared-tools/glob.js +168 -168
- package/src/agents/shared-tools/grep.js +228 -228
- package/src/agents/shared-tools/index.js +46 -46
- package/src/agents/shared-tools/path-guards.js +161 -161
- package/src/agents/shared-tools/shell.js +383 -383
- package/src/agents/supply-chain/index.js +12 -0
- package/src/agents/supply-chain/tools/attestation-check.js +42 -0
- package/src/agents/supply-chain/tools/base.js +151 -0
- package/src/agents/supply-chain/tools/index.js +52 -0
- package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
- package/src/agents/supply-chain/tools/package-verify.js +56 -0
- package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
- package/src/agents/testing/index.js +12 -0
- package/src/agents/testing/tools/base.js +202 -0
- package/src/agents/testing/tools/coverage-gap.js +144 -0
- package/src/agents/testing/tools/flake-detect.js +125 -0
- package/src/agents/testing/tools/index.js +85 -0
- package/src/agents/testing/tools/mutation-test.js +143 -0
- package/src/agents/testing/tools/snapshot-diff.js +103 -0
- package/src/ai/aidenid.js +1021 -1009
- package/src/ai/client.js +553 -553
- package/src/ai/domain-target-store.js +268 -268
- package/src/ai/identity-store.js +270 -270
- package/src/ai/proxy.js +137 -137
- package/src/ai/site-store.js +145 -145
- package/src/audit/agents/architecture.js +180 -180
- package/src/audit/agents/compliance.js +179 -179
- package/src/audit/agents/documentation.js +165 -165
- package/src/audit/agents/performance.js +145 -145
- package/src/audit/agents/security.js +215 -215
- package/src/audit/agents/testing.js +172 -172
- package/src/audit/orchestrator.js +557 -557
- package/src/audit/package.js +204 -204
- package/src/audit/registry.js +284 -284
- package/src/audit/replay.js +103 -103
- package/src/auth/gate.js +428 -371
- package/src/auth/http.js +681 -611
- package/src/auth/service.js +1106 -1106
- package/src/auth/session-store.js +813 -813
- package/src/cli.js +257 -252
- package/src/commands/ai/identity-lifecycle.js +1338 -1338
- package/src/commands/ai/provision-governance.js +1272 -1272
- package/src/commands/ai/shared.js +147 -147
- package/src/commands/ai.js +11 -11
- package/src/commands/apply.js +12 -12
- package/src/commands/audit.js +1171 -1166
- package/src/commands/auth.js +419 -419
- package/src/commands/chat.js +184 -191
- package/src/commands/config.js +184 -184
- package/src/commands/cost.js +311 -311
- package/src/commands/daemon/core.js +850 -850
- package/src/commands/daemon/extended.js +1048 -1048
- package/src/commands/daemon/shared.js +213 -213
- package/src/commands/daemon.js +11 -11
- package/src/commands/guide.js +174 -174
- package/src/commands/ingest.js +58 -58
- package/src/commands/init.js +55 -55
- package/src/commands/legacy-args.js +20 -10
- package/src/commands/mcp.js +461 -461
- package/src/commands/omargate.js +63 -29
- package/src/commands/persona.js +65 -20
- package/src/commands/plugin.js +260 -260
- package/src/commands/policy.js +132 -132
- package/src/commands/prompt.js +238 -238
- package/src/commands/review.js +704 -704
- package/src/commands/scan.js +865 -872
- package/src/commands/session.js +1238 -0
- package/src/commands/spec.js +771 -716
- package/src/commands/swarm.js +651 -651
- package/src/commands/telemetry.js +202 -202
- package/src/commands/watch.js +511 -511
- package/src/config/agent-dictionary.js +182 -182
- package/src/config/io.js +56 -56
- package/src/config/paths.js +18 -18
- package/src/config/schema.js +55 -55
- package/src/config/service.js +184 -184
- package/src/coord/events-log.js +141 -0
- package/src/coord/handshake.js +719 -0
- package/src/coord/index.js +35 -0
- package/src/coord/paths.js +84 -0
- package/src/coord/priority.js +62 -0
- package/src/coord/tarjan.js +157 -0
- package/src/cost/budget.js +235 -235
- package/src/cost/history.js +188 -188
- package/src/cost/tokenizer.js +160 -0
- package/src/cost/tracker.js +232 -171
- package/src/daemon/artifact-lineage.js +896 -534
- package/src/daemon/assignment-ledger.js +1083 -770
- package/src/daemon/ast-drift.js +496 -0
- package/src/daemon/ast-parser-layer.js +258 -258
- package/src/daemon/budget-governor.js +633 -633
- package/src/daemon/callgraph-overlay.js +646 -646
- package/src/daemon/error-worker.js +1209 -626
- package/src/daemon/fix-cycle.js +384 -377
- package/src/daemon/hybrid-mapper.js +929 -929
- package/src/daemon/ingest-refresh.js +79 -11
- package/src/daemon/jira-lifecycle.js +767 -632
- package/src/daemon/operator-control.js +657 -657
- package/src/daemon/pulse.js +327 -327
- package/src/daemon/reliability-lane.js +471 -471
- package/src/daemon/scope-engine.js +1068 -0
- package/src/daemon/watchdog.js +971 -971
- package/src/events/schema.js +190 -0
- package/src/guide/generator.js +316 -316
- package/src/ingest/engine.js +933 -918
- package/src/ingest/ownership.js +380 -0
- package/src/interactive/index.js +97 -97
- package/src/legacy-cli.js +3228 -2994
- package/src/mcp/registry.js +695 -695
- package/src/memory/blackboard.js +301 -301
- package/src/memory/retrieval.js +581 -581
- package/src/orchestrator/kai-chen.js +126 -0
- package/src/plugin/manifest.js +553 -553
- package/src/policy/packs.js +144 -144
- package/src/prompt/generator.js +136 -118
- package/src/review/ai-review.js +672 -679
- package/src/review/compliance-pack.js +389 -0
- package/src/review/investor-dd-config.js +54 -0
- package/src/review/investor-dd-file-loop.js +303 -0
- package/src/review/investor-dd-file-router.js +406 -0
- package/src/review/investor-dd-html-report.js +233 -0
- package/src/review/investor-dd-notification.js +120 -0
- package/src/review/investor-dd-orchestrator.js +405 -0
- package/src/review/investor-dd-persona-runner.js +275 -0
- package/src/review/live-validator.js +253 -0
- package/src/review/local-review.js +1351 -1305
- package/src/review/omargate-interactive.js +68 -68
- package/src/review/omargate-orchestrator.js +492 -300
- package/src/review/persona-prompts.js +484 -296
- package/src/review/reconciliation-rules.js +329 -0
- package/src/review/replay.js +235 -235
- package/src/review/report.js +664 -664
- package/src/review/reproducibility-chain.js +136 -0
- package/src/review/scan-modes.js +147 -42
- package/src/review/spec-binding.js +487 -487
- package/src/scaffold/generator.js +67 -67
- package/src/scaffold/templates.js +150 -150
- package/src/scan/generator.js +418 -418
- package/src/scan/gh-secrets.js +107 -107
- package/src/session/agent-registry.js +359 -0
- package/src/session/analytics.js +479 -0
- package/src/session/daemon.js +1396 -0
- package/src/session/file-locks.js +666 -0
- package/src/session/paths.js +37 -0
- package/src/session/recap.js +567 -0
- package/src/session/redact.js +82 -0
- package/src/session/runtime-bridge.js +762 -0
- package/src/session/scoring.js +406 -0
- package/src/session/setup-guides.js +304 -0
- package/src/session/store.js +704 -0
- package/src/session/stream.js +333 -0
- package/src/session/sync.js +753 -0
- package/src/session/tasks.js +1054 -0
- package/src/session/templates.js +188 -0
- package/src/spec/generator.js +619 -519
- package/src/spec/regenerate.js +237 -237
- package/src/spec/templates.js +91 -91
- package/src/swarm/dashboard.js +247 -247
- package/src/swarm/factory.js +363 -363
- package/src/swarm/pentest.js +934 -934
- package/src/swarm/registry.js +419 -419
- package/src/swarm/report.js +158 -158
- package/src/swarm/runtime.js +569 -576
- package/src/swarm/scenario-dsl.js +272 -272
- package/src/telemetry/ledger.js +302 -302
- package/src/telemetry/session-tracker.js +234 -234
- package/src/telemetry/sync.js +203 -203
- package/src/ui/command-hints.js +13 -13
- 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
|
+
}
|