sentinelayer-cli 0.41.0 → 0.42.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 +18 -0
- package/package.json +3 -2
- package/src/auth/admission-scope.js +34 -0
- package/src/auth/gate.js +6 -0
- package/src/auth/http.js +4 -0
- package/src/auth/service.js +6 -0
- package/src/board/done-gate.js +155 -0
- package/src/board/evidence-resolvers.js +138 -0
- package/src/cli.js +24 -1
- package/src/commands/board.js +116 -0
- package/src/commands/session.js +483 -47
- package/src/engram/build-state.js +183 -0
- package/src/engram/canonical.js +75 -0
- package/src/engram/document.js +190 -0
- package/src/engram/index.js +11 -2
- package/src/engram/ingest-document.js +107 -0
- package/src/engram/store.js +6 -1
- package/src/engram/tools.js +6 -0
- package/src/events/schema.js +18 -0
- package/src/legacy-cli.js +2 -2
- package/src/net/test-egress-guard.js +158 -0
- package/src/review/investor-dd-orchestrator.js +14 -2
- package/src/session/admission-auth.js +312 -0
- package/src/session/admission-preimage.js +90 -0
- package/src/session/admission.js +620 -0
- package/src/session/checkpoints.js +25 -1
- package/src/session/codebase-context.js +54 -0
- package/src/session/coordination-guidance.js +1 -1
- package/src/session/daemon-spawn.js +20 -2
- package/src/session/daemon.js +7 -1
- package/src/session/event-identity.js +9 -2
- package/src/session/first-message.js +11 -3
- package/src/session/listener.js +3 -0
- package/src/session/live-source.js +47 -16
- package/src/session/message-edits.js +89 -0
- package/src/session/recap.js +26 -4
- package/src/session/remote-hydrate.js +7 -19
- package/src/session/store.js +13 -14
- package/src/session/stream.js +23 -3
- package/src/session/sync.js +128 -11
- package/src/session/tickets.js +316 -0
- package/src/session/title-sync.js +2 -1
- package/src/session/usage-report.js +41 -2
- package/src/session/wake/resolve-target.js +10 -5
package/src/commands/session.js
CHANGED
|
@@ -91,6 +91,27 @@ import {
|
|
|
91
91
|
normalizeSessionOnboarding,
|
|
92
92
|
writeSessionOnboardingBrief,
|
|
93
93
|
} from "../session/invitations.js";
|
|
94
|
+
import {
|
|
95
|
+
cancelAdmission,
|
|
96
|
+
fetchOwnAdmissionReceipt,
|
|
97
|
+
parseScope as parseAdmissionScope,
|
|
98
|
+
parseTtlSeconds as parseAdmissionTtlSeconds,
|
|
99
|
+
runAdmissionJoin,
|
|
100
|
+
} from "../session/admission.js";
|
|
101
|
+
import {
|
|
102
|
+
assertCanonicalAgentId,
|
|
103
|
+
canonicalAgentId,
|
|
104
|
+
currentAdmittedAgent,
|
|
105
|
+
withAgentAdmission,
|
|
106
|
+
} from "../session/admission-auth.js";
|
|
107
|
+
import {
|
|
108
|
+
claimTicket,
|
|
109
|
+
listTickets,
|
|
110
|
+
releaseTicketLease,
|
|
111
|
+
renewTicketLease,
|
|
112
|
+
submitTicket,
|
|
113
|
+
ticketEvents,
|
|
114
|
+
} from "../session/tickets.js";
|
|
94
115
|
import { postFirstSentiMessage } from "../session/first-message.js";
|
|
95
116
|
import { createListenerHostWake } from "../session/wake/listen-wake.js";
|
|
96
117
|
import { appendToStream, readStream, tailStream } from "../session/stream.js";
|
|
@@ -104,6 +125,8 @@ import {
|
|
|
104
125
|
import { readSessionPreview } from "../session/preview.js";
|
|
105
126
|
import {
|
|
106
127
|
createSessionMessageAction,
|
|
128
|
+
editSessionMessage,
|
|
129
|
+
fetchJsonWithFullTimeout,
|
|
107
130
|
fetchSessionUsageLedger,
|
|
108
131
|
fetchSessionPinnedMessages,
|
|
109
132
|
listSessionMessageActions,
|
|
@@ -120,6 +143,7 @@ import {
|
|
|
120
143
|
updateSessionReadCursor,
|
|
121
144
|
} from "../session/sync.js";
|
|
122
145
|
import { hydrateSessionFromRemote } from "../session/remote-hydrate.js";
|
|
146
|
+
import { messageRevision, replyRevisionEvent, sessionMessageRoutingEvent } from "../session/message-edits.js";
|
|
123
147
|
import { runSessionRecall } from "../session/recall/index.js";
|
|
124
148
|
import { mergeLiveSources } from "../session/live-source.js";
|
|
125
149
|
import { listenSessionEvents } from "../session/listener.js";
|
|
@@ -573,11 +597,6 @@ const SESSION_MESSAGE_ACTION_DESCRIPTIONS = Object.freeze([
|
|
|
573
597
|
command: "sl session action <id> disregard --target-sequence <n>",
|
|
574
598
|
description: "Mark a message as intentionally ignored or superseded.",
|
|
575
599
|
},
|
|
576
|
-
{
|
|
577
|
-
type: "view",
|
|
578
|
-
command: "sl session view <id> <sequence>",
|
|
579
|
-
description: "Manually backfill a read receipt for a target message; remote reads record views automatically.",
|
|
580
|
-
},
|
|
581
600
|
]);
|
|
582
601
|
|
|
583
602
|
function normalizeSessionMessageActionType(value) {
|
|
@@ -669,6 +688,11 @@ function buildSessionActionEvent(sessionId, action = {}) {
|
|
|
669
688
|
},
|
|
670
689
|
sessionId,
|
|
671
690
|
ts: actionCreatedAt(action),
|
|
691
|
+
messageRevision: messageRevision(action),
|
|
692
|
+
editedAt: action.editedAt,
|
|
693
|
+
editedBy: action.editedBy,
|
|
694
|
+
canEdit: action.canEdit,
|
|
695
|
+
revisionEvidence: action.revisionEvidence,
|
|
672
696
|
payload: {
|
|
673
697
|
actionId: id,
|
|
674
698
|
actionType,
|
|
@@ -676,6 +700,7 @@ function buildSessionActionEvent(sessionId, action = {}) {
|
|
|
676
700
|
targetCursor: actionTargetCursor(action) || null,
|
|
677
701
|
targetActionId: actionTargetActionId(action) || null,
|
|
678
702
|
note: normalizeString(action.note) || null,
|
|
703
|
+
metadata: action.metadata && typeof action.metadata === "object" ? action.metadata : undefined,
|
|
679
704
|
message: actionDisplayMessage(action),
|
|
680
705
|
source: "session_action",
|
|
681
706
|
},
|
|
@@ -1404,19 +1429,22 @@ async function findReusableSessionCandidate({
|
|
|
1404
1429
|
// can exercise the local materialization path without a real API.
|
|
1405
1430
|
async function fetchRemoteSessionDetail(endpoint, headers) {
|
|
1406
1431
|
let response;
|
|
1432
|
+
let payload;
|
|
1407
1433
|
try {
|
|
1408
|
-
response = await
|
|
1434
|
+
({ response, payload } = await fetchJsonWithFullTimeout(
|
|
1435
|
+
endpoint, { method: "GET", headers, redirect: "error" }, 2_000,
|
|
1436
|
+
));
|
|
1409
1437
|
} catch (err) {
|
|
1410
1438
|
return {
|
|
1411
1439
|
ok: false,
|
|
1412
1440
|
response: null,
|
|
1413
1441
|
status: 0,
|
|
1414
|
-
reason: normalizeString(err?.message) || "fetch_failed",
|
|
1442
|
+
reason: err?.name === "AbortError" ? "request_timeout" : (normalizeString(err?.message) || "fetch_failed"),
|
|
1415
1443
|
retryable: true,
|
|
1416
1444
|
};
|
|
1417
1445
|
}
|
|
1418
1446
|
if (response && response.ok) {
|
|
1419
|
-
return { ok: true,
|
|
1447
|
+
return { ok: true, payload, status: response.status, reason: "", retryable: false };
|
|
1420
1448
|
}
|
|
1421
1449
|
if (!response) {
|
|
1422
1450
|
return { ok: false, response: null, status: 0, reason: "no_response", retryable: true };
|
|
@@ -1431,8 +1459,7 @@ async function fetchRemoteSessionDetail(endpoint, headers) {
|
|
|
1431
1459
|
};
|
|
1432
1460
|
}
|
|
1433
1461
|
|
|
1434
|
-
|
|
1435
|
-
const body = await response.json().catch(() => ({}));
|
|
1462
|
+
function parseRemoteSessionDetailResponse(body) {
|
|
1436
1463
|
return body && body.session && typeof body.session === "object"
|
|
1437
1464
|
? body.session
|
|
1438
1465
|
: body && typeof body === "object"
|
|
@@ -1475,7 +1502,7 @@ async function verifyRemoteSession(sessionId, { targetPath } = {}) {
|
|
|
1475
1502
|
return {
|
|
1476
1503
|
ok: true,
|
|
1477
1504
|
source: "singleton",
|
|
1478
|
-
session:
|
|
1505
|
+
session: parseRemoteSessionDetailResponse(detail.payload),
|
|
1479
1506
|
status: detail.status,
|
|
1480
1507
|
};
|
|
1481
1508
|
}
|
|
@@ -1769,7 +1796,13 @@ export async function ensureWorkspaceSession({
|
|
|
1769
1796
|
);
|
|
1770
1797
|
let titleSync = null;
|
|
1771
1798
|
if (shouldPushTitle) {
|
|
1772
|
-
titleSync = await pushSessionTitleToApi(created.sessionId, effectiveTitle, {
|
|
1799
|
+
titleSync = await pushSessionTitleToApi(created.sessionId, effectiveTitle, {
|
|
1800
|
+
targetPath,
|
|
1801
|
+
// Startup uses the already validated credential; rotation and Retry-After
|
|
1802
|
+
// sleeps belong to explicit auth/title operations, not optional startup.
|
|
1803
|
+
autoRotate: false,
|
|
1804
|
+
maxRetries: 0,
|
|
1805
|
+
});
|
|
1773
1806
|
}
|
|
1774
1807
|
|
|
1775
1808
|
return {
|
|
@@ -1788,11 +1821,8 @@ export async function ensureWorkspaceSession({
|
|
|
1788
1821
|
}
|
|
1789
1822
|
|
|
1790
1823
|
function normalizeAgentId(value, fallbackValue = "cli-user") {
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
.replace(/[^a-z0-9._-]+/g, "-")
|
|
1794
|
-
.replace(/^-+|-+$/g, "");
|
|
1795
|
-
return normalized || fallbackValue;
|
|
1824
|
+
// One canonicalisation for authorisation and execution (see admission-auth.js).
|
|
1825
|
+
return canonicalAgentId(value) || fallbackValue;
|
|
1796
1826
|
}
|
|
1797
1827
|
|
|
1798
1828
|
function canPublishListenerPresence(agentId) {
|
|
@@ -2389,6 +2419,11 @@ function formatEventLine(event = {}) {
|
|
|
2389
2419
|
const ts = normalizeString(event.ts || event.timestamp);
|
|
2390
2420
|
const type = normalizeString(event.event || event.type) || "event";
|
|
2391
2421
|
const agentId = normalizeString(event.agent?.id || event.agentId || "unknown");
|
|
2422
|
+
if (type === "session_message_edited") {
|
|
2423
|
+
const routed = sessionMessageRoutingEvent(event);
|
|
2424
|
+
const target = event.payload?.targetActionId ? `reply ${event.payload.targetActionId}` : `#${event.payload?.targetSequenceId || "?"}`;
|
|
2425
|
+
return `${ts} ${agentId} ${type} ${target} revision ${event.payload?.messageRevision || "?"}: ${normalizeString(routed.payload?.message || routed.payload?.text || "")}`;
|
|
2426
|
+
}
|
|
2392
2427
|
const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
|
|
2393
2428
|
if (type === "session_action" || type === "session_reply" || type === "session_reaction") {
|
|
2394
2429
|
const actionType = normalizeString(payload.actionType) || type.replace(/^session_/, "");
|
|
@@ -2809,6 +2844,43 @@ async function emitAgentKilledEvent(sessionId, agentId, {
|
|
|
2809
2844
|
return event;
|
|
2810
2845
|
}
|
|
2811
2846
|
|
|
2847
|
+
function collectOption(value, previous) {
|
|
2848
|
+
return [...(previous || []), value];
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
/**
|
|
2852
|
+
* Wrap a command action so it runs as its agent: inside that agent's admission when
|
|
2853
|
+
* this machine holds one (every session API call then uses the admission credential
|
|
2854
|
+
* and never the human token), on the legacy path when it holds none, and refused when
|
|
2855
|
+
* what it holds is expired or unusable. `resolveIds` names the (session, agent) pair
|
|
2856
|
+
* exactly the way the action itself resolves it.
|
|
2857
|
+
*/
|
|
2858
|
+
function asAdmittedAgent(resolveIds, action) {
|
|
2859
|
+
return async (...args) => {
|
|
2860
|
+
const { sessionId, agentId } = await resolveIds(...args);
|
|
2861
|
+
return withAgentAdmission(sessionId, agentId, () => action(...args));
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
async function sessionSayAgent(sessionId, _messageParts, options) {
|
|
2866
|
+
const sid = normalizeString(sessionId);
|
|
2867
|
+
const identity = await resolveSessionSayIdentity({
|
|
2868
|
+
sessionId: sid,
|
|
2869
|
+
agentId: options.agent,
|
|
2870
|
+
targetPath: path.resolve(process.cwd(), String(options.path || ".")),
|
|
2871
|
+
});
|
|
2872
|
+
return { sessionId: sid, agentId: identity.agentId };
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
async function sessionReadAgent(sessionId, options) {
|
|
2876
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
2877
|
+
return { sessionId: normalizeString(sessionId), agentId: await defaultAgentId(options.agent, targetPath) };
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
async function sessionListenAgent(options) {
|
|
2881
|
+
return { sessionId: resolveSessionIdOption(options), agentId: normalizeAgentId(options.agent, "cli-user") };
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2812
2884
|
export function registerSessionCommand(program) {
|
|
2813
2885
|
const session = program
|
|
2814
2886
|
.command("session")
|
|
@@ -2923,8 +2995,9 @@ export function registerSessionCommand(program) {
|
|
|
2923
2995
|
};
|
|
2924
2996
|
|
|
2925
2997
|
// Best-effort admin visibility sync. Session creation remains local-first.
|
|
2998
|
+
let metadataSync = Promise.resolve(null);
|
|
2926
2999
|
if (remoteSync.attempted) {
|
|
2927
|
-
|
|
3000
|
+
metadataSync = syncSessionMetadataToApi(created.sessionId, {
|
|
2928
3001
|
targetPath,
|
|
2929
3002
|
sessionId: created.sessionId,
|
|
2930
3003
|
status: created.status,
|
|
@@ -2934,7 +3007,7 @@ export function registerSessionCommand(program) {
|
|
|
2934
3007
|
ttlSeconds,
|
|
2935
3008
|
template: created.template,
|
|
2936
3009
|
codebaseContext: created.codebaseContext,
|
|
2937
|
-
}).catch(() =>
|
|
3010
|
+
}, { targetPath, timeoutMs: 2_000 }).catch(() => null);
|
|
2938
3011
|
}
|
|
2939
3012
|
|
|
2940
3013
|
// Make the session managed by default: spawn the Senti daemon as a
|
|
@@ -2971,6 +3044,14 @@ export function registerSessionCommand(program) {
|
|
|
2971
3044
|
}).catch((error) => ({ posted: false, reason: normalizeString(error?.message) || "error" }));
|
|
2972
3045
|
}
|
|
2973
3046
|
payload.firstMessage = firstMessage;
|
|
3047
|
+
// Own the request lifecycle; do not leave a referenced fetch running
|
|
3048
|
+
// after emitting the single JSON document (even with --no-daemon).
|
|
3049
|
+
const metadataResult = await metadataSync;
|
|
3050
|
+
if (remoteSync.attempted) {
|
|
3051
|
+
remoteSync.status = metadataResult?.synced ? "synced" : "degraded";
|
|
3052
|
+
remoteSync.metadataSynced = Boolean(metadataResult?.synced);
|
|
3053
|
+
remoteSync.reason = metadataResult?.synced ? "metadata_acknowledged" : (metadataResult?.reason || "metadata_sync_failed");
|
|
3054
|
+
}
|
|
2974
3055
|
|
|
2975
3056
|
if (shouldEmitJson(options, command)) {
|
|
2976
3057
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -3319,6 +3400,21 @@ export function registerSessionCommand(program) {
|
|
|
3319
3400
|
.option("--role <role>", "Agent role: coder, reviewer, tester, observer", "coder")
|
|
3320
3401
|
.option("--model <model>", "Agent model hint", "cli")
|
|
3321
3402
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
3403
|
+
.option(
|
|
3404
|
+
"--goal <text>",
|
|
3405
|
+
"Request goal-and-scope admission: say what this agent is coming to do. A room owner approves it on the web; the agent then receives its own scoped, expiring identity.",
|
|
3406
|
+
)
|
|
3407
|
+
.option("--deliverable <text>", "An output this agent will produce (repeatable)", collectOption, [])
|
|
3408
|
+
.option("--stop-when <text>", "A condition that ends this agent's work (repeatable)", collectOption, [])
|
|
3409
|
+
.option(
|
|
3410
|
+
"--scope <actions>",
|
|
3411
|
+
"Comma-separated actions to request: session.read, session.post, session.react, tickets.read, tickets.work",
|
|
3412
|
+
)
|
|
3413
|
+
.option("--ttl <duration>", "How long the grant should last, e.g. 90m or 2h (5m..24h)", "2h")
|
|
3414
|
+
.option("--provider <name>", "Model provider hint for the admission request")
|
|
3415
|
+
.option("--no-wait", "Request admission and return immediately with the approval URL")
|
|
3416
|
+
.option("--wait-seconds <n>", "How long to wait for approval before returning pending", "900")
|
|
3417
|
+
.option("--cancel-admission", "Cancel this workspace's pending admission request for the session")
|
|
3322
3418
|
.option("--json", "Emit machine-readable output")
|
|
3323
3419
|
.action(async (sessionId, options, command) => {
|
|
3324
3420
|
const normalizedSessionId = normalizeString(sessionId);
|
|
@@ -3329,6 +3425,34 @@ export function registerSessionCommand(program) {
|
|
|
3329
3425
|
const explicitAgent = normalizeString(options.agent);
|
|
3330
3426
|
const legacyName = normalizeString(options.name);
|
|
3331
3427
|
const inviteToken = normalizeString(options.inviteToken);
|
|
3428
|
+
const emitJson = shouldEmitJson(options, command);
|
|
3429
|
+
// ONE requested identity for the whole join, validated before any side effect.
|
|
3430
|
+
// --name is the identity only when --agent is absent (its legacy alias); beside an
|
|
3431
|
+
// explicit --agent it is a free-text display label. Either way the identity is
|
|
3432
|
+
// used in its canonical spelling from here on, so the admission, its receipt,
|
|
3433
|
+
// the local registration and every later implicit command name the same agent.
|
|
3434
|
+
if (!explicitAgent) assertCanonicalAgentId(legacyName, "--name");
|
|
3435
|
+
const requestedAgent = canonicalAgentId(explicitAgent || legacyName);
|
|
3436
|
+
if (options.cancelAdmission) {
|
|
3437
|
+
const cancelled = await cancelAdmission(normalizedSessionId, {
|
|
3438
|
+
agentId: requestedAgent,
|
|
3439
|
+
targetPath,
|
|
3440
|
+
});
|
|
3441
|
+
const payload = {
|
|
3442
|
+
command: "session join",
|
|
3443
|
+
joined: false,
|
|
3444
|
+
sessionId: normalizedSessionId,
|
|
3445
|
+
admission: {
|
|
3446
|
+
status: cancelled?.status || "cancelled",
|
|
3447
|
+
admissionId: cancelled?.admissionId || null,
|
|
3448
|
+
},
|
|
3449
|
+
};
|
|
3450
|
+
if (emitJson) console.log(JSON.stringify(payload, null, 2));
|
|
3451
|
+
else console.log(`Admission request ${payload.admission.admissionId || ""} cancelled.`);
|
|
3452
|
+
return;
|
|
3453
|
+
}
|
|
3454
|
+
// Membership first. Only a member may ask for an agent to be admitted, so an
|
|
3455
|
+
// invitation is accepted BEFORE the admission request, never after.
|
|
3332
3456
|
let invitationAccept = null;
|
|
3333
3457
|
let invitationAcceptResult = null;
|
|
3334
3458
|
if (inviteToken) {
|
|
@@ -3336,7 +3460,7 @@ export function registerSessionCommand(program) {
|
|
|
3336
3460
|
targetPath,
|
|
3337
3461
|
invitationToken: inviteToken,
|
|
3338
3462
|
seatKey: options.seatKey,
|
|
3339
|
-
agentId:
|
|
3463
|
+
agentId: requestedAgent,
|
|
3340
3464
|
idempotencyKey: options.idempotencyKey,
|
|
3341
3465
|
});
|
|
3342
3466
|
invitationAcceptResult =
|
|
@@ -3344,6 +3468,55 @@ export function registerSessionCommand(program) {
|
|
|
3344
3468
|
? invitationAccept.result
|
|
3345
3469
|
: null;
|
|
3346
3470
|
}
|
|
3471
|
+
let admission = null;
|
|
3472
|
+
if (normalizeString(options.goal)) {
|
|
3473
|
+
const admissionAgent = requestedAgent;
|
|
3474
|
+
if (!admissionAgent) {
|
|
3475
|
+
throw new Error("--goal requires --agent <id>: admission is requested for one named agent.");
|
|
3476
|
+
}
|
|
3477
|
+
const waitSeconds = Number.parseInt(String(options.waitSeconds ?? "900"), 10);
|
|
3478
|
+
admission = await runAdmissionJoin(normalizedSessionId, {
|
|
3479
|
+
agentId: admissionAgent,
|
|
3480
|
+
displayName: legacyName && explicitAgent ? legacyName : "",
|
|
3481
|
+
model: options.model,
|
|
3482
|
+
provider: options.provider,
|
|
3483
|
+
goal: options.goal,
|
|
3484
|
+
deliverables: options.deliverable || [],
|
|
3485
|
+
stopConditions: options.stopWhen || [],
|
|
3486
|
+
actions: parseAdmissionScope(options.scope),
|
|
3487
|
+
ttlSeconds: parseAdmissionTtlSeconds(options.ttl),
|
|
3488
|
+
wait: options.wait !== false,
|
|
3489
|
+
waitTimeoutMs: (Number.isFinite(waitSeconds) && waitSeconds >= 0 ? waitSeconds : 900) * 1000,
|
|
3490
|
+
targetPath,
|
|
3491
|
+
onPending: ({ approveUrl }) => {
|
|
3492
|
+
// stderr, so --json stdout stays a single parseable document.
|
|
3493
|
+
const line = `Waiting for a room owner to approve this agent: ${approveUrl}`;
|
|
3494
|
+
if (emitJson) process.stderr.write(`${line}\n`);
|
|
3495
|
+
else console.log(pc.yellow(line));
|
|
3496
|
+
},
|
|
3497
|
+
});
|
|
3498
|
+
if (admission.status !== "active") {
|
|
3499
|
+
// Only an ACTIVE admission joins. Pending, denied, cancelled or expired never
|
|
3500
|
+
// registers the agent or materializes the room for it.
|
|
3501
|
+
process.exitCode = admission.status === "pending" ? 3 : 4;
|
|
3502
|
+
const payload = {
|
|
3503
|
+
command: "session join",
|
|
3504
|
+
joined: false,
|
|
3505
|
+
sessionId: normalizedSessionId,
|
|
3506
|
+
agentId: admissionAgent,
|
|
3507
|
+
invitationAccepted: Boolean(invitationAcceptResult),
|
|
3508
|
+
admission,
|
|
3509
|
+
};
|
|
3510
|
+
if (emitJson) {
|
|
3511
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
3512
|
+
} else if (admission.status === "pending") {
|
|
3513
|
+
console.log(pc.yellow(`Admission pending. A room owner approves it here: ${admission.approveUrl || "(see the session page)"}`));
|
|
3514
|
+
} else {
|
|
3515
|
+
console.log(pc.red(`Admission ${admission.status}. The agent was not joined.`));
|
|
3516
|
+
}
|
|
3517
|
+
return;
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3347
3520
|
|
|
3348
3521
|
// PR #483 contract: verify the session exists and the caller has access
|
|
3349
3522
|
// BEFORE materializing local cache state. Without this we'd silently
|
|
@@ -3355,7 +3528,48 @@ export function registerSessionCommand(program) {
|
|
|
3355
3528
|
// before the invite is accepted the user may legitimately receive 403,
|
|
3356
3529
|
// so --invite-token performs the guarded accept mutation first and this
|
|
3357
3530
|
// verification proves membership immediately afterward.
|
|
3358
|
-
|
|
3531
|
+
// An admitted agent proves its grant is live with ITS OWN credential, via the
|
|
3532
|
+
// grant's receipt, and never with the human token. A cached "active" is not
|
|
3533
|
+
// reported until this succeeds (a revoked grant is not joined), and nothing
|
|
3534
|
+
// about the room is fetched on the human's authority.
|
|
3535
|
+
const admittedAgentId = admission ? requestedAgent : "";
|
|
3536
|
+
let admissionReceipt = null;
|
|
3537
|
+
if (admission) {
|
|
3538
|
+
try {
|
|
3539
|
+
admissionReceipt = await withAgentAdmission(normalizedSessionId, admittedAgentId, () =>
|
|
3540
|
+
fetchOwnAdmissionReceipt(normalizedSessionId, { targetPath }),
|
|
3541
|
+
);
|
|
3542
|
+
// The grant must be for EXACTLY the identity this join registers, byte for
|
|
3543
|
+
// byte: a receipt for any other spelling is a different agent's authority.
|
|
3544
|
+
const receiptAgent = String(admissionReceipt?.agentId ?? "");
|
|
3545
|
+
if (receiptAgent !== admittedAgentId) {
|
|
3546
|
+
throw new Error(
|
|
3547
|
+
`the admission is for agent "${receiptAgent || "(none)"}", not "${admittedAgentId}"`,
|
|
3548
|
+
);
|
|
3549
|
+
}
|
|
3550
|
+
} catch (error) {
|
|
3551
|
+
process.exitCode = 4;
|
|
3552
|
+
const payload = {
|
|
3553
|
+
command: "session join",
|
|
3554
|
+
joined: false,
|
|
3555
|
+
sessionId: normalizedSessionId,
|
|
3556
|
+
agentId: admittedAgentId,
|
|
3557
|
+
admission: {
|
|
3558
|
+
...admission,
|
|
3559
|
+
status: "not_accepted",
|
|
3560
|
+
verified: false,
|
|
3561
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
3562
|
+
},
|
|
3563
|
+
};
|
|
3564
|
+
if (emitJson) console.log(JSON.stringify(payload, null, 2));
|
|
3565
|
+
else console.log(pc.red(`The API no longer accepts this agent's admission; it was not joined.`));
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
admission = { ...admission, verified: true, receipt: admissionReceipt };
|
|
3569
|
+
}
|
|
3570
|
+
const verification = admission
|
|
3571
|
+
? { ok: true, source: "admission_receipt", session: { sessionId: normalizedSessionId } }
|
|
3572
|
+
: await verifyRemoteSession(normalizedSessionId, { targetPath });
|
|
3359
3573
|
if (!verification.ok) {
|
|
3360
3574
|
if (verification.status === 404 || verification.reason === "not_found") {
|
|
3361
3575
|
throw new Error(
|
|
@@ -3382,17 +3596,31 @@ export function registerSessionCommand(program) {
|
|
|
3382
3596
|
skipRemoteProbe: true,
|
|
3383
3597
|
remoteSession,
|
|
3384
3598
|
});
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3599
|
+
// History only for a grant that may read it, and then on the admission.
|
|
3600
|
+
const joinHydration = !admission
|
|
3601
|
+
? await hydrateJoinBriefingContext(normalizedSessionId, { targetPath })
|
|
3602
|
+
: (admissionReceipt?.actions || []).includes("session.read")
|
|
3603
|
+
? await withAgentAdmission(normalizedSessionId, admittedAgentId, () =>
|
|
3604
|
+
hydrateJoinBriefingContext(normalizedSessionId, { targetPath }),
|
|
3605
|
+
)
|
|
3606
|
+
: { skipped: true, reason: "the grant does not include session.read" };
|
|
3388
3607
|
|
|
3389
3608
|
const acceptedOnboarding = normalizeSessionOnboarding(
|
|
3390
3609
|
invitationAcceptResult?.onboarding,
|
|
3391
3610
|
invitationAcceptResult?.claimedSeat,
|
|
3392
3611
|
);
|
|
3393
3612
|
const acceptedAgentId = normalizeString(acceptedOnboarding?.agentId);
|
|
3394
|
-
|
|
3613
|
+
// An admitted join registers the admitted identity and nothing else. Otherwise an
|
|
3614
|
+
// invitation's onboarding agent id is the identity only when none was given, and
|
|
3615
|
+
// it is held to the same canonical rule before anything is registered locally.
|
|
3616
|
+
if (!admission && !explicitAgent && acceptedAgentId) {
|
|
3617
|
+
assertCanonicalAgentId(acceptedAgentId, "the invitation's agent id");
|
|
3618
|
+
}
|
|
3619
|
+
const agentSeed = admission ? admittedAgentId : explicitAgent || acceptedAgentId || legacyName;
|
|
3395
3620
|
const resolvedAgentId = await defaultAgentId(agentSeed, targetPath);
|
|
3621
|
+
if (admission && resolvedAgentId !== admittedAgentId) {
|
|
3622
|
+
throw new Error(`join would register "${resolvedAgentId}", not the admitted agent "${admittedAgentId}".`);
|
|
3623
|
+
}
|
|
3396
3624
|
const roleWasExplicit = optionWasSetByCli(command, "role");
|
|
3397
3625
|
const role =
|
|
3398
3626
|
normalizeString(roleWasExplicit ? options.role : acceptedOnboarding?.role || options.role) ||
|
|
@@ -3485,6 +3713,7 @@ export function registerSessionCommand(program) {
|
|
|
3485
3713
|
jsonPath: onboardingBrief.jsonPath,
|
|
3486
3714
|
}
|
|
3487
3715
|
: null,
|
|
3716
|
+
admission,
|
|
3488
3717
|
};
|
|
3489
3718
|
if (shouldEmitJson(options, command)) {
|
|
3490
3719
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -3505,6 +3734,23 @@ export function registerSessionCommand(program) {
|
|
|
3505
3734
|
console.log(pc.green(`Invitation accepted${seatLabel}`));
|
|
3506
3735
|
}
|
|
3507
3736
|
console.log(pc.gray(`agent=${joined.agentId} role=${joined.role} model=${joined.model}`));
|
|
3737
|
+
if (admission) {
|
|
3738
|
+
if (admission.status === "active") {
|
|
3739
|
+
const expires = new Date(admission.grant.expiresAt * 1000).toISOString();
|
|
3740
|
+
console.log(
|
|
3741
|
+
pc.green(
|
|
3742
|
+
`Admitted${admission.reused ? " (existing grant)" : ""}: ${admission.grant.actions.join(", ")} until ${expires}`,
|
|
3743
|
+
),
|
|
3744
|
+
);
|
|
3745
|
+
if (admission.identity?.subject) console.log(pc.gray(`identity=${admission.identity.subject}`));
|
|
3746
|
+
console.log(pc.gray(`credential stored at ${admission.credential.storage} (never printed)`));
|
|
3747
|
+
} else if (admission.status === "pending") {
|
|
3748
|
+
console.log(pc.yellow(`Admission pending approval: ${admission.approveUrl}`));
|
|
3749
|
+
console.log(pc.gray("Re-run the same command to resume waiting."));
|
|
3750
|
+
} else {
|
|
3751
|
+
console.log(pc.red(`Admission ${admission.status}.`));
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3508
3754
|
if (payload.onboardingGuide?.markdownPath) {
|
|
3509
3755
|
console.log(pc.gray(`onboarding=${payload.onboardingGuide.markdownPath}`));
|
|
3510
3756
|
}
|
|
@@ -3544,7 +3790,7 @@ export function registerSessionCommand(program) {
|
|
|
3544
3790
|
.option("--local-only", "Append only to the local session cache without remote send confirmation")
|
|
3545
3791
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
3546
3792
|
.option("--json", "Emit machine-readable output")
|
|
3547
|
-
.action(async (sessionId, messageParts, options, command) => {
|
|
3793
|
+
.action(asAdmittedAgent(sessionSayAgent, async (sessionId, messageParts, options, command) => {
|
|
3548
3794
|
const normalizedSessionId = normalizeString(sessionId);
|
|
3549
3795
|
if (!normalizedSessionId) {
|
|
3550
3796
|
throw new Error("session id is required.");
|
|
@@ -3672,7 +3918,7 @@ export function registerSessionCommand(program) {
|
|
|
3672
3918
|
console.error(pc.yellow(`Identity warning: ${identity.identityWarning}`));
|
|
3673
3919
|
}
|
|
3674
3920
|
console.log(formatEventLine(persisted));
|
|
3675
|
-
});
|
|
3921
|
+
}));
|
|
3676
3922
|
|
|
3677
3923
|
session
|
|
3678
3924
|
.command("post-agent <sessionId> <message>")
|
|
@@ -3962,7 +4208,20 @@ export function registerSessionCommand(program) {
|
|
|
3962
4208
|
console.log(formatEventLine(persisted));
|
|
3963
4209
|
});
|
|
3964
4210
|
|
|
3965
|
-
|
|
4211
|
+
// Every message-action command (react, reply, ack, ...) runs as its authoring
|
|
4212
|
+
// agent: inside that agent's admission when this machine holds one.
|
|
4213
|
+
async function runMessageActionCommand(args = {}) {
|
|
4214
|
+
const sid = normalizeString(args.sessionId);
|
|
4215
|
+
const identity = await resolveMessageActionIdentity({
|
|
4216
|
+
sessionId: sid,
|
|
4217
|
+
optionAgent: args.options?.agent,
|
|
4218
|
+
targetPath: path.resolve(process.cwd(), String(args.options?.path || ".")),
|
|
4219
|
+
env: process.env,
|
|
4220
|
+
});
|
|
4221
|
+
return withAgentAdmission(sid, identity.agentId, () => runMessageActionCommandUnscoped(args));
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
async function runMessageActionCommandUnscoped({
|
|
3966
4225
|
sessionId,
|
|
3967
4226
|
actionType,
|
|
3968
4227
|
options,
|
|
@@ -4193,20 +4452,50 @@ export function registerSessionCommand(program) {
|
|
|
4193
4452
|
});
|
|
4194
4453
|
|
|
4195
4454
|
session
|
|
4196
|
-
.command("
|
|
4197
|
-
.description("
|
|
4198
|
-
.option("--agent <id>", "
|
|
4199
|
-
.option("--
|
|
4455
|
+
.command("edit <sessionId> <target> <message...>")
|
|
4456
|
+
.description("Edit your message by sequence or threaded reply UUID; preserves its audit history")
|
|
4457
|
+
.option("--agent <id>", "Authoring agent (defaults to the joined session agent)")
|
|
4458
|
+
.option("--expected-revision <n>", "Guard against concurrent edits; otherwise fetch the current revision once")
|
|
4459
|
+
.option("--idempotency-key <key>", "Reuse an explicit key when retrying the same edit")
|
|
4200
4460
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
4201
4461
|
.option("--json", "Emit machine-readable output")
|
|
4202
|
-
.action(async (sessionId,
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4462
|
+
.action(async (sessionId, target, messageParts, options, command) => {
|
|
4463
|
+
const sid = normalizeString(sessionId);
|
|
4464
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
4465
|
+
const text = Array.isArray(messageParts) ? messageParts.join(" ") : String(messageParts || "");
|
|
4466
|
+
const sequence = /^\d+$/.test(target) ? Number(target) : null;
|
|
4467
|
+
if (sequence !== null && (!Number.isSafeInteger(sequence) || sequence <= 0)) throw new Error("target sequence must be a positive safe integer.");
|
|
4468
|
+
if (!sequence && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(target)) {
|
|
4469
|
+
throw new Error("Edit target must be a positive message sequence or a threaded reply UUID.");
|
|
4470
|
+
}
|
|
4471
|
+
const identity = await resolveMessageActionIdentity({ sessionId: sid, optionAgent: options.agent, targetPath, env: process.env });
|
|
4472
|
+
if (shouldBlockImplicitCliUserSessionSay(identity)) throw new Error(identity.identityWarning || "session edit requires an authoring agent identity; pass --agent <id>.");
|
|
4473
|
+
const expectedRevision = options.expectedRevision === undefined ? null : Number(options.expectedRevision);
|
|
4474
|
+
if (expectedRevision !== null && (!Number.isSafeInteger(expectedRevision) || expectedRevision <= 0)) throw new Error("expected-revision must be a positive safe integer.");
|
|
4475
|
+
return withAgentAdmission(sid, identity.agentId, async () => {
|
|
4476
|
+
const result = await editSessionMessage(sid, {
|
|
4477
|
+
targetPath,
|
|
4478
|
+
targetSequenceId: sequence,
|
|
4479
|
+
targetActionId: sequence ? "" : target,
|
|
4480
|
+
text,
|
|
4481
|
+
agentId: identity.agentId,
|
|
4482
|
+
expectedRevision,
|
|
4483
|
+
idempotencyKey: options.idempotencyKey,
|
|
4484
|
+
timeoutMs: 15_000,
|
|
4485
|
+
});
|
|
4486
|
+
if (!result.ok) throw new Error(`Session edit failed (${result.reason || "unknown"}). No local message was changed.`);
|
|
4487
|
+
let localAppend = { appended: 0, upgraded: 0, failed: 0 };
|
|
4488
|
+
try {
|
|
4489
|
+
await ensureLocalSessionForRemoteCommand(sid, { targetPath });
|
|
4490
|
+
const current = result.reply ? replyRevisionEvent(sid, result.reply) : result.event;
|
|
4491
|
+
localAppend = await appendMissingRemoteEvents(sid, [current, result.notification].filter(Boolean), { targetPath });
|
|
4492
|
+
} catch {
|
|
4493
|
+
localAppend = { appended: 0, upgraded: 0, failed: 1 };
|
|
4494
|
+
}
|
|
4495
|
+
const payload = { command: "session edit", sessionId: sid, target, ...result, localAppend, localAppendComplete: localAppend.failed === 0 };
|
|
4496
|
+
if (shouldEmitJson(options, command)) console.log(JSON.stringify(payload, null, 2));
|
|
4497
|
+
else console.log(`${result.changed ? "Edited" : "Confirmed"} ${sequence ? `#${sequence}` : `reply ${target}`} at revision ${messageRevision(result.reply || result.event)}.${localAppend.failed ? " Remote edit succeeded; local cache needs sync." : ""}`);
|
|
4498
|
+
return payload;
|
|
4210
4499
|
});
|
|
4211
4500
|
});
|
|
4212
4501
|
|
|
@@ -4951,7 +5240,7 @@ export function registerSessionCommand(program) {
|
|
|
4951
5240
|
`Total log files to retain including the active file (default ${DEFAULT_ROTATING_LOG_MAX_FILES})`,
|
|
4952
5241
|
String(DEFAULT_ROTATING_LOG_MAX_FILES),
|
|
4953
5242
|
)
|
|
4954
|
-
.action(async (options) => {
|
|
5243
|
+
.action(asAdmittedAgent(sessionListenAgent, async (options) => {
|
|
4955
5244
|
const normalizedSessionId = resolveSessionIdOption(options);
|
|
4956
5245
|
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
4957
5246
|
const agentId = normalizeAgentId(options.agent, "cli-user");
|
|
@@ -5043,9 +5332,15 @@ export function registerSessionCommand(program) {
|
|
|
5043
5332
|
options.maxPolls === undefined
|
|
5044
5333
|
? null
|
|
5045
5334
|
: parsePositiveInteger(options.maxPolls, "max-polls", 1);
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5335
|
+
if (currentAdmittedAgent() && requestedTransport === "stream") {
|
|
5336
|
+
throw new Error(
|
|
5337
|
+
"An admitted agent listens by polling: the event stream is not part of an admission grant, so a revoke never has to chase an open stream. Use --transport poll.",
|
|
5338
|
+
);
|
|
5339
|
+
}
|
|
5340
|
+
const listenTransport =
|
|
5341
|
+
requestedTransport === "auto" && (maxPolls !== null || currentAdmittedAgent())
|
|
5342
|
+
? "poll"
|
|
5343
|
+
: requestedTransport;
|
|
5049
5344
|
const since = options.since === undefined ? undefined : String(options.since);
|
|
5050
5345
|
if (options.fromNow && options.since !== undefined) {
|
|
5051
5346
|
throw new Error("Use either --from-now or --since, not both.");
|
|
@@ -5367,7 +5662,7 @@ export function registerSessionCommand(program) {
|
|
|
5367
5662
|
}).catch(() => {});
|
|
5368
5663
|
}
|
|
5369
5664
|
}
|
|
5370
|
-
});
|
|
5665
|
+
}));
|
|
5371
5666
|
|
|
5372
5667
|
session
|
|
5373
5668
|
.command("daemon [sessionId]")
|
|
@@ -5939,7 +6234,7 @@ export function registerSessionCommand(program) {
|
|
|
5939
6234
|
.option("--include-control-events", "Include listener lifecycle/control-plane events in transcript output")
|
|
5940
6235
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
5941
6236
|
.option("--json", "Emit machine-readable output")
|
|
5942
|
-
.action(async (sessionId, options, command) => {
|
|
6237
|
+
.action(asAdmittedAgent(sessionReadAgent, async (sessionId, options, command) => {
|
|
5943
6238
|
const normalizedSessionId = normalizeString(sessionId);
|
|
5944
6239
|
if (!normalizedSessionId) {
|
|
5945
6240
|
throw new Error("session id is required.");
|
|
@@ -6283,7 +6578,7 @@ export function registerSessionCommand(program) {
|
|
|
6283
6578
|
console.log(formatEventLine(event));
|
|
6284
6579
|
}
|
|
6285
6580
|
}
|
|
6286
|
-
});
|
|
6581
|
+
}));
|
|
6287
6582
|
|
|
6288
6583
|
session
|
|
6289
6584
|
.command("search <sessionId> <query>")
|
|
@@ -6492,6 +6787,147 @@ export function registerSessionCommand(program) {
|
|
|
6492
6787
|
}
|
|
6493
6788
|
});
|
|
6494
6789
|
|
|
6790
|
+
// ---------------------------------------------------------------- tickets
|
|
6791
|
+
// The room's shared backlog (API "Ticket Lease Contract (T2)"). Every subcommand is
|
|
6792
|
+
// an ACTOR command: the runCli choke point runs it on the agent's admission.
|
|
6793
|
+
const ticket = session
|
|
6794
|
+
.command("ticket")
|
|
6795
|
+
.description("The room's shared backlog: list, claim, renew, release and submit tickets");
|
|
6796
|
+
|
|
6797
|
+
// Ticket work is attributed by the CREDENTIAL, never by a label: the lease routes take the
|
|
6798
|
+
// holder from authentication alone. So an agent identity is allowed only with that agent's
|
|
6799
|
+
// live admission in scope; without it, the request would go out on the human token and
|
|
6800
|
+
// claim as the PERSON while this command reported the agent. Refused before any request.
|
|
6801
|
+
async function ticketIdentity(sessionId, options, targetPath) {
|
|
6802
|
+
const identity = await resolveSessionSayIdentity({ sessionId, agentId: options.agent, targetPath });
|
|
6803
|
+
const agent = normalizeString(identity.agentId);
|
|
6804
|
+
if (!agent || agent === "cli-user") return "human";
|
|
6805
|
+
const admitted = currentAdmittedAgent();
|
|
6806
|
+
if (!admitted || admitted.sessionId !== sessionId || admitted.agentId !== agent) {
|
|
6807
|
+
throw new Error(
|
|
6808
|
+
`Agent "${agent}" has no live admission in this room. Ticket work is attributed by the credential, so ` +
|
|
6809
|
+
`without one this would act as you, not as "${agent}". Admit it first: ` +
|
|
6810
|
+
`sl session join ${sessionId} --agent ${agent} --goal "<what it is here to do>"`
|
|
6811
|
+
);
|
|
6812
|
+
}
|
|
6813
|
+
return agent;
|
|
6814
|
+
}
|
|
6815
|
+
|
|
6816
|
+
function ticketOutput(options, command, payload, line) {
|
|
6817
|
+
if (shouldEmitJson(options, command)) {
|
|
6818
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
6819
|
+
return;
|
|
6820
|
+
}
|
|
6821
|
+
console.log(line);
|
|
6822
|
+
}
|
|
6823
|
+
|
|
6824
|
+
ticket
|
|
6825
|
+
.command("list <sessionId>")
|
|
6826
|
+
.description("List the room's tickets (--available: only those claimable now)")
|
|
6827
|
+
.option("--available", "Only tickets that can be claimed now (never blocked work)")
|
|
6828
|
+
.option("--agent <id>", "Agent id reading the backlog (defaults to the joined session agent)")
|
|
6829
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
6830
|
+
.option("--json", "Emit machine-readable output")
|
|
6831
|
+
.action(async (sessionId, options, command) => {
|
|
6832
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
6833
|
+
const result = await listTickets(normalizeString(sessionId), { targetPath, available: Boolean(options.available) });
|
|
6834
|
+
const lines = result.items.map((t) => {
|
|
6835
|
+
const holder = t.lease ? ` · ${t.lease.live ? "leased by" : "lease EXPIRED, was"} ${t.lease.holder}` : "";
|
|
6836
|
+
const done = t.completion && !t.completion.verified ? " (manual, unverified)" : "";
|
|
6837
|
+
return `${t.id} ${t.status}${done} v${t.version} ${t.title}${holder}`;
|
|
6838
|
+
});
|
|
6839
|
+
ticketOutput(options, command, { command: "session ticket list", ...result }, lines.join("\n") || "No tickets.");
|
|
6840
|
+
});
|
|
6841
|
+
|
|
6842
|
+
ticket
|
|
6843
|
+
.command("claim <sessionId> <ticketId>")
|
|
6844
|
+
.description("Claim a ticket: take its fenced lease")
|
|
6845
|
+
.option("--expected-version <n>", "Refuse if the ticket has changed since this version")
|
|
6846
|
+
.option("--agent <id>", "Agent id claiming the ticket (defaults to the joined session agent)")
|
|
6847
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
6848
|
+
.option("--json", "Emit machine-readable output")
|
|
6849
|
+
.action(async (sessionId, ticketId, options, command) => {
|
|
6850
|
+
const sid = normalizeString(sessionId);
|
|
6851
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
6852
|
+
const identity = await ticketIdentity(sid, options, targetPath);
|
|
6853
|
+
const expectedVersion =
|
|
6854
|
+
options.expectedVersion === undefined ? null : parsePositiveInteger(options.expectedVersion, "expected-version", 1);
|
|
6855
|
+
const result = await claimTicket(sid, normalizeString(ticketId), { identity, expectedVersion, targetPath });
|
|
6856
|
+
ticketOutput(options, command, { command: "session ticket claim", identity, ...result },
|
|
6857
|
+
`Claimed ${ticketId} (fence ${result.lease.fence}, until ${result.lease.expiresAt}).`);
|
|
6858
|
+
});
|
|
6859
|
+
|
|
6860
|
+
ticket
|
|
6861
|
+
.command("renew <sessionId> <ticketId>")
|
|
6862
|
+
.description("Renew the lease this identity holds on a ticket")
|
|
6863
|
+
.option("--agent <id>", "Agent id holding the lease (defaults to the joined session agent)")
|
|
6864
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
6865
|
+
.option("--json", "Emit machine-readable output")
|
|
6866
|
+
.action(async (sessionId, ticketId, options, command) => {
|
|
6867
|
+
const sid = normalizeString(sessionId);
|
|
6868
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
6869
|
+
const identity = await ticketIdentity(sid, options, targetPath);
|
|
6870
|
+
const result = await renewTicketLease(sid, normalizeString(ticketId), { identity, targetPath });
|
|
6871
|
+
ticketOutput(options, command, { command: "session ticket renew", identity, ...result },
|
|
6872
|
+
`Renewed ${ticketId} until ${result.lease.expiresAt}.`);
|
|
6873
|
+
});
|
|
6874
|
+
|
|
6875
|
+
ticket
|
|
6876
|
+
.command("release <sessionId> <ticketId>")
|
|
6877
|
+
.description("Give a ticket back: it returns to open for the next claimant")
|
|
6878
|
+
.option("--reason <text>", "Why it is being released")
|
|
6879
|
+
.option("--agent <id>", "Agent id holding the lease (defaults to the joined session agent)")
|
|
6880
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
6881
|
+
.option("--json", "Emit machine-readable output")
|
|
6882
|
+
.action(async (sessionId, ticketId, options, command) => {
|
|
6883
|
+
const sid = normalizeString(sessionId);
|
|
6884
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
6885
|
+
const identity = await ticketIdentity(sid, options, targetPath);
|
|
6886
|
+
const result = await releaseTicketLease(sid, normalizeString(ticketId), {
|
|
6887
|
+
identity, targetPath, reason: normalizeString(options.reason) || null,
|
|
6888
|
+
});
|
|
6889
|
+
ticketOutput(options, command, { command: "session ticket release", identity, ...result }, `Released ${ticketId}.`);
|
|
6890
|
+
});
|
|
6891
|
+
|
|
6892
|
+
ticket
|
|
6893
|
+
.command("submit <sessionId> <ticketId>")
|
|
6894
|
+
.description("Submit the work for review (in_review). Completion needs verification; this is not done.")
|
|
6895
|
+
.option("--sha <sha>", "The implementation commit")
|
|
6896
|
+
.option("--evidence-file <path>", "JSON array of re-checkable evidence claims")
|
|
6897
|
+
.option("--agent <id>", "Agent id holding the lease (defaults to the joined session agent)")
|
|
6898
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
6899
|
+
.option("--json", "Emit machine-readable output")
|
|
6900
|
+
.action(async (sessionId, ticketId, options, command) => {
|
|
6901
|
+
const sid = normalizeString(sessionId);
|
|
6902
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
6903
|
+
const identity = await ticketIdentity(sid, options, targetPath);
|
|
6904
|
+
let evidence = [];
|
|
6905
|
+
if (options.evidenceFile) {
|
|
6906
|
+
evidence = JSON.parse(await fsp.readFile(path.resolve(targetPath, String(options.evidenceFile)), "utf8"));
|
|
6907
|
+
if (!Array.isArray(evidence)) throw new Error("--evidence-file must contain a JSON array.");
|
|
6908
|
+
}
|
|
6909
|
+
const result = await submitTicket(sid, normalizeString(ticketId), {
|
|
6910
|
+
identity, targetPath, implementationSha: normalizeString(options.sha) || null, evidence,
|
|
6911
|
+
});
|
|
6912
|
+
ticketOutput(options, command, { command: "session ticket submit", identity, ...result },
|
|
6913
|
+
`Submitted ${ticketId} for review.`);
|
|
6914
|
+
});
|
|
6915
|
+
|
|
6916
|
+
ticket
|
|
6917
|
+
.command("events <sessionId>")
|
|
6918
|
+
.description("The room's ticket events after a cursor, in commit order")
|
|
6919
|
+
.option("--after <cursor>", "Only events after this cursor", "0")
|
|
6920
|
+
.option("--agent <id>", "Agent id reading the feed (defaults to the joined session agent)")
|
|
6921
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
6922
|
+
.option("--json", "Emit machine-readable output")
|
|
6923
|
+
.action(async (sessionId, options, command) => {
|
|
6924
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
6925
|
+
const after = Number.parseInt(String(options.after ?? "0"), 10) || 0;
|
|
6926
|
+
const result = await ticketEvents(normalizeString(sessionId), { after, targetPath });
|
|
6927
|
+
const lines = (result.events || []).map((e) => `#${e.cursor} ${e.kind} ${e.ticketId} v${e.ticketVersion} by ${e.actorId}`);
|
|
6928
|
+
ticketOutput(options, command, { command: "session ticket events", ...result }, lines.join("\n") || "No new ticket events.");
|
|
6929
|
+
});
|
|
6930
|
+
|
|
6495
6931
|
const checkpoint = session
|
|
6496
6932
|
.command("checkpoint")
|
|
6497
6933
|
.description("List, show, create, and generate durable session checkpoints");
|