@sublang/playbook 12.2.2 → 13.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/docs/cli.md +56 -52
- package/docs/configuration.md +2 -2
- package/docs/embedding.md +68 -32
- package/package.json +11 -3
- package/reference/sdlc/code.playbook/bin/interactive-session.js +1 -0
- package/reference/sdlc/code.playbook/bin/launch-config.js +13 -7
- package/reference/sdlc/code.playbook/bin/playbook.js +42 -4
- package/reference/sdlc/code.playbook/bin/portable-codec.js +190 -0
- package/reference/sdlc/code.playbook/bin/replay-observer.js +18 -2
- package/reference/sdlc/code.playbook/bin/run.js +62 -18
- package/reference/sdlc/code.playbook/bin/session-host.js +104 -0
- package/reference/sdlc/code.playbook/bin/session-store.js +638 -70
- package/reference/sdlc/code.playbook/code.fsm.js +43 -5
- package/reference/sdlc/code.playbook/code.fsm.ts +66 -9
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +5 -0
- package/reference/sdlc/code.playbook/playbook-captain.js +75 -19
- package/reference/sdlc/code.playbook/playbook-captain.ts +93 -26
- package/reference/sdlc/code.playbook/session-host.d.ts +75 -0
- package/reference/sdlc/code.playbook/session-host.js +18 -0
- package/reference/sdlc/code.playbook/session-store.d.ts +176 -1
- package/reference/sdlc/code.playbook/session-store.js +22 -6
- package/reference/sdlc/decide.playbook/decide.fsm.js +4 -0
- package/reference/sdlc/decide.playbook/decide.fsm.ts +4 -0
- package/reference/sdlc/decide.playbook/decide.playbook.js +23 -5
- package/reference/sdlc/decide.playbook/decide.playbook.ts +25 -4
- package/reference/sdlc/dev.playbook/dev.fsm.js +57 -7
- package/reference/sdlc/dev.playbook/dev.fsm.ts +80 -11
- package/reference/sdlc/review.playbook/review.fsm.js +11 -1
- package/reference/sdlc/review.playbook/review.fsm.ts +15 -1
- package/slc/gears2fsm.md +21 -3
- package/slc/link.md +21 -2
- package/src/runtime.d.ts +7 -0
- package/src/runtime.ts +12 -0
- package/src/xstate-playbook-runtime.d.ts +13 -0
- package/src/xstate-playbook-runtime.js +75 -13
- package/src/xstate-playbook-runtime.ts +87 -21
- package/src/xstate-runtime.js +35 -4
- package/src/xstate-runtime.ts +49 -4
|
@@ -103,6 +103,11 @@ type SnapshotAgentEnvelope = DeepReadonly<
|
|
|
103
103
|
type PlayerLedgerSnapshotEntry = DeepReadonly<PlayerLedgerEntry>;
|
|
104
104
|
|
|
105
105
|
export interface PlaybookCaptainDeps {
|
|
106
|
+
continuity?: {
|
|
107
|
+
beforeCall(participantId: string): Promise<void>;
|
|
108
|
+
acknowledged(participantId: string, token: string): void;
|
|
109
|
+
reset(participantId: string, reason: 'missing_hint' | 'rejected_hint'): Promise<void>;
|
|
110
|
+
};
|
|
106
111
|
loadModule?: (specifier: string) => Promise<unknown>;
|
|
107
112
|
createSessionId?: () => string;
|
|
108
113
|
hostCapabilities?: Readonly<
|
|
@@ -549,8 +554,24 @@ const INTERNAL_CAPTAIN_ID = 'captain';
|
|
|
549
554
|
const UUID_PATTERN =
|
|
550
555
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
551
556
|
const PLAYER_ID_PATTERN = /^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)*$/;
|
|
552
|
-
const
|
|
557
|
+
const ROLE_ID_WHITESPACE_OR_CONTROL = /[\s\p{Cc}]/u;
|
|
553
558
|
const HOST_CAPABILITIES_OPTION_KEY = 'hostCapabilities';
|
|
559
|
+
|
|
560
|
+
// PBCLI-4: a local role id is its source role name lowercased by Unicode case
|
|
561
|
+
// mapping — nonempty, free of whitespace and control characters, and equal to
|
|
562
|
+
// its own lowercase form. The rule restricts neither script nor alphabet, so
|
|
563
|
+
// `coder`, `编码者`, and `作者` are canonical while `Coder` is not. Callers
|
|
564
|
+
// enforce the reserved `captain` name separately. The CLI host owns the same
|
|
565
|
+
// predicate in `bin/session-store.js`; that private module already imports
|
|
566
|
+
// this one, so the text is repeated here rather than cycled back.
|
|
567
|
+
function isCanonicalLocalRoleId(value: unknown): value is string {
|
|
568
|
+
return (
|
|
569
|
+
typeof value === 'string' &&
|
|
570
|
+
value.length > 0 &&
|
|
571
|
+
value === value.toLowerCase() &&
|
|
572
|
+
!ROLE_ID_WHITESPACE_OR_CONTROL.test(value)
|
|
573
|
+
);
|
|
574
|
+
}
|
|
554
575
|
const UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID =
|
|
555
576
|
'reconcile:unresolved-effect';
|
|
556
577
|
const UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID = 'abandon:unresolved-effect';
|
|
@@ -1452,9 +1473,7 @@ function isValidRegistryEntry(
|
|
|
1452
1473
|
!Array.isArray(e.requiredRoleIds) ||
|
|
1453
1474
|
e.requiredRoleIds.some(
|
|
1454
1475
|
(role) =>
|
|
1455
|
-
|
|
1456
|
-
!ROLE_ID_PATTERN.test(role) ||
|
|
1457
|
-
role === INTERNAL_CAPTAIN_ID,
|
|
1476
|
+
!isCanonicalLocalRoleId(role) || role === INTERNAL_CAPTAIN_ID,
|
|
1458
1477
|
) ||
|
|
1459
1478
|
new Set(e.requiredRoleIds).size !== e.requiredRoleIds.length ||
|
|
1460
1479
|
!Array.isArray(e.concurrentRoleSets)
|
|
@@ -1773,7 +1792,7 @@ function snapshotFrameRoleBindings(
|
|
|
1773
1792
|
const bindings = snapshotRecord(value, path);
|
|
1774
1793
|
return Object.fromEntries(
|
|
1775
1794
|
Object.entries(bindings).map(([roleId, raw]) => {
|
|
1776
|
-
if (!
|
|
1795
|
+
if (!isCanonicalLocalRoleId(roleId) || roleId === INTERNAL_CAPTAIN_ID) {
|
|
1777
1796
|
throw new TypeError(`${path} has invalid role id ${JSON.stringify(roleId)}`);
|
|
1778
1797
|
}
|
|
1779
1798
|
const playerId = snapshotString(raw, `${path}.${roleId}`);
|
|
@@ -1805,6 +1824,7 @@ function normalizeHostPlayerResult(
|
|
|
1805
1824
|
'resumeToken',
|
|
1806
1825
|
'finalText',
|
|
1807
1826
|
'error',
|
|
1827
|
+
'errorCode',
|
|
1808
1828
|
]);
|
|
1809
1829
|
const normalized: Record<string, unknown> = {};
|
|
1810
1830
|
for (const key of Reflect.ownKeys(descriptors)) {
|
|
@@ -1827,13 +1847,18 @@ function normalizeHostPlayerResult(
|
|
|
1827
1847
|
const record = snapshotRecord(snapshotJsonValue(normalized, path), path);
|
|
1828
1848
|
rejectSnapshotKeys(
|
|
1829
1849
|
record,
|
|
1830
|
-
['status', 'playerId', 'turnId', 'resumeToken', 'finalText', 'error'],
|
|
1850
|
+
['status', 'playerId', 'turnId', 'resumeToken', 'finalText', 'error', 'errorCode'],
|
|
1831
1851
|
path,
|
|
1832
1852
|
);
|
|
1833
1853
|
if (record.playerId !== expectedPlayerId) {
|
|
1834
1854
|
throw new TypeError(`${path}.playerId does not match the requested player`);
|
|
1835
1855
|
}
|
|
1836
1856
|
snapshotInteger(record.turnId, `${path}.turnId`, 1);
|
|
1857
|
+
if (record.errorCode !== undefined &&
|
|
1858
|
+
(record.errorCode !== 'SESSION_RESUME_REJECTED' ||
|
|
1859
|
+
record.status !== 'error' || record.resumeToken !== undefined)) {
|
|
1860
|
+
throw new TypeError(`${path}.errorCode is not a definite resume rejection`);
|
|
1861
|
+
}
|
|
1837
1862
|
return validatePlayerResult(
|
|
1838
1863
|
{
|
|
1839
1864
|
status: record.status,
|
|
@@ -2974,6 +2999,7 @@ export function createPlaybookCaptainShell(
|
|
|
2974
2999
|
PlaybookCaptainDeps['createCaptainRuntime']
|
|
2975
3000
|
> = deps.createCaptainRuntime ?? createDefaultCaptainRuntime;
|
|
2976
3001
|
const unresolvedEffectSettlement = deps.unresolvedEffectSettlement;
|
|
3002
|
+
const continuity = deps.continuity;
|
|
2977
3003
|
let pendingHostCapabilities = deps.hostCapabilities;
|
|
2978
3004
|
let currentEffectLedger = () => emptyPlaybookEffectLedger();
|
|
2979
3005
|
// The returned shell must not retain the caller's aggregate dependency
|
|
@@ -4362,16 +4388,37 @@ export function createPlaybookCaptainShell(
|
|
|
4362
4388
|
try {
|
|
4363
4389
|
let rawResult: unknown;
|
|
4364
4390
|
try {
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4391
|
+
const call = async (resume: string | false): Promise<unknown> => {
|
|
4392
|
+
await continuity?.beforeCall(binding.playerId);
|
|
4393
|
+
signal.throwIfAborted();
|
|
4394
|
+
const raw = await trackHostCall(
|
|
4395
|
+
frame,
|
|
4396
|
+
classifySettingsCall(() =>
|
|
4397
|
+
context.callPlayer(binding.playerId, prompt, { resume, settings }),
|
|
4398
|
+
),
|
|
4399
|
+
);
|
|
4400
|
+
hostResolved = true;
|
|
4401
|
+
return raw;
|
|
4402
|
+
};
|
|
4403
|
+
if (options.resume === false) {
|
|
4404
|
+
await continuity?.reset(binding.playerId, 'missing_hint');
|
|
4405
|
+
}
|
|
4406
|
+
rawResult = await call(options.resume);
|
|
4407
|
+
normalizeHostPlayerResult(rawResult, binding.playerId);
|
|
4408
|
+
if (typeof options.resume === 'string' && options.resume.length > 0 &&
|
|
4409
|
+
Object.getOwnPropertyDescriptor(rawResult as object, 'errorCode')?.value ===
|
|
4410
|
+
'SESSION_RESUME_REJECTED') {
|
|
4411
|
+
if (playerTransactions.get(binding.playerId) !== calling ||
|
|
4412
|
+
calling.abandoned || signal.aborted || activeTurn !== admittedTurn ||
|
|
4413
|
+
frame.playerCallScope !== scope || !frames.includes(frame)) {
|
|
4414
|
+
signal.throwIfAborted();
|
|
4415
|
+
throw new Error(`${frameLabel(frame)} player rejection arrived after its runtime operation ended`);
|
|
4416
|
+
}
|
|
4417
|
+
delete ledger.resumeToken;
|
|
4418
|
+
await continuity?.reset(binding.playerId, 'rejected_hint');
|
|
4419
|
+
hostResolved = false;
|
|
4420
|
+
rawResult = await call(false);
|
|
4421
|
+
}
|
|
4375
4422
|
} catch (error) {
|
|
4376
4423
|
if (error instanceof AgentSettingsPreflightError) {
|
|
4377
4424
|
if (
|
|
@@ -4725,6 +4772,9 @@ export function createPlaybookCaptainShell(
|
|
|
4725
4772
|
try {
|
|
4726
4773
|
if (resumeToken === undefined) delete ledger.resumeToken;
|
|
4727
4774
|
else ledger.resumeToken = resumeToken;
|
|
4775
|
+
if (pending.status === 'ok' && resumeToken !== undefined) {
|
|
4776
|
+
continuity?.acknowledged(binding.playerId, resumeToken);
|
|
4777
|
+
}
|
|
4728
4778
|
} finally {
|
|
4729
4779
|
playerTransactions.delete(binding.playerId);
|
|
4730
4780
|
}
|
|
@@ -5177,6 +5227,12 @@ export function createPlaybookCaptainShell(
|
|
|
5177
5227
|
childSessionId: frame.sessionId,
|
|
5178
5228
|
state: result.state,
|
|
5179
5229
|
...(result.output !== undefined ? { output: result.output } : {}),
|
|
5230
|
+
// DR-048: the child runtime read this record from its own artifact;
|
|
5231
|
+
// the host relays it unchanged so the caller's bridge can route a
|
|
5232
|
+
// failure terminal without knowing the callee's output fields.
|
|
5233
|
+
...(result.terminal !== undefined
|
|
5234
|
+
? { terminal: result.terminal }
|
|
5235
|
+
: {}),
|
|
5180
5236
|
};
|
|
5181
5237
|
}
|
|
5182
5238
|
if (result.outcome === 'aborted') {
|
|
@@ -6141,7 +6197,7 @@ export function createPlaybookCaptainShell(
|
|
|
6141
6197
|
class CaptainContinuityError extends Error {
|
|
6142
6198
|
constructor(cause: unknown) {
|
|
6143
6199
|
super(
|
|
6144
|
-
'the session Captain conversation
|
|
6200
|
+
'the session Captain conversation lost continuity',
|
|
6145
6201
|
{ cause },
|
|
6146
6202
|
);
|
|
6147
6203
|
this.name = 'CaptainContinuityError';
|
|
@@ -6165,8 +6221,11 @@ export function createPlaybookCaptainShell(
|
|
|
6165
6221
|
finalText?: string;
|
|
6166
6222
|
resumeToken?: string;
|
|
6167
6223
|
error?: string;
|
|
6224
|
+
errorCode?: 'SESSION_RESUME_REJECTED';
|
|
6168
6225
|
}> => {
|
|
6169
6226
|
const queued = captainQueue.add(async () => {
|
|
6227
|
+
context.signal.throwIfAborted();
|
|
6228
|
+
await continuity?.beforeCall('captain');
|
|
6170
6229
|
context.signal.throwIfAborted();
|
|
6171
6230
|
attempt.providerBoundaryEntered = true;
|
|
6172
6231
|
const result = await classifySettingsCall(() =>
|
|
@@ -6185,14 +6244,12 @@ export function createPlaybookCaptainShell(
|
|
|
6185
6244
|
finalText?: string;
|
|
6186
6245
|
resumeToken?: string;
|
|
6187
6246
|
error?: string;
|
|
6247
|
+
errorCode?: 'SESSION_RESUME_REJECTED';
|
|
6188
6248
|
}>;
|
|
6189
6249
|
};
|
|
6190
6250
|
|
|
6191
|
-
//
|
|
6192
|
-
//
|
|
6193
|
-
// seeded with the reseed digest plus the current ControlView digest. A
|
|
6194
|
-
// conversation that is owed a reseed carries the digest on its very next
|
|
6195
|
-
// call, so the turn after a failed reseed starts seeded rather than blank.
|
|
6251
|
+
// Only proven pre-execution rejection permits an immediate fresh call.
|
|
6252
|
+
// Other continuity loss leaves the journal reseed for the next Boss turn.
|
|
6196
6253
|
const durableCall = async (
|
|
6197
6254
|
context: CaptainContext,
|
|
6198
6255
|
compose: (options: { reseedDigest?: string }) => string,
|
|
@@ -6209,10 +6266,11 @@ export function createPlaybookCaptainShell(
|
|
|
6209
6266
|
const representedJournalSeq = journalSeq;
|
|
6210
6267
|
const firstAttempt = { providerBoundaryEntered: false };
|
|
6211
6268
|
let result:
|
|
6212
|
-
| { status: string; finalText?: string; resumeToken?: string; error?: string }
|
|
6269
|
+
| { status: string; finalText?: string; resumeToken?: string; error?: string; errorCode?: 'SESSION_RESUME_REJECTED' }
|
|
6213
6270
|
| undefined;
|
|
6214
6271
|
let failure: unknown;
|
|
6215
6272
|
try {
|
|
6273
|
+
if (resume === false) await continuity?.reset('captain', 'missing_hint');
|
|
6216
6274
|
result = await rawDurableCall(
|
|
6217
6275
|
context,
|
|
6218
6276
|
compose(
|
|
@@ -6259,9 +6317,10 @@ export function createPlaybookCaptainShell(
|
|
|
6259
6317
|
failure !== undefined ||
|
|
6260
6318
|
result === undefined ||
|
|
6261
6319
|
result.status !== 'ok' ||
|
|
6262
|
-
result.resumeToken ===
|
|
6320
|
+
typeof result.resumeToken !== 'string' || result.resumeToken.trim().length === 0;
|
|
6263
6321
|
if (!unsynchronized) {
|
|
6264
6322
|
conversation = { kind: 'pinned', token: result!.resumeToken! };
|
|
6323
|
+
continuity?.acknowledged('captain', result!.resumeToken!);
|
|
6265
6324
|
if (activeTurn) {
|
|
6266
6325
|
activeTurn.captainSyncedJournalSeq = representedJournalSeq;
|
|
6267
6326
|
}
|
|
@@ -6277,9 +6336,16 @@ export function createPlaybookCaptainShell(
|
|
|
6277
6336
|
// stays `needsSeeding` until a call comes back with a token, so a reseed
|
|
6278
6337
|
// that itself fails leaves the obligation standing for the next turn.
|
|
6279
6338
|
conversation = { kind: 'needsSeeding' };
|
|
6339
|
+
if (typeof resume !== 'string' || resume.length === 0 ||
|
|
6340
|
+
result?.status !== 'error' || result.errorCode !== 'SESSION_RESUME_REJECTED') {
|
|
6341
|
+
throw markControlFailure(new CaptainContinuityError(
|
|
6342
|
+
failure ?? result?.error ?? 'callCaptain did not establish continuation',
|
|
6343
|
+
));
|
|
6344
|
+
}
|
|
6345
|
+
await continuity?.reset('captain', 'rejected_hint');
|
|
6280
6346
|
const recap = reseedDigest();
|
|
6281
6347
|
let reissued:
|
|
6282
|
-
| { status: string; finalText?: string; resumeToken?: string; error?: string }
|
|
6348
|
+
| { status: string; finalText?: string; resumeToken?: string; error?: string; errorCode?: 'SESSION_RESUME_REJECTED' }
|
|
6283
6349
|
| undefined;
|
|
6284
6350
|
const reissueAttempt = { providerBoundaryEntered: false };
|
|
6285
6351
|
try {
|
|
@@ -6299,7 +6365,7 @@ export function createPlaybookCaptainShell(
|
|
|
6299
6365
|
}
|
|
6300
6366
|
throw markControlFailure(new CaptainContinuityError(error));
|
|
6301
6367
|
}
|
|
6302
|
-
if (reissued.status !== 'ok' || reissued.resumeToken ===
|
|
6368
|
+
if (reissued.status !== 'ok' || typeof reissued.resumeToken !== 'string' || reissued.resumeToken.trim().length === 0) {
|
|
6303
6369
|
throw markControlFailure(
|
|
6304
6370
|
new CaptainContinuityError(
|
|
6305
6371
|
reissued.error ??
|
|
@@ -6308,6 +6374,7 @@ export function createPlaybookCaptainShell(
|
|
|
6308
6374
|
);
|
|
6309
6375
|
}
|
|
6310
6376
|
conversation = { kind: 'pinned', token: reissued.resumeToken };
|
|
6377
|
+
continuity?.acknowledged('captain', reissued.resumeToken);
|
|
6311
6378
|
if (activeTurn) activeTurn.captainSyncedJournalSeq = journalSeq;
|
|
6312
6379
|
return {
|
|
6313
6380
|
...(reissued.finalText !== undefined
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
|
+
|
|
4
|
+
import type { PlaybookCaptainShell, PlaybookCaptainShellSnapshot } from './playbook-captain.js';
|
|
5
|
+
import type { PlaybookEffectLedger } from './host-capabilities.js';
|
|
6
|
+
import type { PlaybookSessionLifecycle, SessionExecutionProjection, SessionStructuralProjection, SessionFreshBoundary, SessionRecovery } from './session-store.js';
|
|
7
|
+
|
|
8
|
+
export interface SessionHost {
|
|
9
|
+
readonly shell: PlaybookCaptainShell;
|
|
10
|
+
readonly host: { runBossTurn(input: string): Promise<void>; abortActiveTurn(): void; dispose(): Promise<void>; [key: string]: any };
|
|
11
|
+
readonly snapshot: PlaybookCaptainShellSnapshot;
|
|
12
|
+
reconcileRepositoryEffects(): Promise<PlaybookEffectLedger>;
|
|
13
|
+
}
|
|
14
|
+
export interface SessionHostOptions {
|
|
15
|
+
readonly config: SessionExecutionProjection;
|
|
16
|
+
readonly sessionId?: string;
|
|
17
|
+
readonly cwd: string;
|
|
18
|
+
readonly sessionLease: PlaybookSessionLifecycle;
|
|
19
|
+
readonly loadModule: (specifier: string) => Promise<any>;
|
|
20
|
+
readonly restoreSnapshot?: PlaybookCaptainShellSnapshot;
|
|
21
|
+
readonly reconcileUncertainTurnReplay?: boolean;
|
|
22
|
+
readonly signal?: AbortSignal;
|
|
23
|
+
readonly [key: string]: any;
|
|
24
|
+
}
|
|
25
|
+
export declare function createCaptainSessionHost(options: SessionHostOptions): Promise<SessionHost>;
|
|
26
|
+
export declare function installRetainedGenerationsForLaunch(options: {
|
|
27
|
+
lease: PlaybookSessionLifecycle;
|
|
28
|
+
shell: PlaybookCaptainShell;
|
|
29
|
+
freshBoundary?: SessionFreshBoundary;
|
|
30
|
+
onFreshRecord?: (record: SessionRecovery | undefined) => void;
|
|
31
|
+
onLegacyRecord?: (record: any) => void | Promise<void>;
|
|
32
|
+
onInvalidRecord?: (record: any) => void | Promise<void>;
|
|
33
|
+
retainedGenerations?: Readonly<Record<string, any>>;
|
|
34
|
+
reconcileRepositoryEffects?: () => Promise<PlaybookEffectLedger>;
|
|
35
|
+
}): Promise<Readonly<Record<string, any>>>;
|
|
36
|
+
export declare function executionConfigFromPlan(plan: any): SessionExecutionProjection;
|
|
37
|
+
export declare function validateFrozenExecutionConfig(structural: SessionStructuralProjection, execution: SessionExecutionProjection, dependencies: { loadModule: (specifier: string) => Promise<any>; prepareRegistryModule?: (request: {id: string; from: string; authoredFrom: string}) => Promise<string | void> }): Promise<SessionExecutionProjection>;
|
|
38
|
+
export declare function driveHeadlessCaptainTurn(options: any): Promise<any>;
|
|
39
|
+
export declare function normalizeLaunchPlan(top: any, options?: any): Promise<any>;
|
|
40
|
+
export declare function loadLaunchPlan(options: any): Promise<any>;
|
|
41
|
+
export declare function composeGenericConfig(top: any, loadModule: (specifier: string) => Promise<any>, configPath?: string): Promise<any>;
|
|
42
|
+
export declare function projectTmuxConfig(plan: any): any;
|
|
43
|
+
export declare function resolveLaunchSessionsDir(options: any): string;
|
|
44
|
+
|
|
45
|
+
import type { SharedSessionStore, ReplayStreamStatus, ReplayStreamEntry, SessionGraph } from './session-store.js';
|
|
46
|
+
export interface OpenSessionHostOptions {
|
|
47
|
+
readonly store?: SharedSessionStore;
|
|
48
|
+
readonly sessionsDir?: string;
|
|
49
|
+
readonly sessionId?: string;
|
|
50
|
+
readonly mode?: 'new' | 'continue' | 'retry';
|
|
51
|
+
readonly cwd?: string;
|
|
52
|
+
readonly config?: SessionExecutionProjection;
|
|
53
|
+
readonly plan?: any;
|
|
54
|
+
readonly loadModule?: (specifier: string) => Promise<any>;
|
|
55
|
+
readonly prepareRegistryModule?: (request: {id: string; from: string; authoredFrom: string}) => Promise<string | void>;
|
|
56
|
+
readonly observers?: readonly { onRecord?(record: any): void | Promise<void> }[];
|
|
57
|
+
readonly onStoredRecord?: (record: ReplayStreamEntry, status: ReplayStreamStatus) => void | Promise<void>;
|
|
58
|
+
readonly onCheckpoint?: (record: SessionRecovery) => void | Promise<void>;
|
|
59
|
+
readonly onIncomplete?: () => void | Promise<void>;
|
|
60
|
+
readonly graphs?: readonly { playbookId: string; graph: SessionGraph | null }[];
|
|
61
|
+
readonly initialVisible?: readonly string[];
|
|
62
|
+
readonly [key: string]: any;
|
|
63
|
+
}
|
|
64
|
+
export interface SessionHostController {
|
|
65
|
+
readonly sessionId: string;
|
|
66
|
+
readonly host: SessionHost['host'];
|
|
67
|
+
readonly shell: PlaybookCaptainShell;
|
|
68
|
+
readonly lease: PlaybookSessionLifecycle;
|
|
69
|
+
read(): Promise<SessionRecovery | undefined>;
|
|
70
|
+
handleBossTurn(input: string): Promise<SessionRecovery>;
|
|
71
|
+
retry(): Promise<SessionRecovery>;
|
|
72
|
+
dispose(): Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
export declare function openSessionHost(options: OpenSessionHostOptions): Promise<SessionHostController>;
|
|
75
|
+
export declare function discardSessionUncertain(store: SharedSessionStore, sessionId: string): Promise<SessionRecovery | undefined>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
createCaptainSessionHost,
|
|
6
|
+
installRetainedGenerationsForLaunch,
|
|
7
|
+
executionConfigFromPlan,
|
|
8
|
+
validateFrozenExecutionConfig,
|
|
9
|
+
driveHeadlessCaptainTurn,
|
|
10
|
+
} from './bin/run.js';
|
|
11
|
+
export {
|
|
12
|
+
normalizeLaunchPlan,
|
|
13
|
+
loadLaunchPlan,
|
|
14
|
+
composeGenericConfig,
|
|
15
|
+
projectTmuxConfig,
|
|
16
|
+
resolveLaunchSessionsDir,
|
|
17
|
+
} from './bin/launch-config.js';
|
|
18
|
+
export { openSessionHost, discardSessionUncertain } from './bin/session-host.js';
|
|
@@ -48,7 +48,7 @@ export type ReplayStreamStatus =
|
|
|
48
48
|
export interface PlaybookSessionSummary {
|
|
49
49
|
readonly schemaVersion: number;
|
|
50
50
|
readonly sessionId: string;
|
|
51
|
-
readonly state: 'settled' | 'uncertain';
|
|
51
|
+
readonly state: 'settled' | 'uncertain' | 'history-only';
|
|
52
52
|
readonly cwd: string;
|
|
53
53
|
readonly updatedAt: string;
|
|
54
54
|
}
|
|
@@ -80,3 +80,178 @@ export interface PlaybookSessionLease {
|
|
|
80
80
|
streamStatus(): ReplayStreamStatus;
|
|
81
81
|
release(): Promise<ReplayStreamStatus>;
|
|
82
82
|
}
|
|
83
|
+
|
|
84
|
+
/** Recovery data is interpreted by the version-aware codec, not by live runtimes. */
|
|
85
|
+
export type SessionSnapshot = Readonly<Record<string, any>>;
|
|
86
|
+
export interface SessionEffectLedger {
|
|
87
|
+
readonly schemaVersion: 1;
|
|
88
|
+
readonly revision: number;
|
|
89
|
+
readonly boundaries: readonly Readonly<Record<string, any>>[];
|
|
90
|
+
readonly logicalOperations: readonly Readonly<Record<string, any>>[];
|
|
91
|
+
}
|
|
92
|
+
export interface SessionUnresolvedEffect {
|
|
93
|
+
readonly classification: 'one-descendant-commit' | 'multiple-commits' |
|
|
94
|
+
'rewritten-or-non-descendant' | 'worktree-only-change' |
|
|
95
|
+
'concurrent-or-foreign-change' | 'observation-ambiguous' | 'incomplete';
|
|
96
|
+
readonly baselineHead: string;
|
|
97
|
+
readonly afterHead?: string;
|
|
98
|
+
readonly commitOid?: string;
|
|
99
|
+
}
|
|
100
|
+
export type SessionRetentionUpdate =
|
|
101
|
+
| { readonly kind: 'retain'; readonly rootPlaybookId: string; readonly generation: Readonly<Record<string, any>> }
|
|
102
|
+
| { readonly kind: 'clear'; readonly rootPlaybookId: string };
|
|
103
|
+
|
|
104
|
+
export interface SessionExecutionProjection {
|
|
105
|
+
readonly schemaVersion: 2;
|
|
106
|
+
readonly captain: Readonly<Record<string, any>>;
|
|
107
|
+
readonly players: readonly Readonly<Record<string, any>>[];
|
|
108
|
+
readonly catalog: Readonly<Record<string, any>>;
|
|
109
|
+
}
|
|
110
|
+
export interface SessionStructuralProjection {
|
|
111
|
+
readonly schemaVersion: 1;
|
|
112
|
+
readonly captain: Readonly<Record<string, any>>;
|
|
113
|
+
readonly players: readonly Readonly<Record<string, any>>[];
|
|
114
|
+
readonly catalog: Readonly<Record<string, any>>;
|
|
115
|
+
}
|
|
116
|
+
export interface SessionRecovery {
|
|
117
|
+
readonly schemaVersion: 6;
|
|
118
|
+
readonly kind: 'captain-session';
|
|
119
|
+
readonly sessionId: string;
|
|
120
|
+
readonly cwd: string;
|
|
121
|
+
readonly createdAt: string;
|
|
122
|
+
readonly updatedAt: string;
|
|
123
|
+
readonly state: 'settled' | 'uncertain';
|
|
124
|
+
readonly structuralProjection: SessionStructuralProjection;
|
|
125
|
+
readonly lastAppliedExecutionProjection: SessionExecutionProjection;
|
|
126
|
+
readonly snapshot: SessionSnapshot;
|
|
127
|
+
readonly effectLedger: SessionEffectLedger;
|
|
128
|
+
readonly unresolvedEffects: readonly SessionUnresolvedEffect[];
|
|
129
|
+
readonly retainedGenerations?: Readonly<Record<string, any>>;
|
|
130
|
+
readonly uncertain?: {
|
|
131
|
+
readonly input: string;
|
|
132
|
+
readonly attemptId: string;
|
|
133
|
+
readonly attemptNumber: number;
|
|
134
|
+
readonly baseUpdatedAt: string | null;
|
|
135
|
+
readonly markedAt: string;
|
|
136
|
+
readonly attemptedExecutionProjection: SessionExecutionProjection;
|
|
137
|
+
readonly abandonment?: Readonly<Record<string, any>>;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export interface SessionReplayCheckpoint {
|
|
141
|
+
readonly seq: number;
|
|
142
|
+
readonly sha256: string;
|
|
143
|
+
readonly incomplete: boolean;
|
|
144
|
+
}
|
|
145
|
+
export type SessionManifest = Omit<SessionRecovery, 'schemaVersion'> & {
|
|
146
|
+
readonly schemaVersion: 7;
|
|
147
|
+
readonly replay: SessionReplayCheckpoint;
|
|
148
|
+
readonly contextSeq: number;
|
|
149
|
+
} | {
|
|
150
|
+
readonly schemaVersion: 7;
|
|
151
|
+
readonly kind: 'captain-session';
|
|
152
|
+
readonly state: 'history-only';
|
|
153
|
+
readonly sessionId: string;
|
|
154
|
+
readonly cwd: string;
|
|
155
|
+
readonly createdAt: string;
|
|
156
|
+
readonly updatedAt: string;
|
|
157
|
+
readonly replay: SessionReplayCheckpoint;
|
|
158
|
+
readonly contextSeq: number | null;
|
|
159
|
+
readonly reason: string;
|
|
160
|
+
};
|
|
161
|
+
export type StoredSessionManifest = SessionManifest | {
|
|
162
|
+
readonly schemaVersion: number;
|
|
163
|
+
readonly sessionId: string;
|
|
164
|
+
readonly cwd?: unknown;
|
|
165
|
+
readonly [key: string]: unknown;
|
|
166
|
+
};
|
|
167
|
+
export interface SessionGraph {
|
|
168
|
+
readonly initial: string;
|
|
169
|
+
readonly nodes: readonly { readonly id: string; readonly kind: 'state' | 'final'; readonly tags: readonly string[]; readonly parent?: string; readonly role?: string; readonly description?: string }[];
|
|
170
|
+
readonly edges: readonly { readonly id: string; readonly from: string; readonly to: string; readonly event: string }[];
|
|
171
|
+
}
|
|
172
|
+
export interface SessionContext {
|
|
173
|
+
readonly type: 'session_context';
|
|
174
|
+
readonly timestamp: number;
|
|
175
|
+
readonly contextVersion: 1;
|
|
176
|
+
readonly captainId: string;
|
|
177
|
+
readonly configuration: SessionExecutionProjection;
|
|
178
|
+
readonly graphs: readonly { readonly playbookId: string; readonly graph: SessionGraph | null }[];
|
|
179
|
+
readonly initialVisible: readonly string[];
|
|
180
|
+
}
|
|
181
|
+
export interface SessionHistory extends ReplayStreamReadResult {
|
|
182
|
+
readonly synthetic?: true;
|
|
183
|
+
readonly missing: boolean;
|
|
184
|
+
readonly incomplete: boolean;
|
|
185
|
+
readonly pendingTail: boolean;
|
|
186
|
+
readonly damage?: { readonly seq: number; readonly offset: number; readonly reason: string };
|
|
187
|
+
readonly digests: readonly string[];
|
|
188
|
+
readonly completeBytes: number;
|
|
189
|
+
}
|
|
190
|
+
export interface SessionValidation {
|
|
191
|
+
readonly integrityValid: boolean;
|
|
192
|
+
readonly sessionId: string;
|
|
193
|
+
readonly resumable: boolean;
|
|
194
|
+
readonly reasons: readonly string[];
|
|
195
|
+
readonly manifest: StoredSessionManifest;
|
|
196
|
+
readonly history: SessionHistory;
|
|
197
|
+
}
|
|
198
|
+
export interface SessionHints {
|
|
199
|
+
readonly players: Readonly<Record<string, string>>;
|
|
200
|
+
readonly captain?: { readonly kind: 'pinned'; readonly token: string } | { readonly kind: 'needsCatchUp'; readonly resume: string | false; readonly afterJournalSeq: number };
|
|
201
|
+
}
|
|
202
|
+
export interface SessionFreshBoundary {
|
|
203
|
+
readonly cwd: string;
|
|
204
|
+
readonly structuralProjection: SessionStructuralProjection;
|
|
205
|
+
readonly executionProjection: SessionExecutionProjection;
|
|
206
|
+
readonly snapshot: SessionSnapshot;
|
|
207
|
+
readonly context?: SessionContext;
|
|
208
|
+
readonly onLegacyRecord?: (record: any) => void | Promise<void>;
|
|
209
|
+
readonly onInvalidRecord?: (record: any) => void | Promise<void>;
|
|
210
|
+
}
|
|
211
|
+
export interface PlaybookSessionLifecycle extends PlaybookSessionLease {
|
|
212
|
+
read(): Promise<SessionRecovery | undefined>;
|
|
213
|
+
readManifest(): Promise<StoredSessionManifest>;
|
|
214
|
+
initializeSettledWithPredecessor(options: SessionFreshBoundary): Promise<SessionRecovery>;
|
|
215
|
+
abandonFreshSettled(options: { expected: SessionRecovery }): Promise<boolean>;
|
|
216
|
+
beginTurn(options: { input: string; attemptId: string; attemptedExecutionProjection: SessionExecutionProjection }): Promise<SessionRecovery>;
|
|
217
|
+
beginRetry(options: { expectedAttemptId: string; nextAttemptId: string }): Promise<SessionRecovery>;
|
|
218
|
+
settle(options: { attemptId: string; snapshot: SessionSnapshot; unresolvedEffects: readonly SessionUnresolvedEffect[]; retentionUpdates?: readonly SessionRetentionUpdate[] }): Promise<SessionRecovery>;
|
|
219
|
+
discard(options: { attemptId: string }): Promise<SessionRecovery | undefined>;
|
|
220
|
+
beginUnresolvedEffectAbandonment(options: any): Promise<any>;
|
|
221
|
+
completeUnresolvedEffectAbandonment(options: any): Promise<any>;
|
|
222
|
+
recoverUnresolvedEffectAbandonment(): Promise<SessionRecovery | undefined>;
|
|
223
|
+
writeEffectLedger(authority: any, commands: readonly Readonly<Record<string, any>>[]): Promise<SessionEffectLedger>;
|
|
224
|
+
assertOwner(): Promise<any>;
|
|
225
|
+
assertContinuable(context?: { cwd?: string; executionProjection?: SessionExecutionProjection }): Promise<SessionValidation>;
|
|
226
|
+
recordContext(context: SessionContext): Promise<number>;
|
|
227
|
+
consumeHints(): Promise<SessionHints>;
|
|
228
|
+
acknowledgeHint(participantId: string, token: string): void;
|
|
229
|
+
clearHint(participantId: string): void;
|
|
230
|
+
}
|
|
231
|
+
export interface SharedSessionStore {
|
|
232
|
+
readonly sessionsDir: string;
|
|
233
|
+
prepare(): Promise<void>;
|
|
234
|
+
read(sessionId: string): Promise<SessionRecovery>;
|
|
235
|
+
readManifest(sessionId: string): Promise<StoredSessionManifest>;
|
|
236
|
+
/** Observation only; mutations still require acquiring the lease. */
|
|
237
|
+
readLeaseState(sessionId: string): Promise<'active' | 'idle' | 'unknown'>;
|
|
238
|
+
readHistory(sessionId: string, options?: ReplayStreamReadOptions): Promise<SessionHistory>;
|
|
239
|
+
readStream(sessionId: string, options?: ReplayStreamReadOptions): Promise<ReplayStreamReadResult>;
|
|
240
|
+
readSummary(sessionId: string): Promise<PlaybookSessionSummary>;
|
|
241
|
+
listSummaries(): Promise<PlaybookSessionListResult>;
|
|
242
|
+
latest(options?: { preferredCwd?: string; onLegacyRecord?: (record: any) => void | Promise<void> }): Promise<SessionRecovery>;
|
|
243
|
+
acquire(sessionId: string): Promise<PlaybookSessionLifecycle>;
|
|
244
|
+
acquireManagement(sessionId: string): Promise<{ readonly sessionId: string; readonly ownerToken: string; assertOwner(): Promise<unknown>; release(): Promise<void> }>;
|
|
245
|
+
validate(sessionId: string, context?: { cwd?: string; executionProjection?: SessionExecutionProjection }): Promise<SessionValidation>;
|
|
246
|
+
delete(sessionId: string): Promise<void>;
|
|
247
|
+
migrateLegacyDefault(options?: { env?: Readonly<Record<string, string | undefined>>; homeDir?: string }): Promise<{ sourceDir: string; migrated: readonly string[]; skipped: readonly { sessionId: string; reason: string }[] }>;
|
|
248
|
+
migrate(sessionId: string, options?: { sourcePath?: string; cwd?: string; backupDir?: string }): Promise<{ manifest: SessionManifest; migrated: boolean; reasons: readonly string[] }>;
|
|
249
|
+
}
|
|
250
|
+
export declare function createSessionStore(options?: { sessionsDir?: string; env?: Readonly<Record<string, string | undefined>>; homeDir?: string; [key: string]: any }): SharedSessionStore;
|
|
251
|
+
export declare function validateSessionManifest(value: unknown): SessionManifest;
|
|
252
|
+
export declare function validateSessionContext(value: unknown): SessionContext;
|
|
253
|
+
export declare function projectCaptainSessionStructure(value: SessionExecutionProjection): SessionStructuralProjection;
|
|
254
|
+
export declare function validateCaptainSessionExecutionProjection(value: unknown): SessionExecutionProjection;
|
|
255
|
+
export declare function validateCaptainSessionStructuralProjection(value: unknown): SessionStructuralProjection;
|
|
256
|
+
export declare function assertCaptainSessionExecutionCompatible(structural: SessionStructuralProjection, execution: SessionExecutionProjection): SessionExecutionProjection;
|
|
257
|
+
export declare function attachSessionHints(snapshot: SessionSnapshot, hints: SessionHints): SessionSnapshot;
|
|
@@ -21,14 +21,17 @@ export function openSessionStore(sessionsDir) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function wrapStore(store) {
|
|
24
|
+
let preparation;
|
|
25
|
+
const prepared = async (operation) => {
|
|
26
|
+
await (preparation ??= store.prepare());
|
|
27
|
+
return operation();
|
|
28
|
+
};
|
|
24
29
|
return Object.freeze({
|
|
25
30
|
sessionsDir: store.sessionsDir,
|
|
26
|
-
list: async () => projectListResult(await store.listSummaries()),
|
|
27
|
-
read: async (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
projectReadResult(await store.readStream(sessionId, options)),
|
|
31
|
-
acquire: async (sessionId) => wrapLease(await store.acquire(sessionId)),
|
|
31
|
+
list: () => prepared(async () => projectListResult(await store.listSummaries())),
|
|
32
|
+
read: (sessionId) => prepared(async () => projectSummary(await store.readSummary(sessionId))),
|
|
33
|
+
readStream: (sessionId, options) => prepared(async () => projectReadResult(await store.readStream(sessionId, options))),
|
|
34
|
+
acquire: (sessionId) => prepared(async () => wrapLease(await store.acquire(sessionId))),
|
|
32
35
|
});
|
|
33
36
|
}
|
|
34
37
|
|
|
@@ -111,3 +114,16 @@ function projectStatus(status) {
|
|
|
111
114
|
incomplete: status.incomplete,
|
|
112
115
|
});
|
|
113
116
|
}
|
|
117
|
+
|
|
118
|
+
export {
|
|
119
|
+
createCaptainSessionStore as createSessionStore,
|
|
120
|
+
projectCaptainSessionStructure,
|
|
121
|
+
validateCaptainSessionExecutionProjection,
|
|
122
|
+
validateCaptainSessionStructuralProjection,
|
|
123
|
+
assertCaptainSessionExecutionCompatible,
|
|
124
|
+
} from './bin/session-store.js';
|
|
125
|
+
export {
|
|
126
|
+
validateSessionManifest,
|
|
127
|
+
validateSessionContext,
|
|
128
|
+
attachSessionHints,
|
|
129
|
+
} from './bin/portable-codec.js';
|
|
@@ -960,6 +960,9 @@ export const decideMachine = setup({
|
|
|
960
960
|
playbook: {
|
|
961
961
|
stateId: 'reportedReviewFailure',
|
|
962
962
|
description: 'DECIDE reports REVIEW’s failure and its last commit.',
|
|
963
|
+
// DR-048: this final state means the workflow failed, so a caller
|
|
964
|
+
// learns that from the machine rather than from DECIDE's output.
|
|
965
|
+
terminal: 'failure',
|
|
963
966
|
},
|
|
964
967
|
},
|
|
965
968
|
},
|
|
@@ -971,6 +974,7 @@ export const decideMachine = setup({
|
|
|
971
974
|
playbook: {
|
|
972
975
|
stateId: 'done',
|
|
973
976
|
description: 'DECIDE completed with an approved commit.',
|
|
977
|
+
terminal: 'success',
|
|
974
978
|
},
|
|
975
979
|
},
|
|
976
980
|
},
|
|
@@ -1233,6 +1233,9 @@ export const decideMachine = setup({
|
|
|
1233
1233
|
playbook: {
|
|
1234
1234
|
stateId: 'reportedReviewFailure',
|
|
1235
1235
|
description: 'DECIDE reports REVIEW’s failure and its last commit.',
|
|
1236
|
+
// DR-048: this final state means the workflow failed, so a caller
|
|
1237
|
+
// learns that from the machine rather than from DECIDE's output.
|
|
1238
|
+
terminal: 'failure',
|
|
1236
1239
|
},
|
|
1237
1240
|
},
|
|
1238
1241
|
},
|
|
@@ -1244,6 +1247,7 @@ export const decideMachine = setup({
|
|
|
1244
1247
|
playbook: {
|
|
1245
1248
|
stateId: 'done',
|
|
1246
1249
|
description: 'DECIDE completed with an approved commit.',
|
|
1250
|
+
terminal: 'success',
|
|
1247
1251
|
},
|
|
1248
1252
|
},
|
|
1249
1253
|
},
|