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