@sublang/playbook 7.0.0 → 9.0.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 +20 -7
- package/docs/cli.md +88 -43
- package/docs/configuration.md +221 -119
- package/docs/embedding.md +78 -27
- package/package.json +4 -3
- package/reference/sdlc/captain.playbook/captain.playbook.js +16 -5
- package/reference/sdlc/captain.playbook/captain.playbook.ts +20 -6
- package/reference/sdlc/code.md +1 -1
- package/reference/sdlc/code.playbook/bin/interactive-session.js +816 -0
- package/reference/sdlc/code.playbook/bin/launch-config.js +1078 -116
- package/reference/sdlc/code.playbook/bin/playbook.js +489 -34
- package/reference/sdlc/code.playbook/bin/run.js +283 -298
- package/reference/sdlc/code.playbook/bin/session-store.js +818 -26
- package/reference/sdlc/code.playbook/code.fsm.d.ts +9 -6
- package/reference/sdlc/code.playbook/code.fsm.introspect.js +2 -2
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +2 -2
- package/reference/sdlc/code.playbook/code.fsm.js +18 -15
- package/reference/sdlc/code.playbook/code.fsm.ts +21 -21
- package/reference/sdlc/code.playbook/code.gears.md +1 -1
- package/reference/sdlc/code.playbook/code.playbook.d.ts +2 -1
- package/reference/sdlc/code.playbook/code.playbook.js +25 -15
- package/reference/sdlc/code.playbook/code.playbook.ts +34 -17
- package/reference/sdlc/code.playbook/code.registry.d.ts +5 -13
- package/reference/sdlc/code.playbook/code.registry.js +3 -10
- package/reference/sdlc/code.playbook/code.registry.ts +7 -32
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +39 -14
- package/reference/sdlc/code.playbook/playbook-captain.js +1014 -299
- package/reference/sdlc/code.playbook/playbook-captain.ts +1450 -406
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +41 -49
- package/reference/sdlc/decide.md +4 -4
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +10 -10
- package/reference/sdlc/decide.playbook/decide.fsm.js +21 -14
- package/reference/sdlc/decide.playbook/decide.fsm.ts +27 -23
- package/reference/sdlc/decide.playbook/decide.gears.md +3 -5
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -13
- package/reference/sdlc/decide.playbook/decide.playbook.js +465 -246
- package/reference/sdlc/decide.playbook/decide.playbook.ts +623 -283
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +5 -13
- package/reference/sdlc/decide.playbook/decide.registry.js +3 -9
- package/reference/sdlc/decide.playbook/decide.registry.ts +7 -31
- package/reference/sdlc/review.md +4 -5
- package/reference/sdlc/review.playbook/review.fsm.d.ts +9 -11
- package/reference/sdlc/review.playbook/review.fsm.js +30 -24
- package/reference/sdlc/review.playbook/review.fsm.ts +39 -35
- package/reference/sdlc/review.playbook/review.gears.md +6 -5
- package/reference/sdlc/review.playbook/review.playbook.d.ts +2 -1
- package/reference/sdlc/review.playbook/review.playbook.js +29 -23
- package/reference/sdlc/review.playbook/review.playbook.ts +38 -28
- package/reference/sdlc/review.playbook/review.registry.d.ts +5 -13
- package/reference/sdlc/review.playbook/review.registry.js +3 -16
- package/reference/sdlc/review.playbook/review.registry.ts +7 -38
- package/slc/gears2fsm.md +45 -24
- package/slc/link.md +297 -135
- package/slc/text2gears.md +19 -18
- package/src/runtime.d.ts +21 -16
- package/src/runtime.ts +20 -23
- package/src/xstate-playbook-runtime.d.ts +34 -20
- package/src/xstate-playbook-runtime.js +973 -400
- package/src/xstate-playbook-runtime.ts +1203 -457
- package/src/xstate-runtime.d.ts +17 -7
- package/src/xstate-runtime.js +198 -81
- package/src/xstate-runtime.ts +339 -112
|
@@ -19,12 +19,24 @@ import {
|
|
|
19
19
|
} from 'node:fs/promises';
|
|
20
20
|
import { homedir, hostname as systemHostname } from 'node:os';
|
|
21
21
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
22
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
23
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
24
|
+
import { assertSupportedEffort } from '@sublang/cligent';
|
|
25
|
+
import { KNOWN_PLAYER_ADAPTERS } from '@sublang/cligent/tmux-play';
|
|
22
26
|
import { snapshotJsonValue } from '../../../../src/xstate-runtime.js';
|
|
27
|
+
import { assertPlaybookCaptainShellSnapshot } from '../playbook-captain.js';
|
|
23
28
|
|
|
24
|
-
export const CAPTAIN_SESSION_RECORD_SCHEMA_VERSION =
|
|
29
|
+
export const CAPTAIN_SESSION_RECORD_SCHEMA_VERSION = 3;
|
|
25
30
|
export const CAPTAIN_SESSION_RECORD_KIND = 'captain-session';
|
|
26
31
|
export const SESSION_ID_PATTERN =
|
|
27
32
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
33
|
+
export const CAPTAIN_SESSION_STRUCTURAL_PROJECTION_SCHEMA_VERSION = 1;
|
|
34
|
+
export const CAPTAIN_SESSION_EXECUTION_PROJECTION_SCHEMA_VERSION = 2;
|
|
35
|
+
|
|
36
|
+
const PLAYER_ID_PATTERN = /^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)*$/;
|
|
37
|
+
const ROLE_ID_PATTERN = /^[a-z][a-z0-9_-]*$/;
|
|
38
|
+
const RESERVED_ID = 'captain';
|
|
39
|
+
const KNOWN_ADAPTERS = new Set(KNOWN_PLAYER_ADAPTERS);
|
|
28
40
|
|
|
29
41
|
const COMMON_RECORD_KEYS = [
|
|
30
42
|
'schemaVersion',
|
|
@@ -34,7 +46,8 @@ const COMMON_RECORD_KEYS = [
|
|
|
34
46
|
'createdAt',
|
|
35
47
|
'updatedAt',
|
|
36
48
|
'cwd',
|
|
37
|
-
'
|
|
49
|
+
'structuralProjection',
|
|
50
|
+
'lastAppliedExecutionProjection',
|
|
38
51
|
'snapshot',
|
|
39
52
|
];
|
|
40
53
|
const UNCERTAIN_KEYS = [
|
|
@@ -43,6 +56,25 @@ const UNCERTAIN_KEYS = [
|
|
|
43
56
|
'attemptId',
|
|
44
57
|
'attemptNumber',
|
|
45
58
|
'markedAt',
|
|
59
|
+
'attemptedExecutionProjection',
|
|
60
|
+
];
|
|
61
|
+
const RELEASED_SCHEMA_2_COMMON_RECORD_KEYS = [
|
|
62
|
+
'schemaVersion',
|
|
63
|
+
'kind',
|
|
64
|
+
'state',
|
|
65
|
+
'sessionId',
|
|
66
|
+
'createdAt',
|
|
67
|
+
'updatedAt',
|
|
68
|
+
'cwd',
|
|
69
|
+
'config',
|
|
70
|
+
'snapshot',
|
|
71
|
+
];
|
|
72
|
+
const RELEASED_SCHEMA_2_UNCERTAIN_KEYS = [
|
|
73
|
+
'baseUpdatedAt',
|
|
74
|
+
'input',
|
|
75
|
+
'attemptId',
|
|
76
|
+
'attemptNumber',
|
|
77
|
+
'markedAt',
|
|
46
78
|
];
|
|
47
79
|
const LEASE_SCHEMA_VERSION = 1;
|
|
48
80
|
const LEASE_KIND = 'captain-session-lease';
|
|
@@ -68,6 +100,15 @@ const DEFAULT_FS_OPERATIONS = Object.freeze({
|
|
|
68
100
|
unlink,
|
|
69
101
|
});
|
|
70
102
|
|
|
103
|
+
class CaptainSessionRecordSchemaError extends Error {
|
|
104
|
+
constructor(schemaVersion, message, cause) {
|
|
105
|
+
super(message);
|
|
106
|
+
this.name = 'CaptainSessionRecordSchemaError';
|
|
107
|
+
this.schemaVersion = schemaVersion;
|
|
108
|
+
this.cause = cause;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
71
112
|
export function defaultCaptainSessionsDir(
|
|
72
113
|
env = process.env,
|
|
73
114
|
home = env.HOME ?? homedir(),
|
|
@@ -130,11 +171,11 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
130
171
|
if (cause?.code === 'ENOENT' && missing === 'undefined') return undefined;
|
|
131
172
|
if (cause?.code === 'ENOENT') {
|
|
132
173
|
throw new Error(
|
|
133
|
-
`Captain session ${JSON.stringify(sessionId)} does not exist`,
|
|
174
|
+
`Captain session ${JSON.stringify(sessionId)} at ${JSON.stringify(path)} does not exist`,
|
|
134
175
|
);
|
|
135
176
|
}
|
|
136
177
|
throw new Error(
|
|
137
|
-
`cannot read Captain session ${JSON.stringify(sessionId)}: ${errorMessage(cause)}`,
|
|
178
|
+
`cannot read Captain session ${JSON.stringify(sessionId)} at ${JSON.stringify(path)}: ${errorMessage(cause)}`,
|
|
138
179
|
);
|
|
139
180
|
}
|
|
140
181
|
let value;
|
|
@@ -142,13 +183,36 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
142
183
|
value = JSON.parse(text);
|
|
143
184
|
} catch (cause) {
|
|
144
185
|
throw new Error(
|
|
145
|
-
`Captain session ${JSON.stringify(sessionId)} is not valid JSON: ${errorMessage(cause)}`,
|
|
186
|
+
`Captain session ${JSON.stringify(sessionId)} at ${JSON.stringify(path)} is not valid JSON: ${errorMessage(cause)}`,
|
|
146
187
|
);
|
|
147
188
|
}
|
|
148
|
-
|
|
189
|
+
let record;
|
|
190
|
+
try {
|
|
191
|
+
record = validateCaptainSessionRecord(value);
|
|
192
|
+
} catch (cause) {
|
|
193
|
+
const context =
|
|
194
|
+
`Captain session ${JSON.stringify(sessionId)} at ` +
|
|
195
|
+
`${JSON.stringify(path)}`;
|
|
196
|
+
if (cause instanceof CaptainSessionRecordSchemaError) {
|
|
197
|
+
if (cause.schemaVersion === 2 && value.sessionId !== sessionId) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`Captain session file ${JSON.stringify(path)} contains record ` +
|
|
200
|
+
JSON.stringify(value.sessionId),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
throw new CaptainSessionRecordSchemaError(
|
|
204
|
+
cause.schemaVersion,
|
|
205
|
+
`${context}: ${cause.message}`,
|
|
206
|
+
cause,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
throw new Error(`${context} is invalid: ${errorMessage(cause)}`, {
|
|
210
|
+
cause,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
149
213
|
if (record.sessionId !== sessionId) {
|
|
150
214
|
throw new Error(
|
|
151
|
-
`Captain session file ${JSON.stringify(
|
|
215
|
+
`Captain session file ${JSON.stringify(path)} contains record ` +
|
|
152
216
|
JSON.stringify(record.sessionId),
|
|
153
217
|
);
|
|
154
218
|
}
|
|
@@ -157,7 +221,15 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
157
221
|
|
|
158
222
|
const read = (sessionId) => readRecord(sessionId);
|
|
159
223
|
|
|
160
|
-
const latest = async () => {
|
|
224
|
+
const latest = async ({ onLegacyRecord } = {}) => {
|
|
225
|
+
if (
|
|
226
|
+
onLegacyRecord !== undefined &&
|
|
227
|
+
typeof onLegacyRecord !== 'function'
|
|
228
|
+
) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
'Captain session legacy-record observer must be a function',
|
|
231
|
+
);
|
|
232
|
+
}
|
|
161
233
|
let names;
|
|
162
234
|
try {
|
|
163
235
|
await assertPrivateDirectory(sessionsDir, fs);
|
|
@@ -175,7 +247,24 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
175
247
|
if (!SESSION_ID_PATTERN.test(sessionId)) continue;
|
|
176
248
|
// Canonically named records are store-owned. Corruption must not make
|
|
177
249
|
// --continue silently select an older logical session.
|
|
178
|
-
|
|
250
|
+
try {
|
|
251
|
+
candidates.push(await readRecord(sessionId));
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (
|
|
254
|
+
error instanceof CaptainSessionRecordSchemaError &&
|
|
255
|
+
error.schemaVersion === 2
|
|
256
|
+
) {
|
|
257
|
+
await onLegacyRecord?.(
|
|
258
|
+
Object.freeze({
|
|
259
|
+
sessionId,
|
|
260
|
+
path: recordPathFor(sessionId),
|
|
261
|
+
schemaVersion: 2,
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
179
268
|
}
|
|
180
269
|
candidates.sort((left, right) => {
|
|
181
270
|
const byUpdated = Date.parse(right.updatedAt) - Date.parse(left.updatedAt);
|
|
@@ -647,10 +736,55 @@ function createLease({
|
|
|
647
736
|
return record;
|
|
648
737
|
});
|
|
649
738
|
|
|
650
|
-
const
|
|
739
|
+
const initializeSettled = ({
|
|
740
|
+
cwd,
|
|
741
|
+
structuralProjection,
|
|
742
|
+
executionProjection,
|
|
743
|
+
snapshot,
|
|
744
|
+
} = {}) =>
|
|
745
|
+
runExclusive(async () => {
|
|
746
|
+
const initial = validateFreshBoundary({
|
|
747
|
+
cwd,
|
|
748
|
+
structuralProjection,
|
|
749
|
+
snapshot,
|
|
750
|
+
});
|
|
751
|
+
const applied = assertCaptainSessionExecutionCompatible(
|
|
752
|
+
initial.structuralProjection,
|
|
753
|
+
executionProjection,
|
|
754
|
+
);
|
|
755
|
+
const createdAt = timestampFrom(now(), 'session timestamp');
|
|
756
|
+
const updatedAt = nextTimestamp(now(), createdAt);
|
|
757
|
+
const record = validateCaptainSessionRecord({
|
|
758
|
+
schemaVersion: CAPTAIN_SESSION_RECORD_SCHEMA_VERSION,
|
|
759
|
+
kind: CAPTAIN_SESSION_RECORD_KIND,
|
|
760
|
+
state: 'settled',
|
|
761
|
+
sessionId,
|
|
762
|
+
createdAt,
|
|
763
|
+
updatedAt,
|
|
764
|
+
cwd: initial.cwd,
|
|
765
|
+
structuralProjection: initial.structuralProjection,
|
|
766
|
+
lastAppliedExecutionProjection: applied,
|
|
767
|
+
snapshot: initial.snapshot,
|
|
768
|
+
});
|
|
769
|
+
await assertOwnerUnchecked();
|
|
770
|
+
await writeRecord(record, { noReplace: true });
|
|
771
|
+
await assertOwnerUnchecked();
|
|
772
|
+
return record;
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
const beginTurn = ({
|
|
776
|
+
input,
|
|
777
|
+
attemptId,
|
|
778
|
+
attemptedExecutionProjection,
|
|
779
|
+
fresh,
|
|
780
|
+
} = {}) =>
|
|
651
781
|
runExclusive(async () => {
|
|
652
782
|
assertAcceptedInput(input);
|
|
653
783
|
assertUuid(attemptId, 'Captain session attempt id');
|
|
784
|
+
const attempted = validateCaptainSessionExecutionProjection(
|
|
785
|
+
attemptedExecutionProjection,
|
|
786
|
+
'Captain session attempted execution projection',
|
|
787
|
+
);
|
|
654
788
|
await assertOwnerUnchecked();
|
|
655
789
|
const prior = await readRecord(sessionId, { missing: 'undefined' });
|
|
656
790
|
let record;
|
|
@@ -668,7 +802,8 @@ function createLease({
|
|
|
668
802
|
createdAt: timestamp,
|
|
669
803
|
updatedAt: timestamp,
|
|
670
804
|
cwd: initial.cwd,
|
|
671
|
-
|
|
805
|
+
structuralProjection: initial.structuralProjection,
|
|
806
|
+
lastAppliedExecutionProjection: attempted,
|
|
672
807
|
snapshot: initial.snapshot,
|
|
673
808
|
uncertain: {
|
|
674
809
|
baseUpdatedAt: null,
|
|
@@ -676,6 +811,7 @@ function createLease({
|
|
|
676
811
|
attemptId,
|
|
677
812
|
attemptNumber: 1,
|
|
678
813
|
markedAt: timestamp,
|
|
814
|
+
attemptedExecutionProjection: attempted,
|
|
679
815
|
},
|
|
680
816
|
});
|
|
681
817
|
await assertOwnerUnchecked();
|
|
@@ -696,7 +832,9 @@ function createLease({
|
|
|
696
832
|
createdAt: prior.createdAt,
|
|
697
833
|
updatedAt: timestamp,
|
|
698
834
|
cwd: prior.cwd,
|
|
699
|
-
|
|
835
|
+
structuralProjection: prior.structuralProjection,
|
|
836
|
+
lastAppliedExecutionProjection:
|
|
837
|
+
prior.lastAppliedExecutionProjection,
|
|
700
838
|
snapshot: prior.snapshot,
|
|
701
839
|
uncertain: {
|
|
702
840
|
baseUpdatedAt: prior.updatedAt,
|
|
@@ -704,6 +842,7 @@ function createLease({
|
|
|
704
842
|
attemptId,
|
|
705
843
|
attemptNumber: 1,
|
|
706
844
|
markedAt: timestamp,
|
|
845
|
+
attemptedExecutionProjection: attempted,
|
|
707
846
|
},
|
|
708
847
|
});
|
|
709
848
|
await assertOwnerUnchecked();
|
|
@@ -737,7 +876,9 @@ function createLease({
|
|
|
737
876
|
createdAt: prior.createdAt,
|
|
738
877
|
updatedAt: timestamp,
|
|
739
878
|
cwd: prior.cwd,
|
|
740
|
-
|
|
879
|
+
structuralProjection: prior.structuralProjection,
|
|
880
|
+
lastAppliedExecutionProjection:
|
|
881
|
+
prior.lastAppliedExecutionProjection,
|
|
741
882
|
snapshot: prior.snapshot,
|
|
742
883
|
uncertain: {
|
|
743
884
|
baseUpdatedAt: prior.uncertain.baseUpdatedAt,
|
|
@@ -745,6 +886,8 @@ function createLease({
|
|
|
745
886
|
attemptId: nextAttemptId,
|
|
746
887
|
attemptNumber: prior.uncertain.attemptNumber + 1,
|
|
747
888
|
markedAt: timestamp,
|
|
889
|
+
attemptedExecutionProjection:
|
|
890
|
+
prior.uncertain.attemptedExecutionProjection,
|
|
748
891
|
},
|
|
749
892
|
});
|
|
750
893
|
await assertOwnerUnchecked();
|
|
@@ -770,7 +913,9 @@ function createLease({
|
|
|
770
913
|
createdAt: prior.createdAt,
|
|
771
914
|
updatedAt: timestamp,
|
|
772
915
|
cwd: prior.cwd,
|
|
773
|
-
|
|
916
|
+
structuralProjection: prior.structuralProjection,
|
|
917
|
+
lastAppliedExecutionProjection:
|
|
918
|
+
prior.uncertain.attemptedExecutionProjection,
|
|
774
919
|
snapshot,
|
|
775
920
|
});
|
|
776
921
|
await assertOwnerUnchecked();
|
|
@@ -803,7 +948,9 @@ function createLease({
|
|
|
803
948
|
createdAt: prior.createdAt,
|
|
804
949
|
updatedAt: prior.uncertain.baseUpdatedAt,
|
|
805
950
|
cwd: prior.cwd,
|
|
806
|
-
|
|
951
|
+
structuralProjection: prior.structuralProjection,
|
|
952
|
+
lastAppliedExecutionProjection:
|
|
953
|
+
prior.lastAppliedExecutionProjection,
|
|
807
954
|
snapshot: prior.snapshot,
|
|
808
955
|
});
|
|
809
956
|
await writeRecord(record, { noReplace: false });
|
|
@@ -822,6 +969,7 @@ function createLease({
|
|
|
822
969
|
sessionId,
|
|
823
970
|
ownerToken: owner.ownerToken,
|
|
824
971
|
read,
|
|
972
|
+
initializeSettled,
|
|
825
973
|
beginTurn,
|
|
826
974
|
beginRetry,
|
|
827
975
|
settle,
|
|
@@ -831,11 +979,115 @@ function createLease({
|
|
|
831
979
|
});
|
|
832
980
|
}
|
|
833
981
|
|
|
982
|
+
export function validateCaptainSessionExecutionProjection(
|
|
983
|
+
value,
|
|
984
|
+
path = 'Captain session execution projection',
|
|
985
|
+
) {
|
|
986
|
+
const projection = requireRecord(snapshotJsonValue(value, path), path);
|
|
987
|
+
validateCaptainSessionProjection(projection, { path, structural: false });
|
|
988
|
+
return projection;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
export function validateCaptainSessionStructuralProjection(
|
|
992
|
+
value,
|
|
993
|
+
path = 'Captain session structural projection',
|
|
994
|
+
) {
|
|
995
|
+
const projection = requireRecord(snapshotJsonValue(value, path), path);
|
|
996
|
+
validateCaptainSessionProjection(projection, { path, structural: true });
|
|
997
|
+
return projection;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
export function projectCaptainSessionStructure(value) {
|
|
1001
|
+
const execution = validateCaptainSessionExecutionProjection(value);
|
|
1002
|
+
const fixedAgent = (agent) => ({
|
|
1003
|
+
adapter: agent.adapter,
|
|
1004
|
+
...(agent.instruction === undefined
|
|
1005
|
+
? {}
|
|
1006
|
+
: { instruction: agent.instruction }),
|
|
1007
|
+
...(agent.permissions === undefined
|
|
1008
|
+
? {}
|
|
1009
|
+
: { permissions: agent.permissions }),
|
|
1010
|
+
});
|
|
1011
|
+
return validateCaptainSessionStructuralProjection({
|
|
1012
|
+
schemaVersion: CAPTAIN_SESSION_STRUCTURAL_PROJECTION_SCHEMA_VERSION,
|
|
1013
|
+
captain: fixedAgent(execution.captain),
|
|
1014
|
+
players: execution.players.map(({ id, ...agent }) => ({
|
|
1015
|
+
id,
|
|
1016
|
+
...fixedAgent(agent),
|
|
1017
|
+
})),
|
|
1018
|
+
catalog: Object.fromEntries(
|
|
1019
|
+
Object.entries(execution.catalog).map(([id, item]) => [
|
|
1020
|
+
id,
|
|
1021
|
+
{
|
|
1022
|
+
id: item.id,
|
|
1023
|
+
from: item.from,
|
|
1024
|
+
manifestCommand: item.manifestCommand,
|
|
1025
|
+
command: item.command,
|
|
1026
|
+
intent: item.intent,
|
|
1027
|
+
artifactSchema: item.artifactSchema,
|
|
1028
|
+
requiredRoleIds: item.requiredRoleIds,
|
|
1029
|
+
concurrentRoleSets: item.concurrentRoleSets,
|
|
1030
|
+
roles: Object.fromEntries(
|
|
1031
|
+
Object.entries(item.roles).map(([roleId, binding]) => [
|
|
1032
|
+
roleId,
|
|
1033
|
+
{ playerId: binding.playerId },
|
|
1034
|
+
]),
|
|
1035
|
+
),
|
|
1036
|
+
options: item.options,
|
|
1037
|
+
},
|
|
1038
|
+
]),
|
|
1039
|
+
),
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
export function assertCaptainSessionExecutionCompatible(
|
|
1044
|
+
structuralProjection,
|
|
1045
|
+
executionProjection,
|
|
1046
|
+
) {
|
|
1047
|
+
const structural = validateCaptainSessionStructuralProjection(
|
|
1048
|
+
structuralProjection,
|
|
1049
|
+
);
|
|
1050
|
+
const execution = validateCaptainSessionExecutionProjection(
|
|
1051
|
+
executionProjection,
|
|
1052
|
+
);
|
|
1053
|
+
const projected = projectCaptainSessionStructure(execution);
|
|
1054
|
+
if (!isDeepStrictEqual(projected, structural)) {
|
|
1055
|
+
throw new Error(
|
|
1056
|
+
'Captain session execution projection does not reproduce the stored structural projection',
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
return execution;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
export function captainSessionSelectedMembers(value) {
|
|
1063
|
+
const structural = validateCaptainSessionStructuralProjection(value);
|
|
1064
|
+
return snapshotJsonValue(
|
|
1065
|
+
{
|
|
1066
|
+
playbookIds: Object.keys(structural.catalog),
|
|
1067
|
+
playerIds: referencedPlayerIds(structural.catalog),
|
|
1068
|
+
},
|
|
1069
|
+
'Captain session selected members',
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
834
1073
|
export function validateCaptainSessionRecord(value) {
|
|
835
1074
|
const record = requireRecord(
|
|
836
1075
|
snapshotJsonValue(value, 'Captain session record'),
|
|
837
1076
|
'Captain session record',
|
|
838
1077
|
);
|
|
1078
|
+
if (record.schemaVersion !== CAPTAIN_SESSION_RECORD_SCHEMA_VERSION) {
|
|
1079
|
+
if (record.schemaVersion === 2) {
|
|
1080
|
+
assertReleasedSchema2CaptainSessionRecord(record);
|
|
1081
|
+
throw new CaptainSessionRecordSchemaError(
|
|
1082
|
+
record.schemaVersion,
|
|
1083
|
+
'Captain session record schema 2 has incompatible root-owned player identity; schema 3 is required',
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
throw new CaptainSessionRecordSchemaError(
|
|
1087
|
+
record.schemaVersion,
|
|
1088
|
+
`Captain session record schema ${JSON.stringify(record.schemaVersion)} is not supported`,
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
839
1091
|
if (record.state !== 'settled' && record.state !== 'uncertain') {
|
|
840
1092
|
throw new Error('Captain session record state is not supported');
|
|
841
1093
|
}
|
|
@@ -846,11 +1098,6 @@ export function validateCaptainSessionRecord(value) {
|
|
|
846
1098
|
: COMMON_RECORD_KEYS,
|
|
847
1099
|
'Captain session record',
|
|
848
1100
|
);
|
|
849
|
-
if (record.schemaVersion !== CAPTAIN_SESSION_RECORD_SCHEMA_VERSION) {
|
|
850
|
-
throw new Error(
|
|
851
|
-
`Captain session record schema ${JSON.stringify(record.schemaVersion)} is not supported`,
|
|
852
|
-
);
|
|
853
|
-
}
|
|
854
1101
|
if (record.kind !== CAPTAIN_SESSION_RECORD_KIND) {
|
|
855
1102
|
throw new Error('Captain session record kind is not supported');
|
|
856
1103
|
}
|
|
@@ -871,8 +1118,25 @@ export function validateCaptainSessionRecord(value) {
|
|
|
871
1118
|
if (resolve(record.cwd) !== record.cwd) {
|
|
872
1119
|
throw new Error('Captain session record cwd must be normalized');
|
|
873
1120
|
}
|
|
874
|
-
|
|
875
|
-
|
|
1121
|
+
const structural = validateCaptainSessionStructuralProjection(
|
|
1122
|
+
record.structuralProjection,
|
|
1123
|
+
'Captain session record structuralProjection',
|
|
1124
|
+
);
|
|
1125
|
+
const lastApplied = assertCaptainSessionExecutionCompatible(
|
|
1126
|
+
structural,
|
|
1127
|
+
record.lastAppliedExecutionProjection,
|
|
1128
|
+
);
|
|
1129
|
+
void lastApplied;
|
|
1130
|
+
const snapshot = assertPlaybookCaptainShellSnapshot(record.snapshot);
|
|
1131
|
+
assertSnapshotMatchesStructure(snapshot, structural);
|
|
1132
|
+
if (
|
|
1133
|
+
snapshot.captain.sessionId === record.sessionId ||
|
|
1134
|
+
snapshot.issuedSessionIds.includes(record.sessionId)
|
|
1135
|
+
) {
|
|
1136
|
+
throw new Error(
|
|
1137
|
+
'Captain session public id collides with an internal Captain session id',
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
876
1140
|
|
|
877
1141
|
if (record.state === 'uncertain') {
|
|
878
1142
|
const uncertain = requireRecord(
|
|
@@ -887,7 +1151,7 @@ export function validateCaptainSessionRecord(value) {
|
|
|
887
1151
|
if (uncertain.baseUpdatedAt !== null) {
|
|
888
1152
|
canonicalTimestamp(uncertain.baseUpdatedAt, 'uncertain.baseUpdatedAt');
|
|
889
1153
|
if (
|
|
890
|
-
Date.parse(uncertain.baseUpdatedAt)
|
|
1154
|
+
Date.parse(uncertain.baseUpdatedAt) <= Date.parse(createdAt) ||
|
|
891
1155
|
Date.parse(uncertain.baseUpdatedAt) >= Date.parse(updatedAt)
|
|
892
1156
|
) {
|
|
893
1157
|
throw new Error(
|
|
@@ -919,11 +1183,107 @@ export function validateCaptainSessionRecord(value) {
|
|
|
919
1183
|
'Captain session uncertain markedAt must equal updatedAt',
|
|
920
1184
|
);
|
|
921
1185
|
}
|
|
1186
|
+
const attempted = assertCaptainSessionExecutionCompatible(
|
|
1187
|
+
structural,
|
|
1188
|
+
uncertain.attemptedExecutionProjection,
|
|
1189
|
+
);
|
|
1190
|
+
if (uncertain.baseUpdatedAt === null) {
|
|
1191
|
+
if (!isDeepStrictEqual(lastApplied, attempted)) {
|
|
1192
|
+
throw new Error(
|
|
1193
|
+
'fresh Captain session baseline and attempted execution projections must match',
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
assertTurnZeroSnapshot(
|
|
1197
|
+
snapshot,
|
|
1198
|
+
'fresh Captain session record snapshot',
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
922
1201
|
}
|
|
923
1202
|
|
|
924
1203
|
return record;
|
|
925
1204
|
}
|
|
926
1205
|
|
|
1206
|
+
function assertReleasedSchema2CaptainSessionRecord(record) {
|
|
1207
|
+
if (record.state !== 'settled' && record.state !== 'uncertain') {
|
|
1208
|
+
throw new Error('Captain session record state is not supported');
|
|
1209
|
+
}
|
|
1210
|
+
rejectUnknownOrMissingKeys(
|
|
1211
|
+
record,
|
|
1212
|
+
record.state === 'uncertain'
|
|
1213
|
+
? [...RELEASED_SCHEMA_2_COMMON_RECORD_KEYS, 'uncertain']
|
|
1214
|
+
: RELEASED_SCHEMA_2_COMMON_RECORD_KEYS,
|
|
1215
|
+
'Captain session record',
|
|
1216
|
+
);
|
|
1217
|
+
if (record.kind !== CAPTAIN_SESSION_RECORD_KIND) {
|
|
1218
|
+
throw new Error('Captain session record kind is not supported');
|
|
1219
|
+
}
|
|
1220
|
+
assertSessionId(record.sessionId);
|
|
1221
|
+
const createdAt = canonicalTimestamp(record.createdAt, 'createdAt');
|
|
1222
|
+
const updatedAt = canonicalTimestamp(record.updatedAt, 'updatedAt');
|
|
1223
|
+
if (Date.parse(updatedAt) < Date.parse(createdAt)) {
|
|
1224
|
+
throw new Error('Captain session record updatedAt precedes createdAt');
|
|
1225
|
+
}
|
|
1226
|
+
if (record.state === 'settled' && updatedAt === createdAt) {
|
|
1227
|
+
throw new Error(
|
|
1228
|
+
'settled Captain session updatedAt must follow its creation marker',
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
if (typeof record.cwd !== 'string' || !isAbsolute(record.cwd)) {
|
|
1232
|
+
throw new Error('Captain session record cwd must be an absolute path');
|
|
1233
|
+
}
|
|
1234
|
+
if (resolve(record.cwd) !== record.cwd) {
|
|
1235
|
+
throw new Error('Captain session record cwd must be normalized');
|
|
1236
|
+
}
|
|
1237
|
+
requireRecord(record.config, 'Captain session record config');
|
|
1238
|
+
requireRecord(record.snapshot, 'Captain session record snapshot');
|
|
1239
|
+
|
|
1240
|
+
if (record.state !== 'uncertain') return;
|
|
1241
|
+
const uncertain = requireRecord(
|
|
1242
|
+
record.uncertain,
|
|
1243
|
+
'Captain session record uncertain',
|
|
1244
|
+
);
|
|
1245
|
+
rejectUnknownOrMissingKeys(
|
|
1246
|
+
uncertain,
|
|
1247
|
+
RELEASED_SCHEMA_2_UNCERTAIN_KEYS,
|
|
1248
|
+
'Captain session record uncertain',
|
|
1249
|
+
);
|
|
1250
|
+
if (uncertain.baseUpdatedAt !== null) {
|
|
1251
|
+
canonicalTimestamp(uncertain.baseUpdatedAt, 'uncertain.baseUpdatedAt');
|
|
1252
|
+
if (
|
|
1253
|
+
Date.parse(uncertain.baseUpdatedAt) < Date.parse(createdAt) ||
|
|
1254
|
+
Date.parse(uncertain.baseUpdatedAt) >= Date.parse(updatedAt)
|
|
1255
|
+
) {
|
|
1256
|
+
throw new Error(
|
|
1257
|
+
'Captain session uncertain baseUpdatedAt must identify an earlier settled boundary',
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
} else {
|
|
1261
|
+
const isFirstAttempt = uncertain.attemptNumber === 1;
|
|
1262
|
+
if (isFirstAttempt !== (updatedAt === createdAt)) {
|
|
1263
|
+
throw new Error(
|
|
1264
|
+
'fresh Captain session retry timestamps must match the attempt boundary',
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
assertAcceptedInput(uncertain.input);
|
|
1269
|
+
assertUuid(uncertain.attemptId, 'Captain session attempt id');
|
|
1270
|
+
if (
|
|
1271
|
+
!Number.isSafeInteger(uncertain.attemptNumber) ||
|
|
1272
|
+
uncertain.attemptNumber <= 0
|
|
1273
|
+
) {
|
|
1274
|
+
throw new Error('Captain session attempt number must be a positive integer');
|
|
1275
|
+
}
|
|
1276
|
+
const markedAt = canonicalTimestamp(
|
|
1277
|
+
uncertain.markedAt,
|
|
1278
|
+
'uncertain.markedAt',
|
|
1279
|
+
);
|
|
1280
|
+
if (markedAt !== updatedAt) {
|
|
1281
|
+
throw new Error(
|
|
1282
|
+
'Captain session uncertain markedAt must equal updatedAt',
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
927
1287
|
function validateFreshBoundary(value) {
|
|
928
1288
|
const boundary = requireRecord(
|
|
929
1289
|
snapshotJsonValue(value, 'fresh Captain session boundary'),
|
|
@@ -931,7 +1291,7 @@ function validateFreshBoundary(value) {
|
|
|
931
1291
|
);
|
|
932
1292
|
rejectUnknownOrMissingKeys(
|
|
933
1293
|
boundary,
|
|
934
|
-
['cwd', '
|
|
1294
|
+
['cwd', 'structuralProjection', 'snapshot'],
|
|
935
1295
|
'fresh Captain session boundary',
|
|
936
1296
|
);
|
|
937
1297
|
if (typeof boundary.cwd !== 'string' || !isAbsolute(boundary.cwd)) {
|
|
@@ -940,11 +1300,384 @@ function validateFreshBoundary(value) {
|
|
|
940
1300
|
if (resolve(boundary.cwd) !== boundary.cwd) {
|
|
941
1301
|
throw new Error('fresh Captain session cwd must be normalized');
|
|
942
1302
|
}
|
|
943
|
-
|
|
944
|
-
|
|
1303
|
+
const structural = validateCaptainSessionStructuralProjection(
|
|
1304
|
+
boundary.structuralProjection,
|
|
1305
|
+
'fresh Captain session structuralProjection',
|
|
1306
|
+
);
|
|
1307
|
+
const snapshot = assertPlaybookCaptainShellSnapshot(boundary.snapshot);
|
|
1308
|
+
assertSnapshotMatchesStructure(snapshot, structural);
|
|
1309
|
+
assertTurnZeroSnapshot(snapshot, 'fresh Captain session boundary snapshot');
|
|
945
1310
|
return boundary;
|
|
946
1311
|
}
|
|
947
1312
|
|
|
1313
|
+
function assertTurnZeroSnapshot(snapshot, path) {
|
|
1314
|
+
if (
|
|
1315
|
+
snapshot.sequences.turn !== 0 ||
|
|
1316
|
+
snapshot.sequences.journal !== 0 ||
|
|
1317
|
+
snapshot.journal.length !== 0 ||
|
|
1318
|
+
snapshot.captain.conversation.kind !== 'unopened'
|
|
1319
|
+
) {
|
|
1320
|
+
throw new Error(`${path} must be an initialized turn-zero shell snapshot`);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function validateCaptainSessionProjection(
|
|
1325
|
+
projection,
|
|
1326
|
+
{ path, structural },
|
|
1327
|
+
) {
|
|
1328
|
+
rejectUnknownOrMissingKeys(
|
|
1329
|
+
projection,
|
|
1330
|
+
['schemaVersion', 'captain', 'players', 'catalog'],
|
|
1331
|
+
path,
|
|
1332
|
+
);
|
|
1333
|
+
const expectedSchema = structural
|
|
1334
|
+
? CAPTAIN_SESSION_STRUCTURAL_PROJECTION_SCHEMA_VERSION
|
|
1335
|
+
: CAPTAIN_SESSION_EXECUTION_PROJECTION_SCHEMA_VERSION;
|
|
1336
|
+
if (projection.schemaVersion !== expectedSchema) {
|
|
1337
|
+
throw new Error(
|
|
1338
|
+
`${path}.schemaVersion ${JSON.stringify(projection.schemaVersion)} is not supported (expected ${expectedSchema})`,
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
validateProjectedAgent(projection.captain, `${path}.captain`, {
|
|
1342
|
+
structural,
|
|
1343
|
+
});
|
|
1344
|
+
if (!Array.isArray(projection.players)) {
|
|
1345
|
+
throw new Error(`${path}.players must be an array`);
|
|
1346
|
+
}
|
|
1347
|
+
const playerIds = [];
|
|
1348
|
+
for (let index = 0; index < projection.players.length; index += 1) {
|
|
1349
|
+
const playerPath = `${path}.players[${index}]`;
|
|
1350
|
+
const player = requireRecord(projection.players[index], playerPath);
|
|
1351
|
+
exactOptionalKeys(
|
|
1352
|
+
player,
|
|
1353
|
+
structural
|
|
1354
|
+
? ['id', 'adapter']
|
|
1355
|
+
: ['id', 'adapter', 'model', 'effort'],
|
|
1356
|
+
['instruction', 'permissions'],
|
|
1357
|
+
playerPath,
|
|
1358
|
+
);
|
|
1359
|
+
assertPlayerId(player.id, `${playerPath}.id`);
|
|
1360
|
+
validateProjectedAgent(player, playerPath, { structural, hasId: true });
|
|
1361
|
+
playerIds.push(player.id);
|
|
1362
|
+
}
|
|
1363
|
+
if (new Set(playerIds).size !== playerIds.length) {
|
|
1364
|
+
throw new Error(`${path}.players contains a duplicate player id`);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
const catalog = requireRecord(projection.catalog, `${path}.catalog`);
|
|
1368
|
+
const catalogEntries = Object.entries(catalog);
|
|
1369
|
+
if (catalogEntries.length === 0) {
|
|
1370
|
+
throw new Error(`${path}.catalog must not be empty`);
|
|
1371
|
+
}
|
|
1372
|
+
const commands = new Set();
|
|
1373
|
+
for (const [id, rawItem] of catalogEntries) {
|
|
1374
|
+
const itemPath = `${path}.catalog.${id}`;
|
|
1375
|
+
const item = requireRecord(rawItem, itemPath);
|
|
1376
|
+
rejectUnknownOrMissingKeys(
|
|
1377
|
+
item,
|
|
1378
|
+
[
|
|
1379
|
+
'id',
|
|
1380
|
+
'from',
|
|
1381
|
+
'manifestCommand',
|
|
1382
|
+
'command',
|
|
1383
|
+
'intent',
|
|
1384
|
+
'artifactSchema',
|
|
1385
|
+
'requiredRoleIds',
|
|
1386
|
+
'concurrentRoleSets',
|
|
1387
|
+
'roles',
|
|
1388
|
+
'options',
|
|
1389
|
+
],
|
|
1390
|
+
itemPath,
|
|
1391
|
+
);
|
|
1392
|
+
if (requireCanonicalNonblank(item.id, `${itemPath}.id`) !== id) {
|
|
1393
|
+
throw new Error(`${itemPath}.id must equal its catalog key`);
|
|
1394
|
+
}
|
|
1395
|
+
if (id === RESERVED_ID) {
|
|
1396
|
+
throw new Error(`${itemPath}.id uses reserved id "captain"`);
|
|
1397
|
+
}
|
|
1398
|
+
validateCanonicalModuleSpecifier(item.from, `${itemPath}.from`);
|
|
1399
|
+
requireCanonicalNonblank(
|
|
1400
|
+
item.manifestCommand,
|
|
1401
|
+
`${itemPath}.manifestCommand`,
|
|
1402
|
+
);
|
|
1403
|
+
const command = requireCanonicalNonblank(
|
|
1404
|
+
item.command,
|
|
1405
|
+
`${itemPath}.command`,
|
|
1406
|
+
);
|
|
1407
|
+
if (command === RESERVED_ID) {
|
|
1408
|
+
throw new Error(`${itemPath}.command uses reserved command "captain"`);
|
|
1409
|
+
}
|
|
1410
|
+
if (commands.has(command)) {
|
|
1411
|
+
throw new Error(`${path}.catalog contains duplicate command ${JSON.stringify(command)}`);
|
|
1412
|
+
}
|
|
1413
|
+
commands.add(command);
|
|
1414
|
+
if (typeof item.intent !== 'string') {
|
|
1415
|
+
throw new Error(`${itemPath}.intent must be a string`);
|
|
1416
|
+
}
|
|
1417
|
+
if (item.artifactSchema !== 2) {
|
|
1418
|
+
throw new Error(`${itemPath}.artifactSchema must be exactly 2`);
|
|
1419
|
+
}
|
|
1420
|
+
const requiredRoleIds = validateRoleIds(
|
|
1421
|
+
item.requiredRoleIds,
|
|
1422
|
+
`${itemPath}.requiredRoleIds`,
|
|
1423
|
+
);
|
|
1424
|
+
validateConcurrentRoleSets(
|
|
1425
|
+
item.concurrentRoleSets,
|
|
1426
|
+
requiredRoleIds,
|
|
1427
|
+
`${itemPath}.concurrentRoleSets`,
|
|
1428
|
+
);
|
|
1429
|
+
const roles = requireRecord(item.roles, `${itemPath}.roles`);
|
|
1430
|
+
if (!isDeepStrictEqual(Object.keys(roles), requiredRoleIds)) {
|
|
1431
|
+
throw new Error(`${itemPath}.roles must exactly follow requiredRoleIds`);
|
|
1432
|
+
}
|
|
1433
|
+
for (const roleId of requiredRoleIds) {
|
|
1434
|
+
const bindingPath = `${itemPath}.roles.${roleId}`;
|
|
1435
|
+
const binding = requireRecord(roles[roleId], bindingPath);
|
|
1436
|
+
rejectUnknownOrMissingKeys(
|
|
1437
|
+
binding,
|
|
1438
|
+
structural
|
|
1439
|
+
? ['playerId']
|
|
1440
|
+
: ['playerId', 'model', 'effort'],
|
|
1441
|
+
bindingPath,
|
|
1442
|
+
);
|
|
1443
|
+
assertPlayerId(binding.playerId, `${bindingPath}.playerId`);
|
|
1444
|
+
if (!structural) {
|
|
1445
|
+
validateTuningSelection(binding.model, `${bindingPath}.model`);
|
|
1446
|
+
const player = projection.players.find(
|
|
1447
|
+
(candidate) => candidate.id === binding.playerId,
|
|
1448
|
+
);
|
|
1449
|
+
if (player === undefined) {
|
|
1450
|
+
throw new Error(
|
|
1451
|
+
`${bindingPath}.playerId names a player absent from ${path}.players`,
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
validateEffortSelection(
|
|
1455
|
+
binding.effort,
|
|
1456
|
+
player.adapter,
|
|
1457
|
+
`${bindingPath}.effort`,
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
for (const [setIndex, set] of item.concurrentRoleSets.entries()) {
|
|
1462
|
+
const concurrentPlayers = set.map(
|
|
1463
|
+
(roleId) => roles[roleId].playerId,
|
|
1464
|
+
);
|
|
1465
|
+
if (new Set(concurrentPlayers).size !== concurrentPlayers.length) {
|
|
1466
|
+
throw new Error(
|
|
1467
|
+
`${itemPath}.concurrentRoleSets[${setIndex}] binds one player more than once`,
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
requireRecord(item.options, `${itemPath}.options`);
|
|
1472
|
+
}
|
|
1473
|
+
const referenced = referencedPlayerIds(catalog);
|
|
1474
|
+
if (!isDeepStrictEqual(playerIds, referenced)) {
|
|
1475
|
+
throw new Error(
|
|
1476
|
+
`${path}.players must equal the ordered player ids referenced by catalog roles`,
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function validateProjectedAgent(value, path, { structural, hasId = false }) {
|
|
1482
|
+
const agent = requireRecord(value, path);
|
|
1483
|
+
if (!hasId) {
|
|
1484
|
+
exactOptionalKeys(
|
|
1485
|
+
agent,
|
|
1486
|
+
structural ? ['adapter'] : ['adapter', 'model', 'effort'],
|
|
1487
|
+
['instruction', 'permissions'],
|
|
1488
|
+
path,
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
const adapter = requireCanonicalNonblank(agent.adapter, `${path}.adapter`);
|
|
1492
|
+
if (!KNOWN_ADAPTERS.has(adapter)) {
|
|
1493
|
+
throw new Error(`${path}.adapter ${JSON.stringify(adapter)} is not supported`);
|
|
1494
|
+
}
|
|
1495
|
+
if (!structural) {
|
|
1496
|
+
validateTuningSelection(agent.model, `${path}.model`);
|
|
1497
|
+
validateEffortSelection(agent.effort, adapter, `${path}.effort`);
|
|
1498
|
+
}
|
|
1499
|
+
if (agent.instruction !== undefined && typeof agent.instruction !== 'string') {
|
|
1500
|
+
throw new Error(`${path}.instruction must be a string`);
|
|
1501
|
+
}
|
|
1502
|
+
if (agent.permissions !== undefined) {
|
|
1503
|
+
validatePermissionPolicy(agent.permissions, `${path}.permissions`);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function validateTuningSelection(value, path) {
|
|
1508
|
+
const selection = requireRecord(value, path);
|
|
1509
|
+
if (selection.kind === 'provider-default') {
|
|
1510
|
+
rejectUnknownOrMissingKeys(selection, ['kind'], path);
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
if (selection.kind === 'value') {
|
|
1514
|
+
rejectUnknownOrMissingKeys(selection, ['kind', 'value'], path);
|
|
1515
|
+
requireNonblank(selection.value, `${path}.value`);
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
throw new Error(`${path}.kind is not supported`);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function validateEffortSelection(value, adapter, path) {
|
|
1522
|
+
validateTuningSelection(value, path);
|
|
1523
|
+
if (value.kind !== 'value') return;
|
|
1524
|
+
try {
|
|
1525
|
+
assertSupportedEffort(adapter, value.value, `${path}.value`);
|
|
1526
|
+
} catch (cause) {
|
|
1527
|
+
throw new Error(errorMessage(cause));
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
function validatePermissionPolicy(value, path) {
|
|
1532
|
+
const permissions = requireRecord(value, path);
|
|
1533
|
+
exactOptionalKeys(
|
|
1534
|
+
permissions,
|
|
1535
|
+
[],
|
|
1536
|
+
[
|
|
1537
|
+
'mode',
|
|
1538
|
+
'fileWrite',
|
|
1539
|
+
'shellExecute',
|
|
1540
|
+
'networkAccess',
|
|
1541
|
+
'writablePaths',
|
|
1542
|
+
],
|
|
1543
|
+
path,
|
|
1544
|
+
);
|
|
1545
|
+
if (
|
|
1546
|
+
permissions.mode !== undefined &&
|
|
1547
|
+
permissions.mode !== 'auto' &&
|
|
1548
|
+
permissions.mode !== 'bypass'
|
|
1549
|
+
) {
|
|
1550
|
+
throw new Error(`${path}.mode must be "auto" or "bypass"`);
|
|
1551
|
+
}
|
|
1552
|
+
for (const key of ['fileWrite', 'shellExecute', 'networkAccess']) {
|
|
1553
|
+
if (
|
|
1554
|
+
permissions[key] !== undefined &&
|
|
1555
|
+
!['allow', 'ask', 'deny'].includes(permissions[key])
|
|
1556
|
+
) {
|
|
1557
|
+
throw new Error(`${path}.${key} must be "allow", "ask", or "deny"`);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
if (permissions.writablePaths !== undefined) {
|
|
1561
|
+
if (
|
|
1562
|
+
!Array.isArray(permissions.writablePaths) ||
|
|
1563
|
+
permissions.writablePaths.some(
|
|
1564
|
+
(entry) => typeof entry !== 'string' || entry.length === 0,
|
|
1565
|
+
)
|
|
1566
|
+
) {
|
|
1567
|
+
throw new Error(
|
|
1568
|
+
`${path}.writablePaths must be an array of non-empty strings`,
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
function validateRoleIds(value, path) {
|
|
1575
|
+
if (
|
|
1576
|
+
!Array.isArray(value) ||
|
|
1577
|
+
value.some(
|
|
1578
|
+
(roleId) =>
|
|
1579
|
+
typeof roleId !== 'string' ||
|
|
1580
|
+
!ROLE_ID_PATTERN.test(roleId) ||
|
|
1581
|
+
roleId === RESERVED_ID,
|
|
1582
|
+
) ||
|
|
1583
|
+
new Set(value).size !== value.length
|
|
1584
|
+
) {
|
|
1585
|
+
throw new Error(`${path} must contain distinct canonical local role ids`);
|
|
1586
|
+
}
|
|
1587
|
+
return value;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function validateConcurrentRoleSets(value, requiredRoleIds, path) {
|
|
1591
|
+
if (!Array.isArray(value)) {
|
|
1592
|
+
throw new Error(`${path} must be an array`);
|
|
1593
|
+
}
|
|
1594
|
+
const required = new Set(requiredRoleIds);
|
|
1595
|
+
const signatures = new Set();
|
|
1596
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1597
|
+
const set = value[index];
|
|
1598
|
+
if (
|
|
1599
|
+
!Array.isArray(set) ||
|
|
1600
|
+
set.length < 2 ||
|
|
1601
|
+
set.some((roleId) => !required.has(roleId)) ||
|
|
1602
|
+
new Set(set).size !== set.length
|
|
1603
|
+
) {
|
|
1604
|
+
throw new Error(
|
|
1605
|
+
`${path}[${index}] must contain at least two distinct required roles`,
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
const signature = JSON.stringify(set);
|
|
1609
|
+
if (signatures.has(signature)) {
|
|
1610
|
+
throw new Error(`${path} contains duplicate set ${signature}`);
|
|
1611
|
+
}
|
|
1612
|
+
signatures.add(signature);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function referencedPlayerIds(catalog) {
|
|
1617
|
+
const seen = new Set();
|
|
1618
|
+
const ids = [];
|
|
1619
|
+
for (const item of Object.values(catalog)) {
|
|
1620
|
+
for (const roleId of item.requiredRoleIds) {
|
|
1621
|
+
const playerId = item.roles[roleId].playerId;
|
|
1622
|
+
if (!seen.has(playerId)) {
|
|
1623
|
+
seen.add(playerId);
|
|
1624
|
+
ids.push(playerId);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
return ids;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
function assertSnapshotMatchesStructure(snapshot, structural) {
|
|
1632
|
+
if (!isDeepStrictEqual(snapshot.captain.agent, structural.captain)) {
|
|
1633
|
+
throw new Error(
|
|
1634
|
+
'Captain session snapshot Captain envelope differs from structuralProjection',
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
const structuralPlayers = new Map(
|
|
1638
|
+
structural.players.map(({ id, ...agent }) => [id, agent]),
|
|
1639
|
+
);
|
|
1640
|
+
if (
|
|
1641
|
+
!isDeepStrictEqual(
|
|
1642
|
+
Object.keys(snapshot.playerSessions),
|
|
1643
|
+
[...structuralPlayers.keys()],
|
|
1644
|
+
)
|
|
1645
|
+
) {
|
|
1646
|
+
throw new Error(
|
|
1647
|
+
'Captain session snapshot player ledger differs from structuralProjection roster',
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
for (const [playerId, agent] of structuralPlayers) {
|
|
1651
|
+
const { resumeToken: _resumeToken, ...savedAgent } =
|
|
1652
|
+
snapshot.playerSessions[playerId];
|
|
1653
|
+
if (!isDeepStrictEqual(savedAgent, agent)) {
|
|
1654
|
+
throw new Error(
|
|
1655
|
+
`Captain session snapshot player ${JSON.stringify(playerId)} envelope differs from structuralProjection`,
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
if (snapshot.mode !== 'engaged.parked') return;
|
|
1660
|
+
for (const frame of snapshot.frames) {
|
|
1661
|
+
const item = structural.catalog[frame.playbookId];
|
|
1662
|
+
if (item === undefined) {
|
|
1663
|
+
throw new Error(
|
|
1664
|
+
`Captain session snapshot frame names unknown stored playbook ${JSON.stringify(frame.playbookId)}`,
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
const roleBindings = Object.fromEntries(
|
|
1668
|
+
item.requiredRoleIds.map((roleId) => [
|
|
1669
|
+
roleId,
|
|
1670
|
+
item.roles[roleId].playerId,
|
|
1671
|
+
]),
|
|
1672
|
+
);
|
|
1673
|
+
if (!isDeepStrictEqual(frame.roleBindings, roleBindings)) {
|
|
1674
|
+
throw new Error(
|
|
1675
|
+
`Captain session snapshot frame ${JSON.stringify(frame.playbookId)} role bindings differ from structuralProjection`,
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
|
|
948
1681
|
function validateLeaseOwner(value) {
|
|
949
1682
|
const owner = requireRecord(
|
|
950
1683
|
snapshotJsonValue(value, 'Captain session lease owner'),
|
|
@@ -994,6 +1727,18 @@ function assertSessionId(value) {
|
|
|
994
1727
|
assertUuid(value, 'Captain session id');
|
|
995
1728
|
}
|
|
996
1729
|
|
|
1730
|
+
function assertPlayerId(value, path) {
|
|
1731
|
+
if (
|
|
1732
|
+
typeof value !== 'string' ||
|
|
1733
|
+
!PLAYER_ID_PATTERN.test(value) ||
|
|
1734
|
+
value === RESERVED_ID
|
|
1735
|
+
) {
|
|
1736
|
+
throw new Error(
|
|
1737
|
+
`${path} must be a non-reserved player id matching ${PLAYER_ID_PATTERN.source}`,
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
997
1742
|
function assertUuid(value, path) {
|
|
998
1743
|
if (typeof value !== 'string' || !SESSION_ID_PATTERN.test(value)) {
|
|
999
1744
|
throw new Error(`${path} must be a UUID`);
|
|
@@ -1020,6 +1765,53 @@ function rejectUnknownOrMissingKeys(value, expected, path) {
|
|
|
1020
1765
|
}
|
|
1021
1766
|
}
|
|
1022
1767
|
|
|
1768
|
+
function exactOptionalKeys(value, required, optional, path) {
|
|
1769
|
+
rejectUnknownOrMissingKeys(
|
|
1770
|
+
value,
|
|
1771
|
+
[
|
|
1772
|
+
...required,
|
|
1773
|
+
...optional.filter((key) => Object.hasOwn(value, key)),
|
|
1774
|
+
],
|
|
1775
|
+
path,
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function requireNonblank(value, path) {
|
|
1780
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
1781
|
+
throw new Error(`${path} must be a nonblank string`);
|
|
1782
|
+
}
|
|
1783
|
+
return value;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
function requireCanonicalNonblank(value, path) {
|
|
1787
|
+
const text = requireNonblank(value, path);
|
|
1788
|
+
if (text !== text.trim()) {
|
|
1789
|
+
throw new Error(`${path} must be in canonical trimmed form`);
|
|
1790
|
+
}
|
|
1791
|
+
return text;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
function validateCanonicalModuleSpecifier(value, path) {
|
|
1795
|
+
const specifier = requireCanonicalNonblank(value, path);
|
|
1796
|
+
if (
|
|
1797
|
+
isAbsolute(specifier) ||
|
|
1798
|
+
/^(?:\.{1,2}(?:[\\/]|$)|[\\/]|[A-Za-z]:[\\/])/.test(specifier)
|
|
1799
|
+
) {
|
|
1800
|
+
throw new Error(`${path} must be a canonical module specifier`);
|
|
1801
|
+
}
|
|
1802
|
+
if (!specifier.startsWith('file:')) return specifier;
|
|
1803
|
+
let canonical;
|
|
1804
|
+
try {
|
|
1805
|
+
canonical = pathToFileURL(fileURLToPath(specifier)).href;
|
|
1806
|
+
} catch {
|
|
1807
|
+
throw new Error(`${path} must be a canonical file URL`);
|
|
1808
|
+
}
|
|
1809
|
+
if (canonical !== specifier) {
|
|
1810
|
+
throw new Error(`${path} must be a canonical file URL`);
|
|
1811
|
+
}
|
|
1812
|
+
return specifier;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1023
1815
|
function canonicalTimestamp(value, field) {
|
|
1024
1816
|
if (
|
|
1025
1817
|
typeof value !== 'string' ||
|