@sublang/playbook 14.0.0 → 14.1.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 +14 -14
- package/package.json +1 -1
- package/reference/sdlc/branch.playbook/branch.fsm.d.ts +1 -1
- package/reference/sdlc/code.playbook/bin/repository-effects.js +187 -14
- package/reference/sdlc/code.playbook/code.fsm.d.ts +1 -1
- package/reference/sdlc/code.playbook/host-capabilities.d.ts +6 -0
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +3 -1
- package/reference/sdlc/code.playbook/playbook-captain.js +357 -16
- package/reference/sdlc/code.playbook/playbook-captain.ts +459 -34
- package/reference/sdlc/code.playbook/session-host.d.ts +6 -3
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +1 -1
- package/reference/sdlc/decide.playbook/decide.playbook.js +25 -1
- package/reference/sdlc/decide.playbook/decide.playbook.ts +35 -2
- package/reference/sdlc/dev.md +15 -8
- package/reference/sdlc/dev.playbook/dev.fsm.d.ts +1 -1
- package/reference/sdlc/dev.playbook/dev.fsm.js +18 -12
- package/reference/sdlc/dev.playbook/dev.fsm.ts +18 -13
- package/reference/sdlc/dev.playbook/dev.gears.md +19 -12
- package/reference/sdlc/dev.playbook/dev.playbook.js +1 -1
- package/reference/sdlc/dev.playbook/dev.playbook.ts +1 -2
- package/reference/sdlc/dev.playbook/dev.registry.js +1 -1
- package/reference/sdlc/dev.playbook/dev.registry.ts +1 -1
- package/reference/sdlc/pr.playbook/pr.fsm.d.ts +1 -1
- package/reference/sdlc/review.playbook/review.fsm.d.ts +1 -1
- package/slc/link.md +32 -0
- package/src/runtime.d.ts +70 -0
- package/src/runtime.js +232 -1
- package/src/runtime.ts +358 -1
- package/src/xstate-playbook-runtime.d.ts +9 -10
- package/src/xstate-playbook-runtime.js +372 -20
- package/src/xstate-playbook-runtime.ts +460 -19
- package/src/xstate-runtime.d.ts +9 -1
- package/src/xstate-runtime.js +299 -19
- package/src/xstate-runtime.ts +362 -21
|
@@ -31,9 +31,11 @@ import {
|
|
|
31
31
|
createAcceptedOutcomeConsumer,
|
|
32
32
|
type AcceptedOutcomeReceipt,
|
|
33
33
|
} from './accepted-outcome.js';
|
|
34
|
+
import { assertPlaybookFailureCause } from './runtime.js';
|
|
34
35
|
import {
|
|
35
36
|
assertPlaybookRuntimeSnapshot,
|
|
36
37
|
assertPlaybookEffectLedger,
|
|
38
|
+
attachPlaybookFailureCause,
|
|
37
39
|
combineAbortSignals,
|
|
38
40
|
createNestedPlaybookBridge,
|
|
39
41
|
detachPersistedMachineSnapshot,
|
|
@@ -60,7 +62,9 @@ import type {
|
|
|
60
62
|
PlaybookEffectBoundaryStart,
|
|
61
63
|
PlaybookEffectLedger,
|
|
62
64
|
PlaybookEffectLedgerCapability,
|
|
65
|
+
NormalizedError,
|
|
63
66
|
PlaybookEffectLogicalOperation,
|
|
67
|
+
PlaybookFailureCause,
|
|
64
68
|
PlaybookPendingBossQuestion,
|
|
65
69
|
PlaybookPorts,
|
|
66
70
|
PlaybookRepositoryReceipt,
|
|
@@ -149,6 +153,12 @@ export interface RuntimeBoundaryCalls {
|
|
|
149
153
|
result: PlayerResult,
|
|
150
154
|
output: PlaybookActorOutput,
|
|
151
155
|
): void;
|
|
156
|
+
/**
|
|
157
|
+
* DR-063 §2: decorate the failure the bridge builds for a non-`ok` result
|
|
158
|
+
* with the cause the boundary decided at the call itself, where the role,
|
|
159
|
+
* the resolved player, and the reported error are known.
|
|
160
|
+
*/
|
|
161
|
+
markPlayerResultFailure?(error: Error): Error;
|
|
152
162
|
callJudge(
|
|
153
163
|
purpose: JudgePurpose,
|
|
154
164
|
stateId: string | undefined,
|
|
@@ -236,6 +246,18 @@ const fsmResultFailures = new WeakSet<object>();
|
|
|
236
246
|
|
|
237
247
|
function markFsmResultFailure(error: Error): Error {
|
|
238
248
|
fsmResultFailures.add(error);
|
|
249
|
+
// DR-063 §2: there is no failure without a cause. A failure the runtime
|
|
250
|
+
// marks with nothing more specific is a runtime defect carrying its own
|
|
251
|
+
// message; a decision site that knows better attaches its cause afterwards.
|
|
252
|
+
attachPlaybookFailureCause(error, {
|
|
253
|
+
code: 'runtime-defect',
|
|
254
|
+
evidence: {
|
|
255
|
+
reason:
|
|
256
|
+
typeof error.message === 'string' && error.message.length > 0
|
|
257
|
+
? error.message
|
|
258
|
+
: 'unknown runtime defect',
|
|
259
|
+
},
|
|
260
|
+
});
|
|
239
261
|
return error;
|
|
240
262
|
}
|
|
241
263
|
|
|
@@ -247,6 +269,74 @@ function isFsmResultFailure(error: unknown): boolean {
|
|
|
247
269
|
);
|
|
248
270
|
}
|
|
249
271
|
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
// DR-063 §1/§2: there is no failure without a cause. Each decision site builds
|
|
274
|
+
// the cause its own evidence states, the runtime attaches it to the error it
|
|
275
|
+
// marks as the FSM failure, and anything else the runtime marks becomes
|
|
276
|
+
// `runtime-defect` carrying its message as `reason`.
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
/** Adjudication reasons this runtime decides itself, and their codes. */
|
|
280
|
+
const GOVERNED_JUDGE_FAILURE_REASONS: readonly string[] = [
|
|
281
|
+
'judge transport failed',
|
|
282
|
+
'corrective judge failed',
|
|
283
|
+
'corrective semantic candidate is invalid',
|
|
284
|
+
'semantic correction budget is unavailable',
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
const ABORTED_FAILURE_CAUSE: PlaybookFailureCause = assertPlaybookFailureCause({
|
|
288
|
+
code: 'aborted',
|
|
289
|
+
evidence: {},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
function runtimeDefectCause(reason: string): PlaybookFailureCause {
|
|
293
|
+
return assertPlaybookFailureCause({
|
|
294
|
+
code: 'runtime-defect',
|
|
295
|
+
evidence: { reason: reason.length === 0 ? 'unknown runtime defect' : reason },
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function failureErrorEvidence(
|
|
300
|
+
error: unknown,
|
|
301
|
+
): { readonly name: string; readonly message: string } | undefined {
|
|
302
|
+
if (error === undefined || error === null) return undefined;
|
|
303
|
+
const { name, message } = normalizeError(error);
|
|
304
|
+
return { name, message };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** A string `code` the rejecting port reported, when it reported one. */
|
|
308
|
+
function failureErrorCode(error: unknown): string | undefined {
|
|
309
|
+
if (typeof error !== 'object' || error === null) return undefined;
|
|
310
|
+
try {
|
|
311
|
+
const code = (error as { code?: unknown }).code;
|
|
312
|
+
return typeof code === 'string' && code.length > 0 ? code : undefined;
|
|
313
|
+
} catch {
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function playerFailureCause(input: {
|
|
319
|
+
readonly roleId: string;
|
|
320
|
+
readonly playerId?: string;
|
|
321
|
+
readonly error: unknown;
|
|
322
|
+
}): PlaybookFailureCause {
|
|
323
|
+
return assertPlaybookFailureCause({
|
|
324
|
+
code: 'player-failed',
|
|
325
|
+
evidence: {
|
|
326
|
+
roleId: input.roleId,
|
|
327
|
+
...(input.playerId === undefined ? {} : { playerId: input.playerId }),
|
|
328
|
+
error: failureErrorEvidence(input.error) ?? {
|
|
329
|
+
name: 'Error',
|
|
330
|
+
message: 'Unknown error',
|
|
331
|
+
},
|
|
332
|
+
...(failureErrorCode(input.error) === undefined
|
|
333
|
+
? {}
|
|
334
|
+
: { errorCode: failureErrorCode(input.error)! }),
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
|
|
250
340
|
// ---------------------------------------------------------------------------
|
|
251
341
|
// DR-028: both call boundaries treat an `ok` result whose `finalText` is
|
|
252
342
|
// missing, empty, or whitespace-only under one empty predicate, and that
|
|
@@ -946,15 +1036,21 @@ export function parseJudgeJson(raw: string): unknown {
|
|
|
946
1036
|
|
|
947
1037
|
export function normalizeErrorCompact(
|
|
948
1038
|
err: unknown,
|
|
949
|
-
):
|
|
1039
|
+
): Omit<NormalizedError, 'stack'> | undefined {
|
|
950
1040
|
if (err === undefined || err === null) return undefined;
|
|
951
1041
|
const normalized = normalizeError(err);
|
|
952
|
-
return {
|
|
1042
|
+
return {
|
|
1043
|
+
name: normalized.name,
|
|
1044
|
+
message: normalized.message,
|
|
1045
|
+
// DR-063 §2: on a failure the cause is why the compact form exists, so it
|
|
1046
|
+
// survives where the stack does not.
|
|
1047
|
+
...(normalized.cause === undefined ? {} : { cause: normalized.cause }),
|
|
1048
|
+
};
|
|
953
1049
|
}
|
|
954
1050
|
|
|
955
1051
|
export function normalizeErrorFull(
|
|
956
1052
|
err: unknown,
|
|
957
|
-
):
|
|
1053
|
+
): NormalizedError | undefined {
|
|
958
1054
|
if (err === undefined || err === null) return undefined;
|
|
959
1055
|
return normalizeError(err);
|
|
960
1056
|
}
|
|
@@ -1090,6 +1186,98 @@ export function composePlayerContinuation(
|
|
|
1090
1186
|
return [...continuationBlocks(input, resuming), body].join('\n\n');
|
|
1091
1187
|
}
|
|
1092
1188
|
|
|
1189
|
+
// DR-062 §4: the Coder is told which uncommitted changes were already in the
|
|
1190
|
+
// worktree when its own call started. The block is composed from the call's
|
|
1191
|
+
// own baseline observation, names paths only, and is bounded.
|
|
1192
|
+
const PRE_EXISTING_CHANGE_GROUPS = [
|
|
1193
|
+
'staged',
|
|
1194
|
+
'modified',
|
|
1195
|
+
'deleted',
|
|
1196
|
+
'untracked',
|
|
1197
|
+
'renamed',
|
|
1198
|
+
'unmerged',
|
|
1199
|
+
'other',
|
|
1200
|
+
] as const;
|
|
1201
|
+
|
|
1202
|
+
type PreExistingChangeGroup = (typeof PRE_EXISTING_CHANGE_GROUPS)[number];
|
|
1203
|
+
|
|
1204
|
+
const PRE_EXISTING_CHANGE_PATH_LIMIT = 40;
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* The group one baseline projection entry belongs to. The shapes are the
|
|
1208
|
+
* porcelain-v2 records the repository observer projects: an `untracked`,
|
|
1209
|
+
* `rename-or-copy`, or `unmerged` kind, and otherwise an `ordinary` record
|
|
1210
|
+
* whose two-character `xy` carries the index state first and the worktree
|
|
1211
|
+
* state second.
|
|
1212
|
+
*/
|
|
1213
|
+
function preExistingChangeGroup(entry: unknown): PreExistingChangeGroup {
|
|
1214
|
+
if (!isPlainObject(entry)) return 'other';
|
|
1215
|
+
if (entry.kind === 'untracked') return 'untracked';
|
|
1216
|
+
if (entry.kind === 'rename-or-copy') return 'renamed';
|
|
1217
|
+
if (entry.kind === 'unmerged') return 'unmerged';
|
|
1218
|
+
const xy = entry.xy;
|
|
1219
|
+
if (entry.kind !== 'ordinary' || typeof xy !== 'string' || xy.length !== 2) {
|
|
1220
|
+
return 'other';
|
|
1221
|
+
}
|
|
1222
|
+
if (xy[1] === 'D') return 'deleted';
|
|
1223
|
+
if (xy[1] === '.') return xy[0] === '.' ? 'other' : 'staged';
|
|
1224
|
+
return 'modified';
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* The pre-existing-changes block appended to an effect-authorized player
|
|
1229
|
+
* prompt, or `undefined` for an empty baseline projection.
|
|
1230
|
+
*/
|
|
1231
|
+
function preExistingChangesBlock(
|
|
1232
|
+
baseline: PlaybookRepositoryReceipt['baseline'] | undefined,
|
|
1233
|
+
): string | undefined {
|
|
1234
|
+
const projection = baseline?.projection;
|
|
1235
|
+
if (!isPlainObject(projection)) return undefined;
|
|
1236
|
+
const grouped = new Map<PreExistingChangeGroup, string[]>();
|
|
1237
|
+
let total = 0;
|
|
1238
|
+
for (const path of Object.keys(projection)) {
|
|
1239
|
+
const group = preExistingChangeGroup(projection[path]);
|
|
1240
|
+
const paths = grouped.get(group);
|
|
1241
|
+
if (paths === undefined) grouped.set(group, [path]);
|
|
1242
|
+
else paths.push(path);
|
|
1243
|
+
total += 1;
|
|
1244
|
+
}
|
|
1245
|
+
if (total === 0) return undefined;
|
|
1246
|
+
const omitted = Math.max(0, total - PRE_EXISTING_CHANGE_PATH_LIMIT);
|
|
1247
|
+
const lines: string[] = [];
|
|
1248
|
+
let budget = PRE_EXISTING_CHANGE_PATH_LIMIT;
|
|
1249
|
+
for (const group of PRE_EXISTING_CHANGE_GROUPS) {
|
|
1250
|
+
const paths = grouped.get(group);
|
|
1251
|
+
if (paths === undefined || budget <= 0) continue;
|
|
1252
|
+
const printed = paths.slice().sort().slice(0, budget);
|
|
1253
|
+
budget -= printed.length;
|
|
1254
|
+
lines.push(`> - ${group}: ${printed.join(', ')}`);
|
|
1255
|
+
}
|
|
1256
|
+
if (omitted > 0 && lines.length > 0) {
|
|
1257
|
+
lines[lines.length - 1] += `, … and ${omitted} more`;
|
|
1258
|
+
}
|
|
1259
|
+
return [
|
|
1260
|
+
'> Uncommitted changes present before this call, belonging to the Boss:',
|
|
1261
|
+
...lines,
|
|
1262
|
+
"> Leave them exactly as they are unless the task or the Boss's request requires building on them; never revert or delete them; when you commit any of them, name them in your final report.",
|
|
1263
|
+
].join('\n');
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* The block for one governed call, or `undefined` where the call is not
|
|
1268
|
+
* effect-authorized — a call whose declared outcomes carry no
|
|
1269
|
+
* `one-descendant-commit` disposition may commit nothing, so it is told
|
|
1270
|
+
* nothing.
|
|
1271
|
+
*/
|
|
1272
|
+
function effectAuthorizedPreExistingBlock(
|
|
1273
|
+
effectBoundary: Pick<XStateEffectBoundarySeed, 'dispositions'>,
|
|
1274
|
+
baseline: PlaybookRepositoryReceipt['baseline'] | undefined,
|
|
1275
|
+
): string | undefined {
|
|
1276
|
+
return effectBoundary.dispositions.includes('one-descendant-commit')
|
|
1277
|
+
? preExistingChangesBlock(baseline)
|
|
1278
|
+
: undefined;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1093
1281
|
const PLACEHOLDER_PATTERN = /<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>/g;
|
|
1094
1282
|
|
|
1095
1283
|
function placeholderFieldName(
|
|
@@ -1692,9 +1880,10 @@ export function createPlayerBridge(
|
|
|
1692
1880
|
);
|
|
1693
1881
|
}
|
|
1694
1882
|
if (result.status !== 'ok') {
|
|
1695
|
-
|
|
1883
|
+
const failure = new Error(
|
|
1696
1884
|
result.error ?? `captainBridge: callPlayer status "${result.status}"`,
|
|
1697
1885
|
);
|
|
1886
|
+
throw boundary?.markPlayerResultFailure?.(failure) ?? failure;
|
|
1698
1887
|
}
|
|
1699
1888
|
const finalText = result.finalText ?? '';
|
|
1700
1889
|
if (isEmptyFinalText(finalText)) {
|
|
@@ -3371,6 +3560,8 @@ export function createXStatePlaybookRuntime<
|
|
|
3371
3560
|
| {
|
|
3372
3561
|
readonly operationId: string;
|
|
3373
3562
|
readonly effectBoundary: XStateEffectBoundarySeed;
|
|
3563
|
+
/** The continuation's own baseline observation (DR-062 §4). */
|
|
3564
|
+
baseline?: PlaybookRepositoryReceipt['baseline'];
|
|
3374
3565
|
playerContinuation?: string | false;
|
|
3375
3566
|
input?: PlaybookPlayerInput;
|
|
3376
3567
|
roleId?: string;
|
|
@@ -3386,6 +3577,15 @@ export function createXStatePlaybookRuntime<
|
|
|
3386
3577
|
let deferInspectionEmissions = false;
|
|
3387
3578
|
let deferredInspectionEmissions: Array<() => void> = [];
|
|
3388
3579
|
let controlPlaneError: unknown;
|
|
3580
|
+
// DR-063 §2: the cause decided at the site the failure was decided, kept
|
|
3581
|
+
// for the failures whose Error the compiled artifact builds itself and the
|
|
3582
|
+
// runtime therefore cannot decorate. It is cleared at each Boss turn start
|
|
3583
|
+
// and at each successful player result, so it is never stale, and it is
|
|
3584
|
+
// only ever read as the cause of an error that carries none of its own.
|
|
3585
|
+
let lastFailureCause: PlaybookFailureCause | undefined;
|
|
3586
|
+
const rememberFailureCause = (cause: PlaybookFailureCause): void => {
|
|
3587
|
+
lastFailureCause = cause;
|
|
3588
|
+
};
|
|
3389
3589
|
// Previous root-machine state for the inspect-driven telemetry /
|
|
3390
3590
|
// status emitter. undefined before the first inspect firing.
|
|
3391
3591
|
let priorState: PlaybookState | undefined;
|
|
@@ -3881,6 +4081,19 @@ export function createXStatePlaybookRuntime<
|
|
|
3881
4081
|
refreshRetainedEffectReconciliation(current);
|
|
3882
4082
|
syncDeferredReconciliationOverlay();
|
|
3883
4083
|
refreshUnresolvedSemanticReconciliation(current);
|
|
4084
|
+
return projectUnresolvedEffectEnvelopes(current);
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
/**
|
|
4088
|
+
* The unresolved envelopes of one already-refreshed ledger mirror. Pure:
|
|
4089
|
+
* `describe()` reads it through the control view without moving anything.
|
|
4090
|
+
*/
|
|
4091
|
+
function projectUnresolvedEffectEnvelopes(
|
|
4092
|
+
current: PlaybookEffectLedger,
|
|
4093
|
+
): readonly (
|
|
4094
|
+
| { readonly kind: 'boundary'; readonly boundaryId: string }
|
|
4095
|
+
| { readonly kind: 'logical-operation'; readonly operationId: string }
|
|
4096
|
+
)[] {
|
|
3884
4097
|
if (!hasUnresolvedReconciliation()) return [];
|
|
3885
4098
|
|
|
3886
4099
|
const boundaryIds = new Set(unresolvedSemanticBoundaryIds);
|
|
@@ -4652,13 +4865,47 @@ export function createXStatePlaybookRuntime<
|
|
|
4652
4865
|
});
|
|
4653
4866
|
}
|
|
4654
4867
|
|
|
4868
|
+
// DR-063 §1: the cause of an unresolved governed settlement. A reconciled
|
|
4869
|
+
// mismatch supplies its own receipt-read cause; the reasons this function
|
|
4870
|
+
// owns map to adjudication, abort, and runtime-defect codes.
|
|
4871
|
+
function governedSettlementCause(
|
|
4872
|
+
reason: string,
|
|
4873
|
+
error: unknown,
|
|
4874
|
+
aborted: boolean,
|
|
4875
|
+
supplied: PlaybookFailureCause | undefined,
|
|
4876
|
+
): PlaybookFailureCause {
|
|
4877
|
+
if (supplied !== undefined) return supplied;
|
|
4878
|
+
if (aborted || reason.includes('aborted')) return ABORTED_FAILURE_CAUSE;
|
|
4879
|
+
if (GOVERNED_JUDGE_FAILURE_REASONS.includes(reason)) {
|
|
4880
|
+
const transport = failureErrorEvidence(error);
|
|
4881
|
+
return assertPlaybookFailureCause({
|
|
4882
|
+
code: 'judge-failed',
|
|
4883
|
+
evidence: {
|
|
4884
|
+
reason,
|
|
4885
|
+
...(transport === undefined ? {} : { error: transport }),
|
|
4886
|
+
},
|
|
4887
|
+
});
|
|
4888
|
+
}
|
|
4889
|
+
return runtimeDefectCause(reason);
|
|
4890
|
+
}
|
|
4891
|
+
|
|
4655
4892
|
function unresolvedGovernedSettlement(
|
|
4656
4893
|
reason: string,
|
|
4657
4894
|
error?: unknown,
|
|
4658
4895
|
signal: AbortSignal | undefined = activeSignal,
|
|
4896
|
+
cause?: PlaybookFailureCause,
|
|
4659
4897
|
): GovernedPlayerSettlement {
|
|
4660
|
-
|
|
4661
|
-
|
|
4898
|
+
const aborted =
|
|
4899
|
+
error !== undefined && signal?.aborted === true
|
|
4900
|
+
? Object.is(error, signal.reason)
|
|
4901
|
+
: false;
|
|
4902
|
+
const decided = governedSettlementCause(reason, error, aborted, cause);
|
|
4903
|
+
rememberFailureCause(decided);
|
|
4904
|
+
if (aborted) {
|
|
4905
|
+
return {
|
|
4906
|
+
status: 'unresolved',
|
|
4907
|
+
error: attachPlaybookFailureCause(error, decided),
|
|
4908
|
+
};
|
|
4662
4909
|
}
|
|
4663
4910
|
const failure =
|
|
4664
4911
|
error instanceof Error
|
|
@@ -4666,7 +4913,10 @@ export function createXStatePlaybookRuntime<
|
|
|
4666
4913
|
: new Error(`${label} governed outcome remains unresolved: ${reason}`);
|
|
4667
4914
|
return {
|
|
4668
4915
|
status: 'unresolved',
|
|
4669
|
-
error:
|
|
4916
|
+
error: attachPlaybookFailureCause(
|
|
4917
|
+
markFsmResultFailure(failure),
|
|
4918
|
+
decided,
|
|
4919
|
+
),
|
|
4670
4920
|
};
|
|
4671
4921
|
}
|
|
4672
4922
|
|
|
@@ -4914,7 +5164,12 @@ export function createXStatePlaybookRuntime<
|
|
|
4914
5164
|
if (reconciliation.status === 'unresolved') {
|
|
4915
5165
|
governedSettlementsByBoundaryId.set(
|
|
4916
5166
|
completion.boundary.boundaryId,
|
|
4917
|
-
unresolvedGovernedSettlement(
|
|
5167
|
+
unresolvedGovernedSettlement(
|
|
5168
|
+
reconciliation.reason,
|
|
5169
|
+
undefined,
|
|
5170
|
+
undefined,
|
|
5171
|
+
reconciliation.cause,
|
|
5172
|
+
),
|
|
4918
5173
|
);
|
|
4919
5174
|
return { finalText, semanticCandidate, unresolved: true };
|
|
4920
5175
|
}
|
|
@@ -5462,9 +5717,19 @@ export function createXStatePlaybookRuntime<
|
|
|
5462
5717
|
try {
|
|
5463
5718
|
const runTracedPlayerCall = async (
|
|
5464
5719
|
resume: string | false = selectedResume,
|
|
5720
|
+
// DR-062 §4: composed from this call's own baseline observation,
|
|
5721
|
+
// so the prompt the trace records is the prompt the player is sent.
|
|
5722
|
+
preExistingBlock?: string,
|
|
5465
5723
|
): Promise<PlayerResult> => {
|
|
5466
5724
|
const identity = callIdentity(resume);
|
|
5467
|
-
const
|
|
5725
|
+
const withPreExisting = (text: string): string =>
|
|
5726
|
+
preExistingBlock === undefined
|
|
5727
|
+
? text
|
|
5728
|
+
: `${text}\n\n${preExistingBlock}`;
|
|
5729
|
+
const callPrompt = withPreExisting(
|
|
5730
|
+
resume === false ? freshPrompt : prompt,
|
|
5731
|
+
);
|
|
5732
|
+
const callFreshPrompt = withPreExisting(freshPrompt);
|
|
5468
5733
|
await emitCallStarted(
|
|
5469
5734
|
'player.call.started',
|
|
5470
5735
|
'player.call.finished',
|
|
@@ -5484,7 +5749,12 @@ export function createXStatePlaybookRuntime<
|
|
|
5484
5749
|
roleId,
|
|
5485
5750
|
callPrompt,
|
|
5486
5751
|
signal,
|
|
5487
|
-
{
|
|
5752
|
+
{
|
|
5753
|
+
resume,
|
|
5754
|
+
...(callPrompt === callFreshPrompt
|
|
5755
|
+
? {}
|
|
5756
|
+
: { freshPrompt: callFreshPrompt }),
|
|
5757
|
+
},
|
|
5488
5758
|
);
|
|
5489
5759
|
// A host promise is not required to honor cancellation. Do not
|
|
5490
5760
|
// let a late result mutate continuity or publish a successful
|
|
@@ -5507,7 +5777,7 @@ export function createXStatePlaybookRuntime<
|
|
|
5507
5777
|
}
|
|
5508
5778
|
// A thrown port call carries no authoritative result, so the
|
|
5509
5779
|
// prior token remains available for a later explicit resume.
|
|
5510
|
-
throw error;
|
|
5780
|
+
throw decidePlayerCallFailure(error, roleId, playerId, signal);
|
|
5511
5781
|
}
|
|
5512
5782
|
|
|
5513
5783
|
let result: PlayerResult;
|
|
@@ -5524,7 +5794,7 @@ export function createXStatePlaybookRuntime<
|
|
|
5524
5794
|
} catch {
|
|
5525
5795
|
// The malformed host result remains authoritative.
|
|
5526
5796
|
}
|
|
5527
|
-
throw error;
|
|
5797
|
+
throw decidePlayerCallFailure(error, roleId, playerId, signal);
|
|
5528
5798
|
}
|
|
5529
5799
|
|
|
5530
5800
|
try {
|
|
@@ -5560,6 +5830,25 @@ export function createXStatePlaybookRuntime<
|
|
|
5560
5830
|
},
|
|
5561
5831
|
position,
|
|
5562
5832
|
);
|
|
5833
|
+
// DR-063 §2: the player bridge builds the failure Error for a
|
|
5834
|
+
// non-`ok` result, so the cause is decided here, where the role,
|
|
5835
|
+
// the player, and the reported error are known, and a successful
|
|
5836
|
+
// result clears it so no later failure inherits it.
|
|
5837
|
+
if (result.status === 'ok') {
|
|
5838
|
+
lastFailureCause = undefined;
|
|
5839
|
+
} else if (result.status === 'aborted') {
|
|
5840
|
+
rememberFailureCause(ABORTED_FAILURE_CAUSE);
|
|
5841
|
+
} else {
|
|
5842
|
+
rememberFailureCause(
|
|
5843
|
+
playerFailureCause({
|
|
5844
|
+
roleId,
|
|
5845
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
5846
|
+
error:
|
|
5847
|
+
result.error ??
|
|
5848
|
+
`callPlayer status ${JSON.stringify(result.status)}`,
|
|
5849
|
+
}),
|
|
5850
|
+
);
|
|
5851
|
+
}
|
|
5563
5852
|
return result;
|
|
5564
5853
|
};
|
|
5565
5854
|
|
|
@@ -5600,6 +5889,10 @@ export function createXStatePlaybookRuntime<
|
|
|
5600
5889
|
try {
|
|
5601
5890
|
deferredContinuation.result = await runTracedPlayerCall(
|
|
5602
5891
|
selectedResume,
|
|
5892
|
+
effectAuthorizedPreExistingBlock(
|
|
5893
|
+
effectBoundary,
|
|
5894
|
+
deferredContinuation.baseline,
|
|
5895
|
+
),
|
|
5603
5896
|
);
|
|
5604
5897
|
} catch (error) {
|
|
5605
5898
|
deferredContinuation.callError = error;
|
|
@@ -5621,7 +5914,18 @@ export function createXStatePlaybookRuntime<
|
|
|
5621
5914
|
const exclusive = await repositoryCapability.runExclusive({
|
|
5622
5915
|
signal,
|
|
5623
5916
|
effectBoundary,
|
|
5624
|
-
|
|
5917
|
+
// DR-062 §4: the call's own baseline, supplied by the host that
|
|
5918
|
+
// captured it under this exclusive claim. A host that supplies
|
|
5919
|
+
// none states no pre-existing change, exactly as an empty
|
|
5920
|
+
// baseline projection does.
|
|
5921
|
+
operation: (context) =>
|
|
5922
|
+
runTracedPlayerCall(
|
|
5923
|
+
selectedResume,
|
|
5924
|
+
effectAuthorizedPreExistingBlock(
|
|
5925
|
+
effectBoundary,
|
|
5926
|
+
context?.baseline,
|
|
5927
|
+
),
|
|
5928
|
+
),
|
|
5625
5929
|
completeEffectBoundary: completionEvidenceFor(
|
|
5626
5930
|
input,
|
|
5627
5931
|
roleId,
|
|
@@ -5652,6 +5956,12 @@ export function createXStatePlaybookRuntime<
|
|
|
5652
5956
|
}
|
|
5653
5957
|
},
|
|
5654
5958
|
|
|
5959
|
+
markPlayerResultFailure(error): Error {
|
|
5960
|
+
return lastFailureCause === undefined
|
|
5961
|
+
? error
|
|
5962
|
+
: attachPlaybookFailureCause(error, lastFailureCause);
|
|
5963
|
+
},
|
|
5964
|
+
|
|
5655
5965
|
takeGovernedPlayerOutput(result): GovernedPlayerSettlement | undefined {
|
|
5656
5966
|
const settlement = governedPlayerSettlements.get(result);
|
|
5657
5967
|
if (settlement !== undefined) governedPlayerSettlements.delete(result);
|
|
@@ -6292,6 +6602,80 @@ export function createXStatePlaybookRuntime<
|
|
|
6292
6602
|
return activeTurnId === undefined ? {} : { turnId: activeTurnId };
|
|
6293
6603
|
}
|
|
6294
6604
|
|
|
6605
|
+
// DR-063 §2: the parked failure's error as every public surface publishes
|
|
6606
|
+
// it — the FSM's own `lastError`, completed with the cause decided where
|
|
6607
|
+
// the failure was decided. An error that already carries a valid cause
|
|
6608
|
+
// keeps it; an artifact-built error takes the runtime's own slot; anything
|
|
6609
|
+
// else is a runtime defect carrying its message. There is no failure
|
|
6610
|
+
// without a cause.
|
|
6611
|
+
function failedStateError(
|
|
6612
|
+
context: Record<string, unknown>,
|
|
6613
|
+
): NormalizedError | undefined {
|
|
6614
|
+
const normalized: NormalizedError | undefined = normalizeErrorFull(
|
|
6615
|
+
context.lastError,
|
|
6616
|
+
);
|
|
6617
|
+
if (normalized === undefined || normalized.cause !== undefined) {
|
|
6618
|
+
return normalized;
|
|
6619
|
+
}
|
|
6620
|
+
return {
|
|
6621
|
+
...normalized,
|
|
6622
|
+
cause: lastFailureCause ?? runtimeDefectCause(normalized.message),
|
|
6623
|
+
};
|
|
6624
|
+
}
|
|
6625
|
+
|
|
6626
|
+
// DR-063 §2: a rejected player port or a result the runtime cannot read is
|
|
6627
|
+
// a `player-failed` failure named by role; an abort is `aborted`.
|
|
6628
|
+
function decidePlayerCallFailure(
|
|
6629
|
+
error: unknown,
|
|
6630
|
+
roleId: string,
|
|
6631
|
+
playerId: string | undefined,
|
|
6632
|
+
signal: AbortSignal,
|
|
6633
|
+
): unknown {
|
|
6634
|
+
const cause = isAbortFailure(error, signal)
|
|
6635
|
+
? ABORTED_FAILURE_CAUSE
|
|
6636
|
+
: playerFailureCause({
|
|
6637
|
+
roleId,
|
|
6638
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
6639
|
+
error,
|
|
6640
|
+
});
|
|
6641
|
+
rememberFailureCause(cause);
|
|
6642
|
+
return attachPlaybookFailureCause(error, cause);
|
|
6643
|
+
}
|
|
6644
|
+
|
|
6645
|
+
function completeFailedStatuses(
|
|
6646
|
+
statuses: readonly ScheduledStatus[],
|
|
6647
|
+
state: PlaybookState,
|
|
6648
|
+
context: Record<string, unknown>,
|
|
6649
|
+
): readonly ScheduledStatus[] {
|
|
6650
|
+
if (state.stateId !== 'failed') return statuses;
|
|
6651
|
+
const lastError = failedStateError(context);
|
|
6652
|
+
if (lastError?.cause === undefined) return statuses;
|
|
6653
|
+
const cause = lastError.cause as unknown as JsonValue;
|
|
6654
|
+
return statuses.map((status) => {
|
|
6655
|
+
const data = status.data;
|
|
6656
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
6657
|
+
return status;
|
|
6658
|
+
}
|
|
6659
|
+
const recorded = data as { readonly [key: string]: JsonValue };
|
|
6660
|
+
const recordedError = recorded.lastError;
|
|
6661
|
+
if (
|
|
6662
|
+
recordedError === undefined ||
|
|
6663
|
+
recordedError === null ||
|
|
6664
|
+
typeof recordedError !== 'object' ||
|
|
6665
|
+
Array.isArray(recordedError)
|
|
6666
|
+
) {
|
|
6667
|
+
return status;
|
|
6668
|
+
}
|
|
6669
|
+
return {
|
|
6670
|
+
...status,
|
|
6671
|
+
data: snapshotJsonValue(
|
|
6672
|
+
{ ...recorded, lastError: { ...recordedError, cause } },
|
|
6673
|
+
'failed status data',
|
|
6674
|
+
),
|
|
6675
|
+
};
|
|
6676
|
+
});
|
|
6677
|
+
}
|
|
6678
|
+
|
|
6295
6679
|
function structuredStateTelemetryPayload(
|
|
6296
6680
|
previousState: PlaybookState | undefined,
|
|
6297
6681
|
state: PlaybookState,
|
|
@@ -6313,7 +6697,7 @@ export function createXStatePlaybookRuntime<
|
|
|
6313
6697
|
payload.pendingBossQuestion = pendingBossQuestion;
|
|
6314
6698
|
}
|
|
6315
6699
|
if (state.stateId === 'failed') {
|
|
6316
|
-
const lastError =
|
|
6700
|
+
const lastError = failedStateError(context);
|
|
6317
6701
|
if (lastError !== undefined) payload.lastError = lastError;
|
|
6318
6702
|
}
|
|
6319
6703
|
return snapshotJsonValue(payload, 'FSM telemetry payload');
|
|
@@ -6518,10 +6902,10 @@ export function createXStatePlaybookRuntime<
|
|
|
6518
6902
|
inspectionEvent.event,
|
|
6519
6903
|
context,
|
|
6520
6904
|
);
|
|
6521
|
-
const stateStatuses =
|
|
6905
|
+
const stateStatuses = completeFailedStatuses(
|
|
6906
|
+
statusesForState(state, context, inspectionEvent.event),
|
|
6522
6907
|
state,
|
|
6523
6908
|
context,
|
|
6524
|
-
inspectionEvent.event,
|
|
6525
6909
|
);
|
|
6526
6910
|
const outcomeStatuses = usesDefaultStatuses
|
|
6527
6911
|
? acceptedOutcomes.map(({ acceptedOutcome }) => ({
|
|
@@ -6621,10 +7005,17 @@ export function createXStatePlaybookRuntime<
|
|
|
6621
7005
|
: outcome === 'aborted'
|
|
6622
7006
|
? activeSignal?.reason
|
|
6623
7007
|
: undefined);
|
|
7008
|
+
if (failure === undefined) return { outcome, state };
|
|
7009
|
+
// DR-063 §2: a `failed` settlement publishes the same cause-completed
|
|
7010
|
+
// error the failed state's status line and telemetry publish.
|
|
7011
|
+
const normalized =
|
|
7012
|
+
outcome === 'failed'
|
|
7013
|
+
? failedStateError({ lastError: failure })
|
|
7014
|
+
: normalizeError(failure);
|
|
6624
7015
|
return {
|
|
6625
7016
|
outcome,
|
|
6626
7017
|
state,
|
|
6627
|
-
...(
|
|
7018
|
+
...(normalized === undefined ? {} : { error: normalized }),
|
|
6628
7019
|
};
|
|
6629
7020
|
}
|
|
6630
7021
|
|
|
@@ -6853,11 +7244,46 @@ export function createXStatePlaybookRuntime<
|
|
|
6853
7244
|
action: {
|
|
6854
7245
|
id: `retry:${retryEvent.type}`,
|
|
6855
7246
|
label: `Retry: ${description}`,
|
|
7247
|
+
// DR-063 §3: retrying a failed call is `ready` — the runtime cannot
|
|
7248
|
+
// see whether the Boss changed the outside world since it failed.
|
|
7249
|
+
standing: 'ready',
|
|
6856
7250
|
},
|
|
6857
7251
|
event: retryEvent,
|
|
6858
7252
|
};
|
|
6859
7253
|
}
|
|
6860
7254
|
|
|
7255
|
+
/**
|
|
7256
|
+
* DR-063 §3: reconciliation only re-reads the host's ledger, which the
|
|
7257
|
+
* control view has just done. Where no checkpoint restoration is eligible
|
|
7258
|
+
* and every unresolved envelope's boundary already holds a complete
|
|
7259
|
+
* receipt, re-reading can resolve nothing, so the action is a no-op.
|
|
7260
|
+
*/
|
|
7261
|
+
function reconciliationStanding(
|
|
7262
|
+
deferredRestoreOperationId: string | undefined,
|
|
7263
|
+
): Pick<PlaybookControlAction, 'standing' | 'reason'> {
|
|
7264
|
+
if (deferredRestoreOperationId !== undefined) return { standing: 'ready' };
|
|
7265
|
+
const current = effectLedgerMirror;
|
|
7266
|
+
const envelopes = projectUnresolvedEffectEnvelopes(current);
|
|
7267
|
+
if (envelopes.length === 0) return { standing: 'ready' };
|
|
7268
|
+
const boundaryIds = envelopes.flatMap((envelope) =>
|
|
7269
|
+
envelope.kind === 'boundary'
|
|
7270
|
+
? [envelope.boundaryId]
|
|
7271
|
+
: (current.logicalOperations.find(
|
|
7272
|
+
({ operationId }) => operationId === envelope.operationId,
|
|
7273
|
+
)?.boundaryIds ?? []),
|
|
7274
|
+
);
|
|
7275
|
+
if (boundaryIds.length === 0) return { standing: 'ready' };
|
|
7276
|
+
const complete = boundaryIds.every(
|
|
7277
|
+
(boundaryId) =>
|
|
7278
|
+
current.boundaries.find(
|
|
7279
|
+
(candidate) => candidate.boundaryId === boundaryId,
|
|
7280
|
+
)?.physicalReceipt !== undefined,
|
|
7281
|
+
);
|
|
7282
|
+
return complete
|
|
7283
|
+
? { standing: 'no-op', reason: 'receipt-complete' }
|
|
7284
|
+
: { standing: 'ready' };
|
|
7285
|
+
}
|
|
7286
|
+
|
|
6861
7287
|
function deriveControlActions(snapshot: unknown): DerivedControlAction[] {
|
|
6862
7288
|
// Actions derive only at the safe point the parked snapshot also
|
|
6863
7289
|
// uses — quiescent actor with status `active` and no pending nested
|
|
@@ -6896,6 +7322,7 @@ export function createXStatePlaybookRuntime<
|
|
|
6896
7322
|
action: {
|
|
6897
7323
|
id: UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID,
|
|
6898
7324
|
label: 'Retry unresolved effect reconciliation',
|
|
7325
|
+
...reconciliationStanding(deferredRestoreOperationId),
|
|
6899
7326
|
},
|
|
6900
7327
|
unresolvedEffectAction: 'reconcile',
|
|
6901
7328
|
...(deferredRestoreOperationId === undefined
|
|
@@ -6906,6 +7333,8 @@ export function createXStatePlaybookRuntime<
|
|
|
6906
7333
|
action: {
|
|
6907
7334
|
id: UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID,
|
|
6908
7335
|
label: 'Abandon unresolved workflow attempt',
|
|
7336
|
+
// DR-063 §3: abandonment always changes the session's standing.
|
|
7337
|
+
standing: 'ready',
|
|
6909
7338
|
},
|
|
6910
7339
|
unresolvedEffectAction: 'abandon',
|
|
6911
7340
|
},
|
|
@@ -6930,6 +7359,8 @@ export function createXStatePlaybookRuntime<
|
|
|
6930
7359
|
action: {
|
|
6931
7360
|
id: `jump:${targetId}`,
|
|
6932
7361
|
label: `Resume from: ${description}`,
|
|
7362
|
+
// DR-063 §3: a jump always moves the machine.
|
|
7363
|
+
standing: 'ready',
|
|
6933
7364
|
},
|
|
6934
7365
|
event,
|
|
6935
7366
|
});
|
|
@@ -7421,8 +7852,12 @@ export function createXStatePlaybookRuntime<
|
|
|
7421
7852
|
signal,
|
|
7422
7853
|
operationId: operation.operationId,
|
|
7423
7854
|
effectBoundary,
|
|
7424
|
-
operation: async (
|
|
7855
|
+
operation: async (context) => {
|
|
7856
|
+
const { playerContinuation } = context ?? {};
|
|
7425
7857
|
validateBinding(playerContinuation);
|
|
7858
|
+
// DR-062 §4: the continuation's own baseline, captured before the
|
|
7859
|
+
// FSM re-enters the bound player state that reads it.
|
|
7860
|
+
continuation.baseline = context?.baseline;
|
|
7426
7861
|
continuation.playerContinuation = selectPlayerResume(
|
|
7427
7862
|
effectBoundary.roleId, boundPlayerId,
|
|
7428
7863
|
);
|
|
@@ -7926,7 +8361,10 @@ export function createXStatePlaybookRuntime<
|
|
|
7926
8361
|
!hasUnresolvedReconciliation()
|
|
7927
8362
|
? pendingBossQuestionForState(state, context)
|
|
7928
8363
|
: undefined;
|
|
7929
|
-
const lastError =
|
|
8364
|
+
const lastError =
|
|
8365
|
+
state.stateId === 'failed'
|
|
8366
|
+
? failedStateError(context)
|
|
8367
|
+
: normalizeErrorFull(context.lastError);
|
|
7930
8368
|
const projectedContext = projectControlContext(context);
|
|
7931
8369
|
const stateDescription =
|
|
7932
8370
|
!hasUnresolvedReconciliation()
|
|
@@ -8325,6 +8763,9 @@ export function createXStatePlaybookRuntime<
|
|
|
8325
8763
|
activeAborts = abortReasonClassifier(signal);
|
|
8326
8764
|
activeAbortEmission = undefined;
|
|
8327
8765
|
controlPlaneError = undefined;
|
|
8766
|
+
// DR-063 §2: each turn decides its own failures, so the slot never
|
|
8767
|
+
// carries a previous turn's cause into this one.
|
|
8768
|
+
lastFailureCause = undefined;
|
|
8328
8769
|
let result: PlaybookRunResult | undefined;
|
|
8329
8770
|
let operationError: unknown;
|
|
8330
8771
|
// The boundary sentinel releases on every exit: a settlement defect
|