sentinelayer-cli 0.46.4 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -3
- package/package.json +12 -7
- package/skills/omar-gate-setup/SKILL.md +103 -0
- package/skills/senti-agent-identity/SKILL.md +80 -0
- package/skills/senti-judge/SKILL.md +139 -0
- package/skills/senti-room-notifications/SKILL.md +73 -0
- package/skills/senti-room-notifications/listen-wrapper.cjs +150 -0
- package/skills/senti-room-notifications/package.json +1 -0
- package/skills/senti-room-notifications/test-classifier.js +109 -0
- package/skills/senti-room-notifications/test-receipt.js +54 -0
- package/skills/sentinelayer-cli/SKILL.md +222 -0
- package/src/agents/devtestbot/runner.js +22 -11
- package/src/agents/devtestbot/tool.js +24 -11
- package/src/agents/jules/fix-cycle.js +7 -1
- package/src/agents/jules/tools/runtime-audit.js +17 -12
- package/src/agents/shared-tools/dispatch-core.js +10 -5
- package/src/ai/aidenid.js +4 -2
- package/src/ai/domain-target-store.js +11 -5
- package/src/ai/identity-store.js +11 -5
- package/src/ai/site-store.js +8 -4
- package/src/audit/orchestrator.js +21 -14
- package/src/audit/package.js +6 -4
- package/src/audit/persona-loop.js +7 -0
- package/src/audit/replay.js +4 -2
- package/src/auth/gate.js +46 -8
- package/src/auth/service.js +34 -15
- package/src/auth/session-store.js +105 -47
- package/src/board/evidence-resolvers.js +3 -4
- package/src/cli.js +23 -3
- package/src/commands/ai/identity-lifecycle.js +30 -26
- package/src/commands/ai/provision-governance.js +32 -24
- package/src/commands/ai/shared.js +6 -4
- package/src/commands/audit.js +14 -12
- package/src/commands/auth.js +24 -4
- package/src/commands/board.js +2 -2
- package/src/commands/chat.js +8 -6
- package/src/commands/judge.js +125 -0
- package/src/commands/legacy-args.js +1 -0
- package/src/commands/mcp.js +45 -52
- package/src/commands/omargate.js +2 -1
- package/src/commands/plugin.js +11 -3
- package/src/commands/review.js +9 -4
- package/src/commands/scan.js +7 -7
- package/src/commands/session.js +607 -57
- package/src/commands/skills.js +45 -0
- package/src/commands/watch.js +9 -7
- package/src/config/output-filesystem.js +156 -0
- package/src/config/schema.js +4 -2
- package/src/config/service.js +40 -4
- package/src/cost/history.js +15 -9
- package/src/daemon/artifact-lineage.js +15 -12
- package/src/daemon/assignment-ledger.js +31 -25
- package/src/daemon/ast-drift.js +7 -3
- package/src/daemon/budget-governor.js +26 -20
- package/src/daemon/error-worker.js +28 -22
- package/src/daemon/fix-cycle.js +5 -4
- package/src/daemon/hybrid-mapper.js +19 -16
- package/src/daemon/ingest-refresh.js +4 -0
- package/src/daemon/jira-lifecycle.js +21 -17
- package/src/daemon/operator-control.js +18 -15
- package/src/daemon/reliability-lane.js +18 -15
- package/src/daemon/scope-engine.js +12 -8
- package/src/daemon/watchdog.js +15 -12
- package/src/engram/build-state.js +24 -3
- package/src/engram/capability-index.js +367 -0
- package/src/engram/compile-graph.js +1063 -0
- package/src/engram/contracts/action-receipt.js +82 -0
- package/src/engram/contracts/capability-index.js +259 -0
- package/src/engram/contracts/capability.js +377 -0
- package/src/engram/contracts/content-hash.js +8 -0
- package/src/engram/contracts/evidence.js +258 -0
- package/src/engram/contracts/graph.js +594 -0
- package/src/engram/contracts/identity.js +8 -0
- package/src/engram/contracts/index.js +149 -0
- package/src/engram/contracts/media-asset.js +85 -0
- package/src/engram/contracts/money.js +249 -0
- package/src/engram/contracts/source-snapshot.js +57 -0
- package/src/engram/contracts/task-context-pack.js +114 -0
- package/src/engram/contracts/temporal-segment.js +72 -0
- package/src/engram/contracts/versions.js +96 -0
- package/src/engram/extract-html.js +692 -0
- package/src/engram/graph-store.js +877 -0
- package/src/engram/index.js +98 -14
- package/src/engram/ingest-site.js +132 -0
- package/src/engram/live-state.js +1180 -0
- package/src/engram/namespace.js +34 -1
- package/src/engram/retrieve-site.js +753 -0
- package/src/engram/safe-reader.js +597 -0
- package/src/engram/source-intake.js +251 -0
- package/src/engram/store.js +227 -9
- package/src/ingest/engine.js +21 -28
- package/src/judge/verdict.js +305 -0
- package/src/legacy-cli.js +96 -19
- package/src/mcp/bridge-inputs.js +28 -4
- package/src/mcp/cli-command-tools.js +70 -0
- package/src/mcp/registry.js +17 -13
- package/src/mcp/session-stdio-server.js +297 -19
- package/src/memory/blackboard.js +5 -4
- package/src/net/gh-api-path.js +35 -9
- package/src/net/test-egress-guard.js +16 -4
- package/src/plugin/manifest.js +12 -8
- package/src/review/ai-review.js +110 -10
- package/src/review/investor-dd-devtestbot.js +17 -6
- package/src/review/investor-dd-orchestrator.js +21 -13
- package/src/review/local-review.js +26 -16
- package/src/review/omargate-cache.js +5 -5
- package/src/review/omargate-orchestrator.js +89 -17
- package/src/review/replay.js +9 -4
- package/src/review/report.js +22 -13
- package/src/review/typesafe-severity.js +201 -0
- package/src/session/admission-auth.js +34 -7
- package/src/session/admission.js +322 -71
- package/src/session/agent-access.js +137 -0
- package/src/session/agent-registry.js +2 -1
- package/src/session/attachment-summary.js +77 -0
- package/src/session/edit-summary.js +19 -0
- package/src/session/invitations.js +65 -1
- package/src/session/mentions.js +17 -9
- package/src/session/message-edits.js +5 -4
- package/src/session/preview.js +10 -2
- package/src/session/recap.js +3 -2
- package/src/session/room-files.js +519 -0
- package/src/session/route-classes.js +22 -8
- package/src/session/setup-guides.js +4 -0
- package/src/session/standup.js +192 -0
- package/src/session/sync.js +229 -10
- package/src/session/tickets.js +41 -0
- package/src/session/transcript.js +2 -1
- package/src/session/wake/resolve-target.js +7 -3
- package/src/skills/listener-runtime.cjs +92 -0
- package/src/skills/service.js +207 -0
- package/src/swarm/factory.js +5 -5
- package/src/swarm/pentest.js +9 -9
- package/src/swarm/report.js +4 -4
- package/src/swarm/runtime.js +15 -12
- package/src/swarm/scenario-dsl.js +7 -5
- package/src/telemetry/ledger.js +16 -7
package/src/commands/session.js
CHANGED
|
@@ -6,6 +6,8 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
6
6
|
import { spawn as defaultSpawn } from "node:child_process";
|
|
7
7
|
|
|
8
8
|
import pc from "picocolors";
|
|
9
|
+
import { appendAttachmentSummary, withAttachmentSummary } from "../session/attachment-summary.js";
|
|
10
|
+
import { formatEditSummary, withEditSummary } from "../session/edit-summary.js";
|
|
9
11
|
|
|
10
12
|
import { credentialFor, isAuthenticated } from "../auth/credential-destinations.js";
|
|
11
13
|
import { SentinelayerApiError, requestJsonMutation } from "../auth/http.js";
|
|
@@ -18,7 +20,7 @@ import {
|
|
|
18
20
|
import { recordProvisionedIdentity } from "../ai/identity-store.js";
|
|
19
21
|
import { fetchAidenIdCredentials } from "../auth/service.js";
|
|
20
22
|
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
21
|
-
import {
|
|
23
|
+
import { resolveOutputContext } from "../config/service.js";
|
|
22
24
|
import {
|
|
23
25
|
listAssignments,
|
|
24
26
|
releaseLease,
|
|
@@ -33,6 +35,7 @@ import {
|
|
|
33
35
|
unregisterAgent,
|
|
34
36
|
} from "../session/agent-registry.js";
|
|
35
37
|
import { inferSessionAgentIdentity } from "../session/agent-identity.js";
|
|
38
|
+
import { legacyJoinAdmissionRefusal, verifyLegacyAgentJoin } from "../session/agent-access.js";
|
|
36
39
|
import { startSenti, stopSenti } from "../session/daemon.js";
|
|
37
40
|
import {
|
|
38
41
|
getDaemonStatus,
|
|
@@ -61,6 +64,7 @@ import {
|
|
|
61
64
|
setupSessionGuides,
|
|
62
65
|
} from "../session/setup-guides.js";
|
|
63
66
|
import { listSessionTasks } from "../session/tasks.js";
|
|
67
|
+
import { formatSessionStandup, sessionStandup } from "../session/standup.js";
|
|
64
68
|
import {
|
|
65
69
|
createSession,
|
|
66
70
|
DEFAULT_TTL_SECONDS,
|
|
@@ -88,6 +92,8 @@ import {
|
|
|
88
92
|
} from "../session/control-events.js";
|
|
89
93
|
import {
|
|
90
94
|
acceptSessionInvitation,
|
|
95
|
+
acceptPendingSessionInvitation,
|
|
96
|
+
listPendingSessionInvitations,
|
|
91
97
|
normalizeSessionOnboarding,
|
|
92
98
|
writeSessionOnboardingBrief,
|
|
93
99
|
} from "../session/invitations.js";
|
|
@@ -118,6 +124,7 @@ import {
|
|
|
118
124
|
} from "../session/reactions.js";
|
|
119
125
|
import {
|
|
120
126
|
claimTicket,
|
|
127
|
+
createTicket,
|
|
121
128
|
listTickets,
|
|
122
129
|
releaseTicketLease,
|
|
123
130
|
renewTicketLease,
|
|
@@ -139,6 +146,7 @@ import {
|
|
|
139
146
|
createSessionMessageAction,
|
|
140
147
|
editSessionMessage,
|
|
141
148
|
fetchJsonWithFullTimeout,
|
|
149
|
+
fetchSessionPresence,
|
|
142
150
|
fetchSessionUsageLedger,
|
|
143
151
|
fetchSessionPinnedMessages,
|
|
144
152
|
listSessionMessageActions,
|
|
@@ -155,7 +163,15 @@ import {
|
|
|
155
163
|
updateSessionReadCursor,
|
|
156
164
|
} from "../session/sync.js";
|
|
157
165
|
import { hydrateSessionFromRemote } from "../session/remote-hydrate.js";
|
|
158
|
-
import {
|
|
166
|
+
import {
|
|
167
|
+
DEFAULT_ROOM_FILES_LIMIT,
|
|
168
|
+
DEFAULT_ROOM_FILES_MAX_PAGES,
|
|
169
|
+
MAX_ROOM_FILES_LIMIT,
|
|
170
|
+
RoomFileError,
|
|
171
|
+
downloadRoomFile,
|
|
172
|
+
listRoomFiles,
|
|
173
|
+
} from "../session/room-files.js";
|
|
174
|
+
import { messageRevision, replyRevisionEvent } from "../session/message-edits.js";
|
|
159
175
|
import { runSessionRecall } from "../session/recall/index.js";
|
|
160
176
|
import { mergeLiveSources } from "../session/live-source.js";
|
|
161
177
|
import { listenSessionEvents } from "../session/listener.js";
|
|
@@ -853,7 +869,7 @@ function formatHumanAskLine(event = {}) {
|
|
|
853
869
|
const message = humanAskMessage(event);
|
|
854
870
|
const shortMessage = message.length > 220 ? `${message.slice(0, 217)}...` : message;
|
|
855
871
|
const suffix = shortMessage ? `: ${shortMessage}` : "";
|
|
856
|
-
return `${sequenceLabel} ${humanAskAgentId(event)}${ts ? ` ${ts}` : ""}${suffix}
|
|
872
|
+
return appendAttachmentSummary(`${sequenceLabel} ${humanAskAgentId(event)}${ts ? ` ${ts}` : ""}${suffix}`, event);
|
|
857
873
|
}
|
|
858
874
|
|
|
859
875
|
function formatRemoteHydrateAppendWarning(result = {}) {
|
|
@@ -1459,6 +1475,7 @@ async function fetchRemoteSessionDetail(endpoint, credential) {
|
|
|
1459
1475
|
endpoint, { method: "GET", credential, redirect: "error" }, 2_000,
|
|
1460
1476
|
));
|
|
1461
1477
|
} catch (err) {
|
|
1478
|
+
if (err?.code === "INVITATION_PENDING") throw err;
|
|
1462
1479
|
return {
|
|
1463
1480
|
ok: false,
|
|
1464
1481
|
response: null,
|
|
@@ -1985,6 +2002,7 @@ async function publishListenerPresenceEvent({
|
|
|
1985
2002
|
clientKind = "cli",
|
|
1986
2003
|
listenerId,
|
|
1987
2004
|
lifecycle = {},
|
|
2005
|
+
minimalPresence = false,
|
|
1988
2006
|
} = {}) {
|
|
1989
2007
|
const normalizedType = normalizeString(lifecycle.type) || "heartbeat";
|
|
1990
2008
|
const identity = inferSessionAgentIdentity({
|
|
@@ -1997,6 +2015,7 @@ async function publishListenerPresenceEvent({
|
|
|
1997
2015
|
return renewSessionPresence(sessionId, {
|
|
1998
2016
|
targetPath,
|
|
1999
2017
|
agentId,
|
|
2018
|
+
minimalPresence,
|
|
2000
2019
|
state:
|
|
2001
2020
|
normalizedType === "stopped"
|
|
2002
2021
|
? "stopped"
|
|
@@ -2439,14 +2458,29 @@ function resolveSessionIdOption(options = {}) {
|
|
|
2439
2458
|
return sessionId;
|
|
2440
2459
|
}
|
|
2441
2460
|
|
|
2461
|
+
function formatFileSize(bytes) {
|
|
2462
|
+
const value = Number(bytes) || 0;
|
|
2463
|
+
if (value < 1024) return `${value} B`;
|
|
2464
|
+
const units = ["KB", "MB", "GB"];
|
|
2465
|
+
let size = value / 1024;
|
|
2466
|
+
let unit = 0;
|
|
2467
|
+
while (size >= 1024 && unit < units.length - 1) {
|
|
2468
|
+
size /= 1024;
|
|
2469
|
+
unit += 1;
|
|
2470
|
+
}
|
|
2471
|
+
return `${size.toFixed(size < 10 ? 1 : 0)} ${units[unit]}`;
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2442
2474
|
function formatEventLine(event = {}) {
|
|
2475
|
+
return appendAttachmentSummary(formatEventText(event), event);
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
function formatEventText(event = {}) {
|
|
2443
2479
|
const ts = normalizeString(event.ts || event.timestamp);
|
|
2444
2480
|
const type = normalizeString(event.event || event.type) || "event";
|
|
2445
2481
|
const agentId = normalizeString(event.agent?.id || event.agentId || "unknown");
|
|
2446
2482
|
if (type === "session_message_edited") {
|
|
2447
|
-
|
|
2448
|
-
const target = event.payload?.targetActionId ? `reply ${event.payload.targetActionId}` : `#${event.payload?.targetSequenceId || "?"}`;
|
|
2449
|
-
return `${ts} ${agentId} ${type} ${target} revision ${event.payload?.messageRevision || "?"}: ${normalizeString(routed.payload?.message || routed.payload?.text || "")}`;
|
|
2483
|
+
return `${ts} ${agentId} ${formatEditSummary(event)}`;
|
|
2450
2484
|
}
|
|
2451
2485
|
const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
|
|
2452
2486
|
if (type === "session_action" || type === "session_reply" || type === "session_reaction") {
|
|
@@ -2903,11 +2937,69 @@ async function sessionListenAgent(options) {
|
|
|
2903
2937
|
return { sessionId: resolveSessionIdOption(options), agentId: normalizeAgentId(options.agent, "cli-user") };
|
|
2904
2938
|
}
|
|
2905
2939
|
|
|
2940
|
+
|
|
2941
|
+
/**
|
|
2942
|
+
* Build the admission-join options object from parsed Commander options.
|
|
2943
|
+
* Extracted so tests can assert the full derivation from CLI flag to
|
|
2944
|
+
* runAdmissionJoin input without network I/O.
|
|
2945
|
+
*/
|
|
2946
|
+
export function buildAdmissionJoinOptions(options, overrides = {}) {
|
|
2947
|
+
const waitSeconds = Number.parseInt(String(options.waitSeconds ?? "900"), 10);
|
|
2948
|
+
return {
|
|
2949
|
+
agentId: overrides.agentId ?? "",
|
|
2950
|
+
displayName: overrides.displayName ?? "",
|
|
2951
|
+
model: options.model,
|
|
2952
|
+
provider: options.provider,
|
|
2953
|
+
goal: options.goal,
|
|
2954
|
+
deliverables: options.deliverable || [],
|
|
2955
|
+
stopConditions: options.stopWhen || [],
|
|
2956
|
+
actions: parseAdmissionScope(options.scope),
|
|
2957
|
+
ttlSeconds: parseAdmissionTtlSeconds(options.ttl),
|
|
2958
|
+
wait: options.wait !== false,
|
|
2959
|
+
recoverActive: Boolean(options.recoverAdmission),
|
|
2960
|
+
noAutoGrant: options.autoGrant === false,
|
|
2961
|
+
waitTimeoutMs: (Number.isFinite(waitSeconds) && waitSeconds >= 0 ? waitSeconds : 900) * 1000,
|
|
2962
|
+
targetPath: overrides.targetPath ?? process.cwd(),
|
|
2963
|
+
onPending: overrides.onPending ?? (() => {}),
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2906
2967
|
export function registerSessionCommand(program) {
|
|
2907
2968
|
const session = program
|
|
2908
2969
|
.command("session")
|
|
2909
2970
|
.description("Multi-agent ephemeral coordination sessions");
|
|
2910
2971
|
|
|
2972
|
+
const invitations = session.command("invitations").description("List and accept pending room invitations");
|
|
2973
|
+
invitations
|
|
2974
|
+
.command("list")
|
|
2975
|
+
.description("List your pending room invitations")
|
|
2976
|
+
.option("--path <path>", "Workspace path for authentication", ".")
|
|
2977
|
+
.option("--json", "Emit the raw API response")
|
|
2978
|
+
.action(async (options, command) => {
|
|
2979
|
+
const result = await listPendingSessionInvitations({ targetPath: path.resolve(options.path) });
|
|
2980
|
+
if (shouldEmitJson(options, command)) {
|
|
2981
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2982
|
+
return;
|
|
2983
|
+
}
|
|
2984
|
+
const entries = Array.isArray(result) ? result : result?.invitations;
|
|
2985
|
+
if (!Array.isArray(entries) || entries.some((entry) => !normalizeString(entry?.invitationId))) {
|
|
2986
|
+
console.log("Pending invitations retrieved. Use `sl session invitations list --json` to inspect the response.");
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2989
|
+
if (entries.length === 0) console.log("No pending invitations.");
|
|
2990
|
+
for (const entry of entries) console.log(`Invitation: ${entry.invitationId}`);
|
|
2991
|
+
});
|
|
2992
|
+
invitations
|
|
2993
|
+
.command("accept <invitationId>")
|
|
2994
|
+
.description("Accept a pending invitation using its invitationId from invitations list")
|
|
2995
|
+
.option("--path <path>", "Workspace path for authentication", ".")
|
|
2996
|
+
.option("--json", "Emit the raw API response")
|
|
2997
|
+
.action(async (invitationId, options, command) => {
|
|
2998
|
+
const result = await acceptPendingSessionInvitation(invitationId, { targetPath: path.resolve(options.path) });
|
|
2999
|
+
if (shouldEmitJson(options, command)) console.log(JSON.stringify(result, null, 2));
|
|
3000
|
+
else console.log("Invitation accepted.");
|
|
3001
|
+
});
|
|
3002
|
+
|
|
2911
3003
|
session
|
|
2912
3004
|
.command("start")
|
|
2913
3005
|
.description(
|
|
@@ -3437,6 +3529,8 @@ export function registerSessionCommand(program) {
|
|
|
3437
3529
|
.option("--no-wait", "Request admission and return immediately with the approval URL")
|
|
3438
3530
|
.option("--wait-seconds <n>", "How long to wait for approval before returning pending", "900")
|
|
3439
3531
|
.option("--cancel-admission", "Cancel this workspace's pending admission request for the session")
|
|
3532
|
+
.option("--recover-admission", "Recover a live admission with the retained agent key; requires the unchanged --goal/scope/ttl")
|
|
3533
|
+
.option("--no-auto-grant", "Skip auto-grant on 403 IDENTITY_FORGERY; fail immediately instead")
|
|
3440
3534
|
.option("--json", "Emit machine-readable output")
|
|
3441
3535
|
.action(async (sessionId, options, command) => {
|
|
3442
3536
|
const normalizedSessionId = normalizeString(sessionId);
|
|
@@ -3455,6 +3549,27 @@ export function registerSessionCommand(program) {
|
|
|
3455
3549
|
// the local registration and every later implicit command name the same agent.
|
|
3456
3550
|
if (!explicitAgent) assertCanonicalAgentId(legacyName, "--name");
|
|
3457
3551
|
const requestedAgent = canonicalAgentId(explicitAgent || legacyName);
|
|
3552
|
+
if (options.recoverAdmission && !normalizeString(options.goal)) {
|
|
3553
|
+
throw new Error("--recover-admission requires --goal and the admission's unchanged scope/ttl.");
|
|
3554
|
+
}
|
|
3555
|
+
const rejectLegacyJoin = (verification, agentId) => {
|
|
3556
|
+
process.exitCode = 4;
|
|
3557
|
+
const payload = {
|
|
3558
|
+
command: "session join", joined: false, sessionId: normalizedSessionId,
|
|
3559
|
+
agentId, remoteAgentVerification: verification,
|
|
3560
|
+
};
|
|
3561
|
+
if (emitJson) console.log(JSON.stringify(payload, null, 2));
|
|
3562
|
+
else {
|
|
3563
|
+
const hint = verification.code.includes("UNAVAILABLE")
|
|
3564
|
+
? "Retry when the API/presence service is available; no new approval was requested."
|
|
3565
|
+
: verification.code.includes("CONTRACT_MISMATCH")
|
|
3566
|
+
? "Verify the CLI/server contract; no new authorization was created."
|
|
3567
|
+
: verification.code === "LEGACY_GRANT_ROLE_MISMATCH"
|
|
3568
|
+
? "Use the already-granted role, or request an explicit owner-approved change."
|
|
3569
|
+
: "Request scoped admission with --goal, or have the owner explicitly authorize its legacy grant.";
|
|
3570
|
+
console.log(pc.red(`Agent was not joined: ${verification.code}. ${hint}`));
|
|
3571
|
+
}
|
|
3572
|
+
};
|
|
3458
3573
|
if (options.cancelAdmission) {
|
|
3459
3574
|
const cancelled = await cancelAdmission(normalizedSessionId, {
|
|
3460
3575
|
agentId: requestedAgent,
|
|
@@ -3473,6 +3588,13 @@ export function registerSessionCommand(program) {
|
|
|
3473
3588
|
else console.log(`Admission request ${payload.admission.admissionId || ""} cancelled.`);
|
|
3474
3589
|
return;
|
|
3475
3590
|
}
|
|
3591
|
+
if (requestedAgent && !normalizeString(options.goal)) {
|
|
3592
|
+
const refusal = await legacyJoinAdmissionRefusal(normalizedSessionId, requestedAgent);
|
|
3593
|
+
if (refusal) {
|
|
3594
|
+
rejectLegacyJoin(refusal, requestedAgent);
|
|
3595
|
+
return;
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3476
3598
|
// Membership first. Only a member may ask for an agent to be admitted, so an
|
|
3477
3599
|
// invitation is accepted BEFORE the admission request, never after.
|
|
3478
3600
|
let invitationAccept = null;
|
|
@@ -3491,24 +3613,15 @@ export function registerSessionCommand(program) {
|
|
|
3491
3613
|
: null;
|
|
3492
3614
|
}
|
|
3493
3615
|
let admission = null;
|
|
3616
|
+
let admissionJoinOptions = null;
|
|
3494
3617
|
if (normalizeString(options.goal)) {
|
|
3495
3618
|
const admissionAgent = requestedAgent;
|
|
3496
3619
|
if (!admissionAgent) {
|
|
3497
3620
|
throw new Error("--goal requires --agent <id>: admission is requested for one named agent.");
|
|
3498
3621
|
}
|
|
3499
|
-
|
|
3500
|
-
admission = await runAdmissionJoin(normalizedSessionId, {
|
|
3622
|
+
admissionJoinOptions = buildAdmissionJoinOptions(options, {
|
|
3501
3623
|
agentId: admissionAgent,
|
|
3502
3624
|
displayName: legacyName && explicitAgent ? legacyName : "",
|
|
3503
|
-
model: options.model,
|
|
3504
|
-
provider: options.provider,
|
|
3505
|
-
goal: options.goal,
|
|
3506
|
-
deliverables: options.deliverable || [],
|
|
3507
|
-
stopConditions: options.stopWhen || [],
|
|
3508
|
-
actions: parseAdmissionScope(options.scope),
|
|
3509
|
-
ttlSeconds: parseAdmissionTtlSeconds(options.ttl),
|
|
3510
|
-
wait: options.wait !== false,
|
|
3511
|
-
waitTimeoutMs: (Number.isFinite(waitSeconds) && waitSeconds >= 0 ? waitSeconds : 900) * 1000,
|
|
3512
3625
|
targetPath,
|
|
3513
3626
|
onPending: ({ approveUrl, phase }) => {
|
|
3514
3627
|
// stderr, so --json stdout stays a single parseable document.
|
|
@@ -3524,6 +3637,7 @@ export function registerSessionCommand(program) {
|
|
|
3524
3637
|
else console.log(line);
|
|
3525
3638
|
},
|
|
3526
3639
|
});
|
|
3640
|
+
admission = await runAdmissionJoin(normalizedSessionId, admissionJoinOptions);
|
|
3527
3641
|
if (admission.status !== "active") {
|
|
3528
3642
|
// Only an ACTIVE admission joins. Pending, denied, cancelled or expired never
|
|
3529
3643
|
// registers the agent or materializes the room for it.
|
|
@@ -3550,6 +3664,34 @@ export function registerSessionCommand(program) {
|
|
|
3550
3664
|
}
|
|
3551
3665
|
}
|
|
3552
3666
|
|
|
3667
|
+
const admittedAgentId = admission ? requestedAgent : "";
|
|
3668
|
+
const acceptedOnboarding = normalizeSessionOnboarding(
|
|
3669
|
+
invitationAcceptResult?.onboarding,
|
|
3670
|
+
invitationAcceptResult?.claimedSeat,
|
|
3671
|
+
);
|
|
3672
|
+
const acceptedAgentId = normalizeString(acceptedOnboarding?.agentId);
|
|
3673
|
+
if (!admission && !explicitAgent && acceptedAgentId) {
|
|
3674
|
+
assertCanonicalAgentId(acceptedAgentId, "the invitation's agent id");
|
|
3675
|
+
}
|
|
3676
|
+
const agentSeed = admission ? admittedAgentId : explicitAgent || acceptedAgentId || legacyName;
|
|
3677
|
+
const resolvedAgentId = await defaultAgentId(agentSeed, targetPath);
|
|
3678
|
+
if (admission && resolvedAgentId !== admittedAgentId) {
|
|
3679
|
+
throw new Error(`join would register "${resolvedAgentId}", not the admitted agent "${admittedAgentId}".`);
|
|
3680
|
+
}
|
|
3681
|
+
const roleWasExplicit = optionWasSetByCli(command, "role");
|
|
3682
|
+
const role =
|
|
3683
|
+
normalizeString(roleWasExplicit ? options.role : acceptedOnboarding?.role || options.role) ||
|
|
3684
|
+
"coder";
|
|
3685
|
+
const model = normalizeString(options.model) || "cli";
|
|
3686
|
+
const namedLegacyAgent = !admission && Boolean(agentSeed || normalizeString(process.env.SENTINELAYER_AGENT_ID));
|
|
3687
|
+
if (namedLegacyAgent) {
|
|
3688
|
+
const refusal = await legacyJoinAdmissionRefusal(normalizedSessionId, resolvedAgentId);
|
|
3689
|
+
if (refusal) {
|
|
3690
|
+
rejectLegacyJoin(refusal, resolvedAgentId);
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3553
3695
|
// PR #483 contract: verify the session exists and the caller has access
|
|
3554
3696
|
// BEFORE materializing local cache state. Without this we'd silently
|
|
3555
3697
|
// create a phantom local NDJSON for a session that's archived or owned
|
|
@@ -3564,21 +3706,36 @@ export function registerSessionCommand(program) {
|
|
|
3564
3706
|
// grant's receipt, and never with the human token. A cached "active" is not
|
|
3565
3707
|
// reported until this succeeds (a revoked grant is not joined), and nothing
|
|
3566
3708
|
// about the room is fetched on the human's authority.
|
|
3567
|
-
const admittedAgentId = admission ? requestedAgent : "";
|
|
3568
3709
|
let admissionReceipt = null;
|
|
3569
3710
|
if (admission) {
|
|
3570
|
-
|
|
3571
|
-
|
|
3711
|
+
const verifyAdmissionReceipt = async () => {
|
|
3712
|
+
const receipt = await withAgentAdmission(normalizedSessionId, admittedAgentId, () =>
|
|
3572
3713
|
fetchOwnAdmissionReceipt(normalizedSessionId, { targetPath }),
|
|
3573
3714
|
);
|
|
3574
3715
|
// The grant must be for EXACTLY the identity this join registers, byte for
|
|
3575
3716
|
// byte: a receipt for any other spelling is a different agent's authority.
|
|
3576
|
-
const receiptAgent = String(
|
|
3717
|
+
const receiptAgent = String(receipt?.agentId ?? "");
|
|
3577
3718
|
if (receiptAgent !== admittedAgentId) {
|
|
3578
3719
|
throw new Error(
|
|
3579
3720
|
`the admission is for agent "${receiptAgent || "(none)"}", not "${admittedAgentId}"`,
|
|
3580
3721
|
);
|
|
3581
3722
|
}
|
|
3723
|
+
return receipt;
|
|
3724
|
+
};
|
|
3725
|
+
try {
|
|
3726
|
+
try {
|
|
3727
|
+
admissionReceipt = await verifyAdmissionReceipt();
|
|
3728
|
+
} catch (error) {
|
|
3729
|
+
// One key-possession recovery for a stale bearer, never a different
|
|
3730
|
+
// identity, denied scope, provider/network failure or generic 403.
|
|
3731
|
+
if (options.recoverAdmission || !(error instanceof SentinelayerApiError) ||
|
|
3732
|
+
![401, 403].includes(error.status) || error.code !== "INVALID_TOKEN") throw error;
|
|
3733
|
+
admission = await runAdmissionJoin(normalizedSessionId, {
|
|
3734
|
+
...admissionJoinOptions, recoverActive: true,
|
|
3735
|
+
});
|
|
3736
|
+
if (admission.status !== "active") throw new Error("The admission is not active; recovery did not join the agent.");
|
|
3737
|
+
admissionReceipt = await verifyAdmissionReceipt();
|
|
3738
|
+
}
|
|
3582
3739
|
} catch (error) {
|
|
3583
3740
|
process.exitCode = 4;
|
|
3584
3741
|
const payload = {
|
|
@@ -3594,7 +3751,7 @@ export function registerSessionCommand(program) {
|
|
|
3594
3751
|
},
|
|
3595
3752
|
};
|
|
3596
3753
|
if (emitJson) console.log(JSON.stringify(payload, null, 2));
|
|
3597
|
-
else console.log(pc.red(`The API no longer accepts this agent's admission; it was not joined.`));
|
|
3754
|
+
else console.log(pc.red(`The API no longer accepts this agent's admission; it was not joined. For a live grant with a retained key, retry the unchanged request with --recover-admission.`));
|
|
3598
3755
|
return;
|
|
3599
3756
|
}
|
|
3600
3757
|
admission = { ...admission, verified: true, receipt: admissionReceipt };
|
|
@@ -3622,6 +3779,16 @@ export function registerSessionCommand(program) {
|
|
|
3622
3779
|
}
|
|
3623
3780
|
|
|
3624
3781
|
const remoteSession = verification.session || {};
|
|
3782
|
+
let legacyAgentVerification = null;
|
|
3783
|
+
if (namedLegacyAgent) {
|
|
3784
|
+
legacyAgentVerification = await verifyLegacyAgentJoin(normalizedSessionId, {
|
|
3785
|
+
agentId: resolvedAgentId, role, model, targetPath, remoteSession,
|
|
3786
|
+
});
|
|
3787
|
+
if (!legacyAgentVerification.ok) {
|
|
3788
|
+
rejectLegacyJoin(legacyAgentVerification, resolvedAgentId);
|
|
3789
|
+
return;
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3625
3792
|
const localSession = await ensureLocalSessionForRemoteCommand(normalizedSessionId, {
|
|
3626
3793
|
targetPath,
|
|
3627
3794
|
title: normalizeString(remoteSession.title),
|
|
@@ -3637,29 +3804,6 @@ export function registerSessionCommand(program) {
|
|
|
3637
3804
|
)
|
|
3638
3805
|
: { skipped: true, reason: "the grant does not include session.read" };
|
|
3639
3806
|
|
|
3640
|
-
const acceptedOnboarding = normalizeSessionOnboarding(
|
|
3641
|
-
invitationAcceptResult?.onboarding,
|
|
3642
|
-
invitationAcceptResult?.claimedSeat,
|
|
3643
|
-
);
|
|
3644
|
-
const acceptedAgentId = normalizeString(acceptedOnboarding?.agentId);
|
|
3645
|
-
// An admitted join registers the admitted identity and nothing else. Otherwise an
|
|
3646
|
-
// invitation's onboarding agent id is the identity only when none was given, and
|
|
3647
|
-
// it is held to the same canonical rule before anything is registered locally.
|
|
3648
|
-
if (!admission && !explicitAgent && acceptedAgentId) {
|
|
3649
|
-
assertCanonicalAgentId(acceptedAgentId, "the invitation's agent id");
|
|
3650
|
-
}
|
|
3651
|
-
const agentSeed = admission ? admittedAgentId : explicitAgent || acceptedAgentId || legacyName;
|
|
3652
|
-
const resolvedAgentId = await defaultAgentId(agentSeed, targetPath);
|
|
3653
|
-
if (admission && resolvedAgentId !== admittedAgentId) {
|
|
3654
|
-
throw new Error(`join would register "${resolvedAgentId}", not the admitted agent "${admittedAgentId}".`);
|
|
3655
|
-
}
|
|
3656
|
-
const roleWasExplicit = optionWasSetByCli(command, "role");
|
|
3657
|
-
const role =
|
|
3658
|
-
normalizeString(roleWasExplicit ? options.role : acceptedOnboarding?.role || options.role) ||
|
|
3659
|
-
"coder";
|
|
3660
|
-
const model = normalizeString(options.model) || "cli";
|
|
3661
|
-
const hasConcreteAgentIdentity = Boolean(explicitAgent || acceptedAgentId);
|
|
3662
|
-
|
|
3663
3807
|
// Registration updates the local participant projection and returns
|
|
3664
3808
|
// onboarding directly to this process. It never writes join, identity,
|
|
3665
3809
|
// or coaching rows into the transcript.
|
|
@@ -3671,7 +3815,7 @@ export function registerSessionCommand(program) {
|
|
|
3671
3815
|
clientKind: "cli",
|
|
3672
3816
|
role,
|
|
3673
3817
|
trackProcessExit: false,
|
|
3674
|
-
|
|
3818
|
+
preserveIdentity: Boolean(admission || namedLegacyAgent),
|
|
3675
3819
|
});
|
|
3676
3820
|
const agentJoinRelayed = false;
|
|
3677
3821
|
const localBriefingPayload =
|
|
@@ -3714,6 +3858,7 @@ export function registerSessionCommand(program) {
|
|
|
3714
3858
|
materializedLocalSession: localSession.materialized,
|
|
3715
3859
|
refreshedLocalSession: Boolean(localSession.refreshed),
|
|
3716
3860
|
verificationSource: verification.source,
|
|
3861
|
+
remoteAgentVerification: legacyAgentVerification,
|
|
3717
3862
|
joinHydration,
|
|
3718
3863
|
eventCount: Number.isFinite(eventCount) ? eventCount : 0,
|
|
3719
3864
|
agentCount: Number.isFinite(agentCount) ? agentCount : 0,
|
|
@@ -3820,6 +3965,7 @@ export function registerSessionCommand(program) {
|
|
|
3820
3965
|
.option("--stdin", "Read the message body from stdin")
|
|
3821
3966
|
.option("--force-cli-user", "Allow fallback sends as cli-user when no agent identity can be resolved")
|
|
3822
3967
|
.option("--local-only", "Append only to the local session cache without remote send confirmation")
|
|
3968
|
+
.option("--no-auto-grant", "Skip auto-grant on 403 IDENTITY_FORGERY; fail immediately instead")
|
|
3823
3969
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
3824
3970
|
.option("--json", "Emit machine-readable output")
|
|
3825
3971
|
.action(asAdmittedAgent(sessionSayAgent, async (sessionId, messageParts, options, command) => {
|
|
@@ -3898,6 +4044,7 @@ export function registerSessionCommand(program) {
|
|
|
3898
4044
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
3899
4045
|
remoteSync = await syncSessionEventToApi(normalizedSessionId, event, {
|
|
3900
4046
|
targetPath,
|
|
4047
|
+
noAutoGrant: options.autoGrant === false,
|
|
3901
4048
|
});
|
|
3902
4049
|
if (remoteSync?.synced) break;
|
|
3903
4050
|
}
|
|
@@ -3960,6 +4107,7 @@ export function registerSessionCommand(program) {
|
|
|
3960
4107
|
.option("--display-name <name>", "Human-readable agent display name")
|
|
3961
4108
|
.option("--role <role>", "Agent role metadata: coder, reviewer, tester, observer", "coder")
|
|
3962
4109
|
.option("--to <agent>", "Direct the message to a specific agent id")
|
|
4110
|
+
.option("--no-auto-grant", "Skip auto-grant on 403 IDENTITY_FORGERY; fail immediately instead")
|
|
3963
4111
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
3964
4112
|
.option("--json", "Emit machine-readable output")
|
|
3965
4113
|
.action(async (sessionId, message, options, command) => {
|
|
@@ -4029,6 +4177,7 @@ export function registerSessionCommand(program) {
|
|
|
4029
4177
|
}
|
|
4030
4178
|
const remoteSync = await syncSessionEventToApi(normalizedSessionId, event, {
|
|
4031
4179
|
targetPath,
|
|
4180
|
+
noAutoGrant: options.autoGrant === false,
|
|
4032
4181
|
});
|
|
4033
4182
|
if (!remoteSync?.synced) {
|
|
4034
4183
|
throw new Error(
|
|
@@ -4096,6 +4245,7 @@ export function registerSessionCommand(program) {
|
|
|
4096
4245
|
.option("--proposal <text>", "Suggested remediation or next step")
|
|
4097
4246
|
.option("--message-file <path>", "Read the observation summary from a UTF-8 file")
|
|
4098
4247
|
.option("--stdin", "Read the observation summary from stdin")
|
|
4248
|
+
.option("--no-auto-grant", "Skip auto-grant on 403 IDENTITY_FORGERY; fail immediately instead")
|
|
4099
4249
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
4100
4250
|
.option("--json", "Emit machine-readable output")
|
|
4101
4251
|
.action(async (sessionId, summaryParts, options, command) => {
|
|
@@ -4201,6 +4351,7 @@ export function registerSessionCommand(program) {
|
|
|
4201
4351
|
}
|
|
4202
4352
|
const remoteSync = await syncSessionEventToApi(normalizedSessionId, event, {
|
|
4203
4353
|
targetPath,
|
|
4354
|
+
noAutoGrant: options.autoGrant === false,
|
|
4204
4355
|
});
|
|
4205
4356
|
if (!remoteSync?.synced) {
|
|
4206
4357
|
throw new Error(
|
|
@@ -4685,6 +4836,132 @@ export function registerSessionCommand(program) {
|
|
|
4685
4836
|
return payload;
|
|
4686
4837
|
});
|
|
4687
4838
|
|
|
4839
|
+
const filesCommand = session
|
|
4840
|
+
.command("files <sessionId>")
|
|
4841
|
+
.description(
|
|
4842
|
+
"List the files published in a room: name, size, type, who uploaded it and when, and the attachment id to download it with",
|
|
4843
|
+
)
|
|
4844
|
+
.option("--limit <n>", `Most files to list (default ${DEFAULT_ROOM_FILES_LIMIT}, max ${MAX_ROOM_FILES_LIMIT})`)
|
|
4845
|
+
.option(
|
|
4846
|
+
"--max-pages <n>",
|
|
4847
|
+
`Most pages of room history to read, 200 events each (default ${DEFAULT_ROOM_FILES_MAX_PAGES})`,
|
|
4848
|
+
)
|
|
4849
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
4850
|
+
.option("--json", "Emit machine-readable output")
|
|
4851
|
+
.addHelpText(
|
|
4852
|
+
"after",
|
|
4853
|
+
"\nDownload a file with: sl session files get <sessionId> <attachmentId> [--out <path>] [--force]",
|
|
4854
|
+
)
|
|
4855
|
+
.action(async (sessionId, options, command) => {
|
|
4856
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
4857
|
+
if (!normalizedSessionId) {
|
|
4858
|
+
throw new Error("session id is required.");
|
|
4859
|
+
}
|
|
4860
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
4861
|
+
const limit = parsePositiveInteger(options.limit, "limit", DEFAULT_ROOM_FILES_LIMIT);
|
|
4862
|
+
const maxPages = parsePositiveInteger(options.maxPages, "max-pages", DEFAULT_ROOM_FILES_MAX_PAGES);
|
|
4863
|
+
const result = await listRoomFiles(normalizedSessionId, { targetPath, limit, maxPages });
|
|
4864
|
+
if (!result.ok) {
|
|
4865
|
+
throw new Error(`Could not list room files (${result.reason || "unknown"}).`);
|
|
4866
|
+
}
|
|
4867
|
+
const payload = {
|
|
4868
|
+
command: "session files",
|
|
4869
|
+
sessionId: normalizedSessionId,
|
|
4870
|
+
count: result.count,
|
|
4871
|
+
complete: result.complete,
|
|
4872
|
+
pages: result.pages,
|
|
4873
|
+
scannedEvents: result.scannedEvents,
|
|
4874
|
+
files: result.files,
|
|
4875
|
+
};
|
|
4876
|
+
if (shouldEmitJson(options, command)) {
|
|
4877
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
4878
|
+
return payload;
|
|
4879
|
+
}
|
|
4880
|
+
if (!result.count) {
|
|
4881
|
+
console.log(
|
|
4882
|
+
pc.gray(
|
|
4883
|
+
result.complete
|
|
4884
|
+
? "No files have been published in this room."
|
|
4885
|
+
: `No files in the last ${result.scannedEvents} events; raise --max-pages to read further back.`,
|
|
4886
|
+
),
|
|
4887
|
+
);
|
|
4888
|
+
return payload;
|
|
4889
|
+
}
|
|
4890
|
+
console.log(pc.bold(`Files published in ${normalizedSessionId} (${result.count}, newest first)`));
|
|
4891
|
+
for (const file of result.files) {
|
|
4892
|
+
const type = file.contentType || file.kind || "unknown type";
|
|
4893
|
+
const who = file.uploadedBy ? ` by ${file.uploadedBy}` : "";
|
|
4894
|
+
const when = file.uploadedAt || file.publishedAt;
|
|
4895
|
+
const message = file.publishedSequenceId ? ` in #${file.publishedSequenceId}` : "";
|
|
4896
|
+
console.log(`${file.filename || "(unnamed)"} ${formatFileSize(file.sizeBytes)} ${type}${who}${when ? ` ${when}` : ""}${message}`);
|
|
4897
|
+
console.log(pc.gray(` attachment id ${file.attachmentId}`));
|
|
4898
|
+
}
|
|
4899
|
+
if (!result.complete) {
|
|
4900
|
+
console.log(pc.gray("More files may exist further back; raise --limit or --max-pages."));
|
|
4901
|
+
}
|
|
4902
|
+
console.log(pc.gray(`Download one with: sl session files get ${normalizedSessionId} <attachmentId> [--out <path>]`));
|
|
4903
|
+
return payload;
|
|
4904
|
+
});
|
|
4905
|
+
|
|
4906
|
+
filesCommand
|
|
4907
|
+
.command("get <sessionId> <attachmentId>")
|
|
4908
|
+
.description(
|
|
4909
|
+
"Download one published room file: streamed to disk, checked against the server's sha256, and written atomically",
|
|
4910
|
+
)
|
|
4911
|
+
.option("--out <path>", "File or directory to write to (default: the server's file name in the workspace)")
|
|
4912
|
+
.option("--force", "Overwrite an existing file")
|
|
4913
|
+
.option("--path <path>", "Workspace path; a relative --out must stay inside it", ".")
|
|
4914
|
+
.option("--json", "Emit machine-readable output")
|
|
4915
|
+
.addHelpText(
|
|
4916
|
+
"after",
|
|
4917
|
+
"\nThe destination folder is checked when the download starts; this does not protect against another local process renaming or relinking folders during the download.",
|
|
4918
|
+
)
|
|
4919
|
+
.action(async (sessionId, attachmentId, options, command) => {
|
|
4920
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
4921
|
+
const normalizedAttachmentId = normalizeString(attachmentId);
|
|
4922
|
+
if (!normalizedSessionId) {
|
|
4923
|
+
throw new Error("session id is required.");
|
|
4924
|
+
}
|
|
4925
|
+
// `files` also defines --path and --json, so Commander gives them to it: read the merged values.
|
|
4926
|
+
const merged = typeof command?.optsWithGlobals === "function" ? command.optsWithGlobals() : options;
|
|
4927
|
+
const targetPath = path.resolve(process.cwd(), String(merged.path || options.path || "."));
|
|
4928
|
+
let result;
|
|
4929
|
+
try {
|
|
4930
|
+
result = await downloadRoomFile(normalizedSessionId, normalizedAttachmentId, {
|
|
4931
|
+
targetPath,
|
|
4932
|
+
out: options.out,
|
|
4933
|
+
force: Boolean(options.force),
|
|
4934
|
+
});
|
|
4935
|
+
} catch (error) {
|
|
4936
|
+
if (error instanceof RoomFileError) {
|
|
4937
|
+
throw new Error(error.message);
|
|
4938
|
+
}
|
|
4939
|
+
throw error;
|
|
4940
|
+
}
|
|
4941
|
+
const payload = {
|
|
4942
|
+
command: "session files get",
|
|
4943
|
+
sessionId: normalizedSessionId,
|
|
4944
|
+
attachmentId: normalizedAttachmentId,
|
|
4945
|
+
path: result.path,
|
|
4946
|
+
bytes: result.bytes,
|
|
4947
|
+
sha256: result.sha256,
|
|
4948
|
+
verified: result.verified,
|
|
4949
|
+
overwritten: result.overwritten,
|
|
4950
|
+
linkRefetched: result.linkRefetched,
|
|
4951
|
+
attachment: result.attachment,
|
|
4952
|
+
};
|
|
4953
|
+
if (shouldEmitJson(options, command)) {
|
|
4954
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
4955
|
+
return payload;
|
|
4956
|
+
}
|
|
4957
|
+
console.log(
|
|
4958
|
+
pc.green(
|
|
4959
|
+
`Saved ${result.attachment.filename || normalizedAttachmentId} (${formatFileSize(result.bytes)}, sha256 verified) to ${result.path}`,
|
|
4960
|
+
),
|
|
4961
|
+
);
|
|
4962
|
+
return payload;
|
|
4963
|
+
});
|
|
4964
|
+
|
|
4688
4965
|
const runFileLockCommand = async (verb, sessionId, files, options, command) => {
|
|
4689
4966
|
const normalizedSessionId = normalizeString(sessionId);
|
|
4690
4967
|
if (!normalizedSessionId) {
|
|
@@ -5331,6 +5608,7 @@ export function registerSessionCommand(program) {
|
|
|
5331
5608
|
"--no-presence",
|
|
5332
5609
|
"Do not renew ephemeral remote presence",
|
|
5333
5610
|
)
|
|
5611
|
+
.option("--presence-check", "Check once whether the server lists this agent after the first presence renewal")
|
|
5334
5612
|
.option(
|
|
5335
5613
|
"--model <model>",
|
|
5336
5614
|
"Model/provider label to publish with listener presence",
|
|
@@ -5587,6 +5865,29 @@ export function registerSessionCommand(program) {
|
|
|
5587
5865
|
let lastPresenceHeartbeatMs = 0;
|
|
5588
5866
|
let lastPresenceHeartbeatFingerprint = "";
|
|
5589
5867
|
let nextPresenceAttemptMs = 0;
|
|
5868
|
+
let minimalPresence = false;
|
|
5869
|
+
let presenceCheckPending = Boolean(options.presenceCheck);
|
|
5870
|
+
const checkPresence = async () => {
|
|
5871
|
+
if (!presenceCheckPending) return;
|
|
5872
|
+
presenceCheckPending = false;
|
|
5873
|
+
const roster = await fetchSessionPresence(normalizedSessionId, { targetPath });
|
|
5874
|
+
const present = roster.ok
|
|
5875
|
+
? roster.present.some((entry) => entry.agentId === agentId)
|
|
5876
|
+
: null;
|
|
5877
|
+
if (emitFormat === "ndjson") {
|
|
5878
|
+
console.log(JSON.stringify(createAgentEvent({
|
|
5879
|
+
event: "session_presence_check",
|
|
5880
|
+
agentId,
|
|
5881
|
+
sessionId: normalizedSessionId,
|
|
5882
|
+
payload: { present, reason: roster.reason || null },
|
|
5883
|
+
})));
|
|
5884
|
+
} else if (present !== null) {
|
|
5885
|
+
console.log(`Presence check: ${agentId} is ${present ? "listed" : "not listed"} in session ${normalizedSessionId}.`);
|
|
5886
|
+
}
|
|
5887
|
+
if (present === null) {
|
|
5888
|
+
console.error(`Presence check: ${agentId} presence is unknown (${roster.reason || "presence_read_failed"}).`);
|
|
5889
|
+
}
|
|
5890
|
+
};
|
|
5590
5891
|
|
|
5591
5892
|
// Periodic in-session success reminders (ack, claim work, reply
|
|
5592
5893
|
// in-thread). Long-running interactive listeners only — skipped under
|
|
@@ -5700,7 +6001,7 @@ export function registerSessionCommand(program) {
|
|
|
5700
6001
|
},
|
|
5701
6002
|
onEvent: async (event) => {
|
|
5702
6003
|
if (emitFormat === "ndjson") {
|
|
5703
|
-
console.log(JSON.stringify(event));
|
|
6004
|
+
console.log(JSON.stringify(withEditSummary(withAttachmentSummary(event))));
|
|
5704
6005
|
} else {
|
|
5705
6006
|
console.log(formatEventLine(event));
|
|
5706
6007
|
}
|
|
@@ -5762,7 +6063,10 @@ export function registerSessionCommand(program) {
|
|
|
5762
6063
|
);
|
|
5763
6064
|
}
|
|
5764
6065
|
}
|
|
5765
|
-
if (!publishPresence)
|
|
6066
|
+
if (!publishPresence) {
|
|
6067
|
+
await checkPresence();
|
|
6068
|
+
return;
|
|
6069
|
+
}
|
|
5766
6070
|
const lifecycleType = normalizeString(lifecycle?.type);
|
|
5767
6071
|
const presenceAttemptMs = Date.now();
|
|
5768
6072
|
if (lifecycleType !== "stopped" && presenceAttemptMs < nextPresenceAttemptMs) {
|
|
@@ -5789,7 +6093,7 @@ export function registerSessionCommand(program) {
|
|
|
5789
6093
|
lastPresenceHeartbeatFingerprint = heartbeatDecision.fingerprint;
|
|
5790
6094
|
lastPresenceHeartbeatMs = nowMs;
|
|
5791
6095
|
}
|
|
5792
|
-
const
|
|
6096
|
+
const presenceOptions = {
|
|
5793
6097
|
sessionId: normalizedSessionId,
|
|
5794
6098
|
targetPath,
|
|
5795
6099
|
agentId,
|
|
@@ -5799,7 +6103,18 @@ export function registerSessionCommand(program) {
|
|
|
5799
6103
|
clientKind,
|
|
5800
6104
|
listenerId,
|
|
5801
6105
|
lifecycle: publishLifecycle,
|
|
5802
|
-
|
|
6106
|
+
minimalPresence,
|
|
6107
|
+
};
|
|
6108
|
+
let presenceResult = await publishListenerPresenceEvent(presenceOptions);
|
|
6109
|
+
if (!minimalPresence && presenceResult?.reason === "api_403" && presenceResult?.code === "AGENT_IDENTITY_REQUIRED") {
|
|
6110
|
+
minimalPresence = true;
|
|
6111
|
+
console.error("Presence renewal refused (403 AGENT_IDENTITY_REQUIRED; runtime/working require scoped agent credentials); retrying with agentId and model only for this listener session.");
|
|
6112
|
+
presenceResult = await publishListenerPresenceEvent({ ...presenceOptions, minimalPresence });
|
|
6113
|
+
}
|
|
6114
|
+
if (!presenceResult?.ok) {
|
|
6115
|
+
console.error(`Presence renewal failed for ${agentId} (${presenceResult?.reason || "not_recorded"}); the server may not list this listener.`);
|
|
6116
|
+
}
|
|
6117
|
+
await checkPresence();
|
|
5803
6118
|
if (presenceResult?.ok) {
|
|
5804
6119
|
nextPresenceAttemptMs = 0;
|
|
5805
6120
|
return;
|
|
@@ -6600,11 +6915,11 @@ export function registerSessionCommand(program) {
|
|
|
6600
6915
|
tail,
|
|
6601
6916
|
beforeSequence,
|
|
6602
6917
|
count: events.length,
|
|
6603
|
-
events,
|
|
6918
|
+
events: events.map(withAttachmentSummary).map(withEditSummary),
|
|
6604
6919
|
includeControlEvents,
|
|
6605
6920
|
hiddenControlEventCount,
|
|
6606
6921
|
unacknowledgedHumanMessageCount: unacknowledgedHumanMessages.length,
|
|
6607
|
-
unacknowledgedHumanMessages,
|
|
6922
|
+
unacknowledgedHumanMessages: unacknowledgedHumanMessages.map(withAttachmentSummary).map(withEditSummary),
|
|
6608
6923
|
recentHumanActivityCount: recentHumanActivity.length,
|
|
6609
6924
|
recentHumanActivity,
|
|
6610
6925
|
autoView,
|
|
@@ -6706,7 +7021,7 @@ export function registerSessionCommand(program) {
|
|
|
6706
7021
|
if (item.event) {
|
|
6707
7022
|
if (!includeControlEvents && isSessionControlEvent(item.event)) continue;
|
|
6708
7023
|
if (emitJson) {
|
|
6709
|
-
console.log(JSON.stringify({ source: item.source, event: item.event }));
|
|
7024
|
+
console.log(JSON.stringify({ source: item.source, event: withEditSummary(withAttachmentSummary(item.event)) }));
|
|
6710
7025
|
} else {
|
|
6711
7026
|
const sourceTag = item.source === "sse" ? pc.cyan("[sse]") : pc.gray("[fs] ");
|
|
6712
7027
|
console.log(`${sourceTag} ${formatEventLine(item.event)}`);
|
|
@@ -6737,7 +7052,7 @@ export function registerSessionCommand(program) {
|
|
|
6737
7052
|
continue;
|
|
6738
7053
|
}
|
|
6739
7054
|
if (emitJson) {
|
|
6740
|
-
console.log(JSON.stringify(event));
|
|
7055
|
+
console.log(JSON.stringify(withEditSummary(withAttachmentSummary(event))));
|
|
6741
7056
|
} else {
|
|
6742
7057
|
console.log(formatEventLine(event));
|
|
6743
7058
|
}
|
|
@@ -6795,6 +7110,170 @@ export function registerSessionCommand(program) {
|
|
|
6795
7110
|
}
|
|
6796
7111
|
});
|
|
6797
7112
|
|
|
7113
|
+
session
|
|
7114
|
+
.command("events <sessionId>")
|
|
7115
|
+
.description("Return one raw, verifiable page of session events from the remote API (ascending by sequence, no local merge). Use --before-sequence to walk backwards.")
|
|
7116
|
+
.option("--remote", "Fetch from the SentinelLayer API (required; this command is remote-only)")
|
|
7117
|
+
.option("--before-sequence <n>", "Return events older than this sequence id (omit for newest)")
|
|
7118
|
+
.option("--limit <n>", "Maximum events (1-200, default 50)", "50")
|
|
7119
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
7120
|
+
.option("--no-auto-grant", "Accepted, has no effect (session events is read-only; reads never auto-grant)")
|
|
7121
|
+
.option("--json", "Emit machine-readable output")
|
|
7122
|
+
.action(async (sessionId, options, command) => {
|
|
7123
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
7124
|
+
if (!normalizedSessionId) {
|
|
7125
|
+
throw new Error("session id is required.");
|
|
7126
|
+
}
|
|
7127
|
+
if (!options.remote) {
|
|
7128
|
+
throw new Error("session events requires --remote (this command is remote-only; use 'session read' for local streams).");
|
|
7129
|
+
}
|
|
7130
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
7131
|
+
const beforeSequence = parseOptionalPositiveInteger(options.beforeSequence, "before-sequence");
|
|
7132
|
+
const rawLimit = parsePositiveInteger(options.limit, "limit", 50);
|
|
7133
|
+
if (rawLimit > 200) {
|
|
7134
|
+
throw new Error("limit must be between 1 and 200.");
|
|
7135
|
+
}
|
|
7136
|
+
const limit = Math.max(1, rawLimit);
|
|
7137
|
+
const emitJson = shouldEmitJson(options, command);
|
|
7138
|
+
|
|
7139
|
+
const authSession = await resolveActiveAuthSession({
|
|
7140
|
+
cwd: targetPath,
|
|
7141
|
+
env: process.env,
|
|
7142
|
+
autoRotate: false,
|
|
7143
|
+
});
|
|
7144
|
+
if (!isAuthenticated(authSession)) {
|
|
7145
|
+
throw new Error(`Remote session events requires authentication. Run \`${authLoginHint()}\` first.`);
|
|
7146
|
+
}
|
|
7147
|
+
|
|
7148
|
+
const result = await pollSessionEventsBefore(normalizedSessionId, {
|
|
7149
|
+
targetPath,
|
|
7150
|
+
beforeSequence,
|
|
7151
|
+
limit,
|
|
7152
|
+
timeoutMs: 15_000,
|
|
7153
|
+
});
|
|
7154
|
+
|
|
7155
|
+
const contractViolation = pageContractViolation(normalizedSessionId, beforeSequence, limit, result);
|
|
7156
|
+
const rawNext = result?.rawNextBeforeSequence;
|
|
7157
|
+
|
|
7158
|
+
const payload = {
|
|
7159
|
+
command: "session events",
|
|
7160
|
+
targetPath,
|
|
7161
|
+
sessionId: normalizedSessionId,
|
|
7162
|
+
ok: result.ok && !contractViolation,
|
|
7163
|
+
beforeSequence: beforeSequence || null,
|
|
7164
|
+
count: Array.isArray(result.events) ? result.events.length : 0,
|
|
7165
|
+
nextBeforeSequence: rawNext ?? null,
|
|
7166
|
+
hasMore: Boolean(result.hasMore),
|
|
7167
|
+
partial: Boolean(result.partial),
|
|
7168
|
+
events: Array.isArray(result.events) ? result.events : [],
|
|
7169
|
+
reason: contractViolation ? "page_contract_violation" : (result.reason || ""),
|
|
7170
|
+
contractViolation: contractViolation || undefined,
|
|
7171
|
+
};
|
|
7172
|
+
|
|
7173
|
+
// C3: non-zero exit on failure in --json mode
|
|
7174
|
+
if (emitJson && !payload.ok) {
|
|
7175
|
+
process.exitCode = 1;
|
|
7176
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
7177
|
+
return;
|
|
7178
|
+
}
|
|
7179
|
+
|
|
7180
|
+
if (emitJson) {
|
|
7181
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
7182
|
+
return;
|
|
7183
|
+
}
|
|
7184
|
+
if (!payload.ok) {
|
|
7185
|
+
if (contractViolation) {
|
|
7186
|
+
throw new Error(`Session events page contract violated: ${contractViolation}`);
|
|
7187
|
+
}
|
|
7188
|
+
throw new Error(`Session events fetch failed (${result.reason || "unknown"}).`);
|
|
7189
|
+
}
|
|
7190
|
+
for (const event of result.events || []) {
|
|
7191
|
+
const seq = Number(event.sequenceId || event.sequence_id || 0);
|
|
7192
|
+
const seqLabel = Number.isFinite(seq) && seq > 0 ? `#${Math.floor(seq)}` : "";
|
|
7193
|
+
console.log(`${seqLabel} ${formatEventLine(event)}`);
|
|
7194
|
+
}
|
|
7195
|
+
if (result.beforeSequence && result.events && result.events.length >= limit) {
|
|
7196
|
+
console.log(pc.gray(`More events before sequence ${result.beforeSequence}.`));
|
|
7197
|
+
}
|
|
7198
|
+
});
|
|
7199
|
+
|
|
7200
|
+
|
|
7201
|
+
function pageContractViolation(requestedSessionId, beforeSequence, limit, result) {
|
|
7202
|
+
if (!result || !result.ok) return null;
|
|
7203
|
+
|
|
7204
|
+
const rawEvents = result.rawEvents;
|
|
7205
|
+
const rawNextBeforeSequence = result.rawNextBeforeSequence;
|
|
7206
|
+
const hasMore = result.hasMore;
|
|
7207
|
+
const partial = result.partial;
|
|
7208
|
+
|
|
7209
|
+
// 1. rawEvents is not an Array (missing, or an object)
|
|
7210
|
+
if (!Array.isArray(rawEvents)) {
|
|
7211
|
+
return "page_events_not_array";
|
|
7212
|
+
}
|
|
7213
|
+
|
|
7214
|
+
// C6: coverage fields must be present booleans; absence is fail-closed
|
|
7215
|
+
if (typeof hasMore !== "boolean") {
|
|
7216
|
+
return "page_coverage_fields_missing";
|
|
7217
|
+
}
|
|
7218
|
+
if (typeof partial !== "boolean") {
|
|
7219
|
+
return "page_coverage_fields_missing";
|
|
7220
|
+
}
|
|
7221
|
+
|
|
7222
|
+
// C6: the bridge cannot use a partial page
|
|
7223
|
+
if (partial === true) {
|
|
7224
|
+
return "page_partial";
|
|
7225
|
+
}
|
|
7226
|
+
|
|
7227
|
+
// 5. hasMore === true and rawNextBeforeSequence is null or missing
|
|
7228
|
+
if (hasMore === true && (rawNextBeforeSequence === null || rawNextBeforeSequence === undefined)) {
|
|
7229
|
+
return "page_has_more_without_cursor";
|
|
7230
|
+
}
|
|
7231
|
+
|
|
7232
|
+
// 4. rawNextBeforeSequence is not null and is not a safe positive integer
|
|
7233
|
+
if (rawNextBeforeSequence !== null && rawNextBeforeSequence !== undefined) {
|
|
7234
|
+
if (!Number.isSafeInteger(rawNextBeforeSequence) || rawNextBeforeSequence <= 0) {
|
|
7235
|
+
return "page_next_before_not_safe_positive_integer";
|
|
7236
|
+
}
|
|
7237
|
+
if (Number.isFinite(beforeSequence) && rawNextBeforeSequence >= beforeSequence) {
|
|
7238
|
+
return "next_before_sequence_not_less_than_requested";
|
|
7239
|
+
}
|
|
7240
|
+
}
|
|
7241
|
+
|
|
7242
|
+
// 3. rawEvents.length > the requested limit
|
|
7243
|
+
if (rawEvents.length > limit) {
|
|
7244
|
+
return "page_too_many_events";
|
|
7245
|
+
}
|
|
7246
|
+
|
|
7247
|
+
// 2. Validate each raw event's sequenceId
|
|
7248
|
+
const seen = new Set();
|
|
7249
|
+
for (const event of rawEvents) {
|
|
7250
|
+
const rawSeq = event?.sequenceId;
|
|
7251
|
+
if (typeof rawSeq !== "number" || !Number.isSafeInteger(rawSeq) || rawSeq <= 0) {
|
|
7252
|
+
return "event_sequence_not_safe_positive_integer";
|
|
7253
|
+
}
|
|
7254
|
+
|
|
7255
|
+
// event's sequenceId >= beforeSequence
|
|
7256
|
+
if (Number.isFinite(beforeSequence) && rawSeq >= beforeSequence) {
|
|
7257
|
+
return "event_sequence_not_before_requested";
|
|
7258
|
+
}
|
|
7259
|
+
|
|
7260
|
+
// event carries a sessionId other than the one requested
|
|
7261
|
+
const eventSessionId = normalizeString(event?.sessionId ?? event?.session_id);
|
|
7262
|
+
if (eventSessionId && eventSessionId !== requestedSessionId) {
|
|
7263
|
+
return "event_session_id_mismatch";
|
|
7264
|
+
}
|
|
7265
|
+
|
|
7266
|
+
// duplicate sequence ids
|
|
7267
|
+
if (seen.has(rawSeq)) {
|
|
7268
|
+
return "duplicate_event_sequence";
|
|
7269
|
+
}
|
|
7270
|
+
seen.add(rawSeq);
|
|
7271
|
+
}
|
|
7272
|
+
|
|
7273
|
+
return null;
|
|
7274
|
+
}
|
|
7275
|
+
|
|
7276
|
+
|
|
6798
7277
|
session
|
|
6799
7278
|
.command("recall <need>")
|
|
6800
7279
|
.description(
|
|
@@ -7230,12 +7709,38 @@ export function registerSessionCommand(program) {
|
|
|
7230
7709
|
);
|
|
7231
7710
|
});
|
|
7232
7711
|
|
|
7712
|
+
session
|
|
7713
|
+
.command("standup <sessionId>")
|
|
7714
|
+
.description("Summarize returned ticket records and their evidence without a model call")
|
|
7715
|
+
.option("--since <24h|ISO>", "Include records updated since this time (inclusive)")
|
|
7716
|
+
.option("--agent <id>", "Agent reading the tickets (defaults to the joined session agent)")
|
|
7717
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
7718
|
+
.option("--json", "Emit machine-readable output")
|
|
7719
|
+
.action(async (sessionId, options, command) => {
|
|
7720
|
+
const result = await sessionStandup(normalizeString(sessionId), {
|
|
7721
|
+
since: options.since, targetPath: path.resolve(process.cwd(), String(options.path || ".")),
|
|
7722
|
+
});
|
|
7723
|
+
console.log(shouldEmitJson(options, command) ? JSON.stringify(result, null, 2) : formatSessionStandup(result));
|
|
7724
|
+
});
|
|
7725
|
+
|
|
7233
7726
|
// ---------------------------------------------------------------- tickets
|
|
7234
7727
|
// The room's shared backlog (API "Ticket Lease Contract (T2)"). Every subcommand is
|
|
7235
7728
|
// an ACTOR command: the runCli choke point runs it on the agent's admission.
|
|
7236
7729
|
const ticket = session
|
|
7237
7730
|
.command("ticket")
|
|
7238
|
-
.description("The room's shared backlog: list, claim, renew, release and submit tickets")
|
|
7731
|
+
.description("The room's shared backlog: list, claim, renew, release and submit tickets")
|
|
7732
|
+
.addHelpText("after", `
|
|
7733
|
+
Workflow:
|
|
7734
|
+
sl session ticket list <sessionId> --available --json
|
|
7735
|
+
sl session ticket claim <sessionId> <ticketId> --expected-version <version>
|
|
7736
|
+
sl session ticket renew <sessionId> <ticketId>
|
|
7737
|
+
sl session ticket submit <sessionId> <ticketId> --sha <commit> --evidence-file evidence.json
|
|
7738
|
+
|
|
7739
|
+
The evidence file is a JSON array of re-checkable claims. Submit means in_review,
|
|
7740
|
+
not done. Use release --reason "handoff" when you cannot continue.
|
|
7741
|
+
Agent work requires that agent's live admission; --agent is not authorization.
|
|
7742
|
+
Guide: https://sentinelayer.com/docs/senti/overview/
|
|
7743
|
+
`);
|
|
7239
7744
|
|
|
7240
7745
|
// Ticket work is attributed by the CREDENTIAL, never by a label: the lease routes take the
|
|
7241
7746
|
// holder from authentication alone. So an agent identity is allowed only with that agent's
|
|
@@ -7264,6 +7769,48 @@ export function registerSessionCommand(program) {
|
|
|
7264
7769
|
console.log(line);
|
|
7265
7770
|
}
|
|
7266
7771
|
|
|
7772
|
+
ticket
|
|
7773
|
+
.command("create <sessionId>")
|
|
7774
|
+
.description("Create a ticket in the room. Agent and source attribution are recorded only by servers that support them; the output says whether they were")
|
|
7775
|
+
.requiredOption("--title <text>", "Ticket title")
|
|
7776
|
+
.option("--body <text>", "Ticket description", "")
|
|
7777
|
+
.option("--agent <id>", "Acting agent identity, authorized by the server")
|
|
7778
|
+
.option("--assignee <id>", "Who should own the work")
|
|
7779
|
+
.option("--source-sequence <n>", "Source message in this room")
|
|
7780
|
+
.option("--idempotency-key <key>", "Reuse the key after an uncertain response")
|
|
7781
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
7782
|
+
.option("--json", "Emit machine-readable output")
|
|
7783
|
+
.action(async (sessionId, options, command) => {
|
|
7784
|
+
const sid = normalizeString(sessionId), targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
7785
|
+
const identity = await resolveSessionSayIdentity({ sessionId: sid, agentId: options.agent, targetPath });
|
|
7786
|
+
const agentId = normalizeString(identity.agentId);
|
|
7787
|
+
const result = await createTicket(sid, { title: options.title, body: options.body, assignee: options.assignee,
|
|
7788
|
+
agentId: agentId === "cli-user" ? "" : agentId, targetPath,
|
|
7789
|
+
sourceSequenceId: options.sourceSequence === undefined ? null : parsePositiveInteger(options.sourceSequence, "source-sequence", 1),
|
|
7790
|
+
...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
7791
|
+
});
|
|
7792
|
+
const { agentRequested, agentRecorded, sourceRequested, sourceRecorded } = result.provenance;
|
|
7793
|
+
const creator = result.ticket?.creator;
|
|
7794
|
+
let creatorNote = " Creator: not recorded.";
|
|
7795
|
+
if (typeof creator?.id === "string" && creator.id.trim()) {
|
|
7796
|
+
if (creator.kind === "agent") {
|
|
7797
|
+
const assurance = creator.credentialBound === true && ["session_admission", "agent_token"].includes(creator.identitySource)
|
|
7798
|
+
? "credential-bound"
|
|
7799
|
+
: creator.credentialBound === false && creator.identitySource === "authorized_agent_grant"
|
|
7800
|
+
? "authorized account grant; not a separate agent credential"
|
|
7801
|
+
: "identity assurance unavailable";
|
|
7802
|
+
creatorNote = ` Creator: agent ${creator.id} (${assurance}).`;
|
|
7803
|
+
} else if (creator.kind === "account") {
|
|
7804
|
+
creatorNote = ` Creator: account ${creator.id} (the credential holder's physical identity is not established).`;
|
|
7805
|
+
}
|
|
7806
|
+
}
|
|
7807
|
+
const notes = [
|
|
7808
|
+
agentRequested && !agentRecorded ? ` The response did not confirm requested agent ${agentRequested}.` : "",
|
|
7809
|
+
sourceRequested !== null && !sourceRecorded ? ` The response did not confirm source #${sourceRequested}.` : "",
|
|
7810
|
+
].join("");
|
|
7811
|
+
ticketOutput(options, command, { command: "session ticket create", ...result }, `Created ${result.ticket.id}.${creatorNote}${notes}`);
|
|
7812
|
+
});
|
|
7813
|
+
|
|
7267
7814
|
ticket
|
|
7268
7815
|
.command("list <sessionId>")
|
|
7269
7816
|
.description("List the room's tickets (--available: only those claimable now)")
|
|
@@ -7627,7 +8174,7 @@ export function registerSessionCommand(program) {
|
|
|
7627
8174
|
|
|
7628
8175
|
session
|
|
7629
8176
|
.command("status <sessionId>")
|
|
7630
|
-
.description("Show session status, agents, and health")
|
|
8177
|
+
.description("Show workspace session status, agents, and file-lock health")
|
|
7631
8178
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
7632
8179
|
.option("--json", "Emit machine-readable output")
|
|
7633
8180
|
.action(async (sessionId, options, command) => {
|
|
@@ -8493,6 +9040,7 @@ export function registerSessionCommand(program) {
|
|
|
8493
9040
|
});
|
|
8494
9041
|
const payload = {
|
|
8495
9042
|
command: "session setup-guides",
|
|
9043
|
+
skillsInstallCommand: "sl skills install",
|
|
8496
9044
|
targetPath,
|
|
8497
9045
|
sessionId: normalizedSessionId,
|
|
8498
9046
|
sectionHeading: result.sectionHeading,
|
|
@@ -8506,6 +9054,7 @@ export function registerSessionCommand(program) {
|
|
|
8506
9054
|
}
|
|
8507
9055
|
|
|
8508
9056
|
console.log(pc.bold(`Session guide sync complete for ${normalizedSessionId}`));
|
|
9057
|
+
console.log("Install bundled Senti agent skills: sl skills install");
|
|
8509
9058
|
console.log(pc.gray(`AGENTS.md: changed=${result.agents.changed} path=${result.agents.path}`));
|
|
8510
9059
|
console.log(pc.gray(`CLAUDE.md: changed=${result.claude.changed} path=${result.claude.path}`));
|
|
8511
9060
|
console.log(
|
|
@@ -8586,7 +9135,7 @@ export function registerSessionCommand(program) {
|
|
|
8586
9135
|
const concurrency = Math.max(1, Math.min(10, requestedConcurrency, count));
|
|
8587
9136
|
const tags = parseCsvTokens(options.tags, ["session", "swarm"]);
|
|
8588
9137
|
const apiUrl = normalizeAidenIdApiUrl(options.apiUrl);
|
|
8589
|
-
const outputRoot = await
|
|
9138
|
+
const { outputRoot, outputFs } = await resolveOutputContext({
|
|
8590
9139
|
cwd: targetPath,
|
|
8591
9140
|
outputDirOverride: options.outputDir,
|
|
8592
9141
|
env: process.env,
|
|
@@ -8679,6 +9228,7 @@ export function registerSessionCommand(program) {
|
|
|
8679
9228
|
for (const identity of provisioned) {
|
|
8680
9229
|
await recordProvisionedIdentity({
|
|
8681
9230
|
outputRoot,
|
|
9231
|
+
outputFs,
|
|
8682
9232
|
response: identity.response || {},
|
|
8683
9233
|
context: {
|
|
8684
9234
|
source: "session-provision-emails",
|