@sublang/playbook 13.3.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/README.md +5 -5
- package/docs/cli.md +14 -14
- package/docs/configuration.md +10 -2
- package/docs/embedding.md +20 -1
- package/package.json +6 -1
- package/reference/sdlc/branch.playbook/branch.fsm.d.ts +1 -1
- package/reference/sdlc/code.playbook/bin/launch-config.js +88 -5
- package/reference/sdlc/code.playbook/bin/playbook.js +4 -0
- package/reference/sdlc/code.playbook/bin/repository-effects.js +198 -17
- package/reference/sdlc/code.playbook/bin/run.js +4 -0
- package/reference/sdlc/code.playbook/bin/session-store.js +2 -2
- 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 +374 -20
- package/reference/sdlc/code.playbook/playbook-captain.ts +478 -38
- 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.d.ts +3 -10
- package/reference/sdlc/dev.playbook/dev.playbook.js +6 -6
- package/reference/sdlc/dev.playbook/dev.playbook.ts +8 -9
- package/reference/sdlc/dev.playbook/dev.registry.js +1 -1
- package/reference/sdlc/dev.playbook/dev.registry.ts +1 -1
- package/reference/sdlc/pr.md +17 -4
- package/reference/sdlc/pr.playbook/pr.fsm.d.ts +18 -4
- package/reference/sdlc/pr.playbook/pr.fsm.js +49 -18
- package/reference/sdlc/pr.playbook/pr.fsm.ts +63 -20
- package/reference/sdlc/pr.playbook/pr.gears.md +20 -7
- package/reference/sdlc/pr.playbook/pr.playbook.js +2 -1
- package/reference/sdlc/pr.playbook/pr.playbook.ts +2 -1
- package/reference/sdlc/review.playbook/review.fsm.d.ts +1 -1
- package/slc/experiments/fsm-scaffold-guidance.md +31 -0
- package/slc/gears2fsm.md +145 -58
- package/slc/link.md +190 -7
- package/slc/materialize-link.mjs +460 -0
- package/slc/optimize.md +46 -6
- package/slc/scaffold-fsm.mjs +225 -0
- package/slc/slc.pin-inputs.json +13 -0
- package/slc/text2gears.md +38 -3
- package/slc/workflow-contracts.json +371 -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 +407 -21
- package/src/xstate-playbook-runtime.ts +514 -20
- package/src/xstate-runtime.d.ts +9 -1
- package/src/xstate-runtime.js +299 -19
- package/src/xstate-runtime.ts +362 -21
|
@@ -21,7 +21,8 @@ import { isDeepStrictEqual } from 'node:util';
|
|
|
21
21
|
import PQueue from 'p-queue';
|
|
22
22
|
import { createActor, fromPromise } from 'xstate';
|
|
23
23
|
import { createAcceptedOutcomeConsumer, } from './accepted-outcome.js';
|
|
24
|
-
import {
|
|
24
|
+
import { assertPlaybookFailureCause } from './runtime.js';
|
|
25
|
+
import { assertPlaybookRuntimeSnapshot, assertPlaybookEffectLedger, attachPlaybookFailureCause, combineAbortSignals, createNestedPlaybookBridge, detachPersistedMachineSnapshot, normalizeError, normalizePlaybookSnapshot, snapshotJsonValue, snapshotPlaybookSession, isPlaybookEffectLedgerMonotonicExtension, PlaybookSemanticCandidateStructureError, reconcilePlaybookSemanticEvidence, validateCaptainResult, validatePlayerResult, waitForPlaybookQuiescence, } from './xstate-runtime.js';
|
|
25
26
|
export const BOSS_REPLY_ERRORS = {
|
|
26
27
|
missingQuestion: "needsBossReply outcome missing 'question' field",
|
|
27
28
|
unregisteredState: (stateId) => `state ${stateId} declared needsBossReply but is not registered as resumable`,
|
|
@@ -38,6 +39,17 @@ export const BOSS_REPLY_ERRORS = {
|
|
|
38
39
|
const fsmResultFailures = new WeakSet();
|
|
39
40
|
function markFsmResultFailure(error) {
|
|
40
41
|
fsmResultFailures.add(error);
|
|
42
|
+
// DR-063 §2: there is no failure without a cause. A failure the runtime
|
|
43
|
+
// marks with nothing more specific is a runtime defect carrying its own
|
|
44
|
+
// message; a decision site that knows better attaches its cause afterwards.
|
|
45
|
+
attachPlaybookFailureCause(error, {
|
|
46
|
+
code: 'runtime-defect',
|
|
47
|
+
evidence: {
|
|
48
|
+
reason: typeof error.message === 'string' && error.message.length > 0
|
|
49
|
+
? error.message
|
|
50
|
+
: 'unknown runtime defect',
|
|
51
|
+
},
|
|
52
|
+
});
|
|
41
53
|
return error;
|
|
42
54
|
}
|
|
43
55
|
function isFsmResultFailure(error) {
|
|
@@ -46,6 +58,63 @@ function isFsmResultFailure(error) {
|
|
|
46
58
|
fsmResultFailures.has(error));
|
|
47
59
|
}
|
|
48
60
|
// ---------------------------------------------------------------------------
|
|
61
|
+
// DR-063 §1/§2: there is no failure without a cause. Each decision site builds
|
|
62
|
+
// the cause its own evidence states, the runtime attaches it to the error it
|
|
63
|
+
// marks as the FSM failure, and anything else the runtime marks becomes
|
|
64
|
+
// `runtime-defect` carrying its message as `reason`.
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
/** Adjudication reasons this runtime decides itself, and their codes. */
|
|
67
|
+
const GOVERNED_JUDGE_FAILURE_REASONS = [
|
|
68
|
+
'judge transport failed',
|
|
69
|
+
'corrective judge failed',
|
|
70
|
+
'corrective semantic candidate is invalid',
|
|
71
|
+
'semantic correction budget is unavailable',
|
|
72
|
+
];
|
|
73
|
+
const ABORTED_FAILURE_CAUSE = assertPlaybookFailureCause({
|
|
74
|
+
code: 'aborted',
|
|
75
|
+
evidence: {},
|
|
76
|
+
});
|
|
77
|
+
function runtimeDefectCause(reason) {
|
|
78
|
+
return assertPlaybookFailureCause({
|
|
79
|
+
code: 'runtime-defect',
|
|
80
|
+
evidence: { reason: reason.length === 0 ? 'unknown runtime defect' : reason },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function failureErrorEvidence(error) {
|
|
84
|
+
if (error === undefined || error === null)
|
|
85
|
+
return undefined;
|
|
86
|
+
const { name, message } = normalizeError(error);
|
|
87
|
+
return { name, message };
|
|
88
|
+
}
|
|
89
|
+
/** A string `code` the rejecting port reported, when it reported one. */
|
|
90
|
+
function failureErrorCode(error) {
|
|
91
|
+
if (typeof error !== 'object' || error === null)
|
|
92
|
+
return undefined;
|
|
93
|
+
try {
|
|
94
|
+
const code = error.code;
|
|
95
|
+
return typeof code === 'string' && code.length > 0 ? code : undefined;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function playerFailureCause(input) {
|
|
102
|
+
return assertPlaybookFailureCause({
|
|
103
|
+
code: 'player-failed',
|
|
104
|
+
evidence: {
|
|
105
|
+
roleId: input.roleId,
|
|
106
|
+
...(input.playerId === undefined ? {} : { playerId: input.playerId }),
|
|
107
|
+
error: failureErrorEvidence(input.error) ?? {
|
|
108
|
+
name: 'Error',
|
|
109
|
+
message: 'Unknown error',
|
|
110
|
+
},
|
|
111
|
+
...(failureErrorCode(input.error) === undefined
|
|
112
|
+
? {}
|
|
113
|
+
: { errorCode: failureErrorCode(input.error) }),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
49
118
|
// DR-028: both call boundaries treat an `ok` result whose `finalText` is
|
|
50
119
|
// missing, empty, or whitespace-only under one empty predicate, and that
|
|
51
120
|
// shape earns exactly one corrective re-ask — the same composed call
|
|
@@ -320,7 +389,13 @@ export function normalizeErrorCompact(err) {
|
|
|
320
389
|
if (err === undefined || err === null)
|
|
321
390
|
return undefined;
|
|
322
391
|
const normalized = normalizeError(err);
|
|
323
|
-
return {
|
|
392
|
+
return {
|
|
393
|
+
name: normalized.name,
|
|
394
|
+
message: normalized.message,
|
|
395
|
+
// DR-063 §2: on a failure the cause is why the compact form exists, so it
|
|
396
|
+
// survives where the stack does not.
|
|
397
|
+
...(normalized.cause === undefined ? {} : { cause: normalized.cause }),
|
|
398
|
+
};
|
|
324
399
|
}
|
|
325
400
|
export function normalizeErrorFull(err) {
|
|
326
401
|
if (err === undefined || err === null)
|
|
@@ -417,6 +492,97 @@ function continuationBlocks(input, resuming = false) {
|
|
|
417
492
|
export function composePlayerContinuation(input, body, resuming = false) {
|
|
418
493
|
return [...continuationBlocks(input, resuming), body].join('\n\n');
|
|
419
494
|
}
|
|
495
|
+
// DR-062 §4: the Coder is told which uncommitted changes were already in the
|
|
496
|
+
// worktree when its own call started. The block is composed from the call's
|
|
497
|
+
// own baseline observation, names paths only, and is bounded.
|
|
498
|
+
const PRE_EXISTING_CHANGE_GROUPS = [
|
|
499
|
+
'staged',
|
|
500
|
+
'modified',
|
|
501
|
+
'deleted',
|
|
502
|
+
'untracked',
|
|
503
|
+
'renamed',
|
|
504
|
+
'unmerged',
|
|
505
|
+
'other',
|
|
506
|
+
];
|
|
507
|
+
const PRE_EXISTING_CHANGE_PATH_LIMIT = 40;
|
|
508
|
+
/**
|
|
509
|
+
* The group one baseline projection entry belongs to. The shapes are the
|
|
510
|
+
* porcelain-v2 records the repository observer projects: an `untracked`,
|
|
511
|
+
* `rename-or-copy`, or `unmerged` kind, and otherwise an `ordinary` record
|
|
512
|
+
* whose two-character `xy` carries the index state first and the worktree
|
|
513
|
+
* state second.
|
|
514
|
+
*/
|
|
515
|
+
function preExistingChangeGroup(entry) {
|
|
516
|
+
if (!isPlainObject(entry))
|
|
517
|
+
return 'other';
|
|
518
|
+
if (entry.kind === 'untracked')
|
|
519
|
+
return 'untracked';
|
|
520
|
+
if (entry.kind === 'rename-or-copy')
|
|
521
|
+
return 'renamed';
|
|
522
|
+
if (entry.kind === 'unmerged')
|
|
523
|
+
return 'unmerged';
|
|
524
|
+
const xy = entry.xy;
|
|
525
|
+
if (entry.kind !== 'ordinary' || typeof xy !== 'string' || xy.length !== 2) {
|
|
526
|
+
return 'other';
|
|
527
|
+
}
|
|
528
|
+
if (xy[1] === 'D')
|
|
529
|
+
return 'deleted';
|
|
530
|
+
if (xy[1] === '.')
|
|
531
|
+
return xy[0] === '.' ? 'other' : 'staged';
|
|
532
|
+
return 'modified';
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* The pre-existing-changes block appended to an effect-authorized player
|
|
536
|
+
* prompt, or `undefined` for an empty baseline projection.
|
|
537
|
+
*/
|
|
538
|
+
function preExistingChangesBlock(baseline) {
|
|
539
|
+
const projection = baseline?.projection;
|
|
540
|
+
if (!isPlainObject(projection))
|
|
541
|
+
return undefined;
|
|
542
|
+
const grouped = new Map();
|
|
543
|
+
let total = 0;
|
|
544
|
+
for (const path of Object.keys(projection)) {
|
|
545
|
+
const group = preExistingChangeGroup(projection[path]);
|
|
546
|
+
const paths = grouped.get(group);
|
|
547
|
+
if (paths === undefined)
|
|
548
|
+
grouped.set(group, [path]);
|
|
549
|
+
else
|
|
550
|
+
paths.push(path);
|
|
551
|
+
total += 1;
|
|
552
|
+
}
|
|
553
|
+
if (total === 0)
|
|
554
|
+
return undefined;
|
|
555
|
+
const omitted = Math.max(0, total - PRE_EXISTING_CHANGE_PATH_LIMIT);
|
|
556
|
+
const lines = [];
|
|
557
|
+
let budget = PRE_EXISTING_CHANGE_PATH_LIMIT;
|
|
558
|
+
for (const group of PRE_EXISTING_CHANGE_GROUPS) {
|
|
559
|
+
const paths = grouped.get(group);
|
|
560
|
+
if (paths === undefined || budget <= 0)
|
|
561
|
+
continue;
|
|
562
|
+
const printed = paths.slice().sort().slice(0, budget);
|
|
563
|
+
budget -= printed.length;
|
|
564
|
+
lines.push(`> - ${group}: ${printed.join(', ')}`);
|
|
565
|
+
}
|
|
566
|
+
if (omitted > 0 && lines.length > 0) {
|
|
567
|
+
lines[lines.length - 1] += `, … and ${omitted} more`;
|
|
568
|
+
}
|
|
569
|
+
return [
|
|
570
|
+
'> Uncommitted changes present before this call, belonging to the Boss:',
|
|
571
|
+
...lines,
|
|
572
|
+
"> 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.",
|
|
573
|
+
].join('\n');
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* The block for one governed call, or `undefined` where the call is not
|
|
577
|
+
* effect-authorized — a call whose declared outcomes carry no
|
|
578
|
+
* `one-descendant-commit` disposition may commit nothing, so it is told
|
|
579
|
+
* nothing.
|
|
580
|
+
*/
|
|
581
|
+
function effectAuthorizedPreExistingBlock(effectBoundary, baseline) {
|
|
582
|
+
return effectBoundary.dispositions.includes('one-descendant-commit')
|
|
583
|
+
? preExistingChangesBlock(baseline)
|
|
584
|
+
: undefined;
|
|
585
|
+
}
|
|
420
586
|
const PLACEHOLDER_PATTERN = /<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>/g;
|
|
421
587
|
function placeholderFieldName(token, fields) {
|
|
422
588
|
const explicit = fields[token];
|
|
@@ -830,7 +996,8 @@ export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onCon
|
|
|
830
996
|
: false);
|
|
831
997
|
}
|
|
832
998
|
if (result.status !== 'ok') {
|
|
833
|
-
|
|
999
|
+
const failure = new Error(result.error ?? `captainBridge: callPlayer status "${result.status}"`);
|
|
1000
|
+
throw boundary?.markPlayerResultFailure?.(failure) ?? failure;
|
|
834
1001
|
}
|
|
835
1002
|
const finalText = result.finalText ?? '';
|
|
836
1003
|
if (isEmptyFinalText(finalText)) {
|
|
@@ -2019,6 +2186,15 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2019
2186
|
let deferInspectionEmissions = false;
|
|
2020
2187
|
let deferredInspectionEmissions = [];
|
|
2021
2188
|
let controlPlaneError;
|
|
2189
|
+
// DR-063 §2: the cause decided at the site the failure was decided, kept
|
|
2190
|
+
// for the failures whose Error the compiled artifact builds itself and the
|
|
2191
|
+
// runtime therefore cannot decorate. It is cleared at each Boss turn start
|
|
2192
|
+
// and at each successful player result, so it is never stale, and it is
|
|
2193
|
+
// only ever read as the cause of an error that carries none of its own.
|
|
2194
|
+
let lastFailureCause;
|
|
2195
|
+
const rememberFailureCause = (cause) => {
|
|
2196
|
+
lastFailureCause = cause;
|
|
2197
|
+
};
|
|
2022
2198
|
// Previous root-machine state for the inspect-driven telemetry /
|
|
2023
2199
|
// status emitter. undefined before the first inspect firing.
|
|
2024
2200
|
let priorState;
|
|
@@ -2388,6 +2564,13 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2388
2564
|
refreshRetainedEffectReconciliation(current);
|
|
2389
2565
|
syncDeferredReconciliationOverlay();
|
|
2390
2566
|
refreshUnresolvedSemanticReconciliation(current);
|
|
2567
|
+
return projectUnresolvedEffectEnvelopes(current);
|
|
2568
|
+
}
|
|
2569
|
+
/**
|
|
2570
|
+
* The unresolved envelopes of one already-refreshed ledger mirror. Pure:
|
|
2571
|
+
* `describe()` reads it through the control view without moving anything.
|
|
2572
|
+
*/
|
|
2573
|
+
function projectUnresolvedEffectEnvelopes(current) {
|
|
2391
2574
|
if (!hasUnresolvedReconciliation())
|
|
2392
2575
|
return [];
|
|
2393
2576
|
const boundaryIds = new Set(unresolvedSemanticBoundaryIds);
|
|
@@ -2455,6 +2638,39 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2455
2638
|
throw new Error(`createPlaybookRuntime.${method}: deferred settlement recovery is required`, { cause: deferredSettlementClosure });
|
|
2456
2639
|
}
|
|
2457
2640
|
}
|
|
2641
|
+
function pendingHasExactUnchangedOrigin(pending) {
|
|
2642
|
+
// The outcome menu is not the selected outcome: a source-authored
|
|
2643
|
+
// unchanged question may share its state with needsBossReply: deferred.
|
|
2644
|
+
// An open operation nevertheless always retains its checkpoint and
|
|
2645
|
+
// cumulative receipt authority, even if a later boundary looks alike.
|
|
2646
|
+
if (runtimeLogicalOperations().some((operation) => operation.logicalReceipt === undefined)) {
|
|
2647
|
+
return false;
|
|
2648
|
+
}
|
|
2649
|
+
const latest = effectLedgerMirror.boundaries
|
|
2650
|
+
.filter(runtimeBoundaryIsOwned)
|
|
2651
|
+
.at(-1);
|
|
2652
|
+
if (latest === undefined ||
|
|
2653
|
+
latest.sourceStateId !== pending.resumeStateId ||
|
|
2654
|
+
pending.asker.kind !== 'role' ||
|
|
2655
|
+
latest.roleId !== pending.asker.roleId ||
|
|
2656
|
+
latest.logicalOperationId !== undefined ||
|
|
2657
|
+
latest.physicalReceipt?.classification !== 'unchanged') {
|
|
2658
|
+
return false;
|
|
2659
|
+
}
|
|
2660
|
+
// Reuse the persisted authority/receipt check rather than infer an arm
|
|
2661
|
+
// from question prose or search older evidence for a convenient match.
|
|
2662
|
+
const persisted = persistedBoundaryReconciliation(latest, effectLedgerMirror);
|
|
2663
|
+
if (persisted === undefined ||
|
|
2664
|
+
persisted.historicalDeferred ||
|
|
2665
|
+
persisted.reconciliation.status !== 'resolved') {
|
|
2666
|
+
return false;
|
|
2667
|
+
}
|
|
2668
|
+
const output = persisted.reconciliation.output;
|
|
2669
|
+
const declaration = outcomeAuthority.governedPlayerStates[latest.sourceStateId]?.[output.guard];
|
|
2670
|
+
return (declaration?.repositoryDisposition === 'unchanged' &&
|
|
2671
|
+
typeof output.question === 'string' &&
|
|
2672
|
+
output.question === pending.question);
|
|
2673
|
+
}
|
|
2458
2674
|
function currentBoundDeferredOperation(pending) {
|
|
2459
2675
|
const projected = {
|
|
2460
2676
|
questionId: pending.questionId,
|
|
@@ -2886,16 +3102,44 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2886
3102
|
},
|
|
2887
3103
|
});
|
|
2888
3104
|
}
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
3105
|
+
// DR-063 §1: the cause of an unresolved governed settlement. A reconciled
|
|
3106
|
+
// mismatch supplies its own receipt-read cause; the reasons this function
|
|
3107
|
+
// owns map to adjudication, abort, and runtime-defect codes.
|
|
3108
|
+
function governedSettlementCause(reason, error, aborted, supplied) {
|
|
3109
|
+
if (supplied !== undefined)
|
|
3110
|
+
return supplied;
|
|
3111
|
+
if (aborted || reason.includes('aborted'))
|
|
3112
|
+
return ABORTED_FAILURE_CAUSE;
|
|
3113
|
+
if (GOVERNED_JUDGE_FAILURE_REASONS.includes(reason)) {
|
|
3114
|
+
const transport = failureErrorEvidence(error);
|
|
3115
|
+
return assertPlaybookFailureCause({
|
|
3116
|
+
code: 'judge-failed',
|
|
3117
|
+
evidence: {
|
|
3118
|
+
reason,
|
|
3119
|
+
...(transport === undefined ? {} : { error: transport }),
|
|
3120
|
+
},
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
return runtimeDefectCause(reason);
|
|
3124
|
+
}
|
|
3125
|
+
function unresolvedGovernedSettlement(reason, error, signal = activeSignal, cause) {
|
|
3126
|
+
const aborted = error !== undefined && signal?.aborted === true
|
|
3127
|
+
? Object.is(error, signal.reason)
|
|
3128
|
+
: false;
|
|
3129
|
+
const decided = governedSettlementCause(reason, error, aborted, cause);
|
|
3130
|
+
rememberFailureCause(decided);
|
|
3131
|
+
if (aborted) {
|
|
3132
|
+
return {
|
|
3133
|
+
status: 'unresolved',
|
|
3134
|
+
error: attachPlaybookFailureCause(error, decided),
|
|
3135
|
+
};
|
|
2892
3136
|
}
|
|
2893
3137
|
const failure = error instanceof Error
|
|
2894
3138
|
? error
|
|
2895
3139
|
: new Error(`${label} governed outcome remains unresolved: ${reason}`);
|
|
2896
3140
|
return {
|
|
2897
3141
|
status: 'unresolved',
|
|
2898
|
-
error: markFsmResultFailure(failure),
|
|
3142
|
+
error: attachPlaybookFailureCause(markFsmResultFailure(failure), decided),
|
|
2899
3143
|
};
|
|
2900
3144
|
}
|
|
2901
3145
|
async function spendSemanticCorrectionBudget(completedBoundary, receipt, finalText, semanticCandidate) {
|
|
@@ -3052,7 +3296,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3052
3296
|
}
|
|
3053
3297
|
const semanticCandidate = snapshotJsonValue(reconciliation.evidence.semanticCandidate, `${label} governed semantic candidate`);
|
|
3054
3298
|
if (reconciliation.status === 'unresolved') {
|
|
3055
|
-
governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, unresolvedGovernedSettlement(reconciliation.reason));
|
|
3299
|
+
governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, unresolvedGovernedSettlement(reconciliation.reason, undefined, undefined, reconciliation.cause));
|
|
3056
3300
|
return { finalText, semanticCandidate, unresolved: true };
|
|
3057
3301
|
}
|
|
3058
3302
|
const output = reconciliation.output;
|
|
@@ -3429,9 +3673,16 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3429
3673
|
}
|
|
3430
3674
|
activePlayerKeys.add(playerKey);
|
|
3431
3675
|
try {
|
|
3432
|
-
const runTracedPlayerCall = async (resume = selectedResume
|
|
3676
|
+
const runTracedPlayerCall = async (resume = selectedResume,
|
|
3677
|
+
// DR-062 §4: composed from this call's own baseline observation,
|
|
3678
|
+
// so the prompt the trace records is the prompt the player is sent.
|
|
3679
|
+
preExistingBlock) => {
|
|
3433
3680
|
const identity = callIdentity(resume);
|
|
3434
|
-
const
|
|
3681
|
+
const withPreExisting = (text) => preExistingBlock === undefined
|
|
3682
|
+
? text
|
|
3683
|
+
: `${text}\n\n${preExistingBlock}`;
|
|
3684
|
+
const callPrompt = withPreExisting(resume === false ? freshPrompt : prompt);
|
|
3685
|
+
const callFreshPrompt = withPreExisting(freshPrompt);
|
|
3435
3686
|
await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt: callPrompt }, position, signal);
|
|
3436
3687
|
let rawResult;
|
|
3437
3688
|
try {
|
|
@@ -3440,7 +3691,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3440
3691
|
// never start after abort, so settle the already-started pair
|
|
3441
3692
|
// as `aborted` through the catch below.
|
|
3442
3693
|
signal.throwIfAborted();
|
|
3443
|
-
rawResult = await requireHostPorts().callPlayer(roleId, callPrompt, signal, {
|
|
3694
|
+
rawResult = await requireHostPorts().callPlayer(roleId, callPrompt, signal, {
|
|
3695
|
+
resume,
|
|
3696
|
+
...(callPrompt === callFreshPrompt
|
|
3697
|
+
? {}
|
|
3698
|
+
: { freshPrompt: callFreshPrompt }),
|
|
3699
|
+
});
|
|
3444
3700
|
// A host promise is not required to honor cancellation. Do not
|
|
3445
3701
|
// let a late result mutate continuity or publish a successful
|
|
3446
3702
|
// finish.
|
|
@@ -3461,7 +3717,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3461
3717
|
}
|
|
3462
3718
|
// A thrown port call carries no authoritative result, so the
|
|
3463
3719
|
// prior token remains available for a later explicit resume.
|
|
3464
|
-
throw error;
|
|
3720
|
+
throw decidePlayerCallFailure(error, roleId, playerId, signal);
|
|
3465
3721
|
}
|
|
3466
3722
|
let result;
|
|
3467
3723
|
try {
|
|
@@ -3476,7 +3732,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3476
3732
|
catch {
|
|
3477
3733
|
// The malformed host result remains authoritative.
|
|
3478
3734
|
}
|
|
3479
|
-
throw error;
|
|
3735
|
+
throw decidePlayerCallFailure(error, roleId, playerId, signal);
|
|
3480
3736
|
}
|
|
3481
3737
|
try {
|
|
3482
3738
|
updatePlayerResume(roleId, playerId, result);
|
|
@@ -3505,6 +3761,24 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3505
3761
|
? { resumeToken: result.resumeToken }
|
|
3506
3762
|
: {}),
|
|
3507
3763
|
}, position);
|
|
3764
|
+
// DR-063 §2: the player bridge builds the failure Error for a
|
|
3765
|
+
// non-`ok` result, so the cause is decided here, where the role,
|
|
3766
|
+
// the player, and the reported error are known, and a successful
|
|
3767
|
+
// result clears it so no later failure inherits it.
|
|
3768
|
+
if (result.status === 'ok') {
|
|
3769
|
+
lastFailureCause = undefined;
|
|
3770
|
+
}
|
|
3771
|
+
else if (result.status === 'aborted') {
|
|
3772
|
+
rememberFailureCause(ABORTED_FAILURE_CAUSE);
|
|
3773
|
+
}
|
|
3774
|
+
else {
|
|
3775
|
+
rememberFailureCause(playerFailureCause({
|
|
3776
|
+
roleId,
|
|
3777
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
3778
|
+
error: result.error ??
|
|
3779
|
+
`callPlayer status ${JSON.stringify(result.status)}`,
|
|
3780
|
+
}));
|
|
3781
|
+
}
|
|
3508
3782
|
return result;
|
|
3509
3783
|
};
|
|
3510
3784
|
const effectBoundary = governedBoundarySeed(input, roleId, callId, turnId);
|
|
@@ -3527,7 +3801,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3527
3801
|
deferredContinuation.playerId = playerId;
|
|
3528
3802
|
deferredContinuation.signal = signal;
|
|
3529
3803
|
try {
|
|
3530
|
-
deferredContinuation.result = await runTracedPlayerCall(selectedResume);
|
|
3804
|
+
deferredContinuation.result = await runTracedPlayerCall(selectedResume, effectAuthorizedPreExistingBlock(effectBoundary, deferredContinuation.baseline));
|
|
3531
3805
|
}
|
|
3532
3806
|
catch (error) {
|
|
3533
3807
|
deferredContinuation.callError = error;
|
|
@@ -3549,7 +3823,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3549
3823
|
const exclusive = await repositoryCapability.runExclusive({
|
|
3550
3824
|
signal,
|
|
3551
3825
|
effectBoundary,
|
|
3552
|
-
|
|
3826
|
+
// DR-062 §4: the call's own baseline, supplied by the host that
|
|
3827
|
+
// captured it under this exclusive claim. A host that supplies
|
|
3828
|
+
// none states no pre-existing change, exactly as an empty
|
|
3829
|
+
// baseline projection does.
|
|
3830
|
+
operation: (context) => runTracedPlayerCall(selectedResume, effectAuthorizedPreExistingBlock(effectBoundary, context?.baseline)),
|
|
3553
3831
|
completeEffectBoundary: completionEvidenceFor(input, roleId, playerId, signal, undefined),
|
|
3554
3832
|
});
|
|
3555
3833
|
return acknowledgeGovernedPlayerResult(exclusive, effectBoundary.boundaryId);
|
|
@@ -3571,6 +3849,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3571
3849
|
activePlayerKeys.delete(playerKey);
|
|
3572
3850
|
}
|
|
3573
3851
|
},
|
|
3852
|
+
markPlayerResultFailure(error) {
|
|
3853
|
+
return lastFailureCause === undefined
|
|
3854
|
+
? error
|
|
3855
|
+
: attachPlaybookFailureCause(error, lastFailureCause);
|
|
3856
|
+
},
|
|
3574
3857
|
takeGovernedPlayerOutput(result) {
|
|
3575
3858
|
const settlement = governedPlayerSettlements.get(result);
|
|
3576
3859
|
if (settlement !== undefined)
|
|
@@ -4085,6 +4368,61 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4085
4368
|
function tracePositionForActiveTurn() {
|
|
4086
4369
|
return activeTurnId === undefined ? {} : { turnId: activeTurnId };
|
|
4087
4370
|
}
|
|
4371
|
+
// DR-063 §2: the parked failure's error as every public surface publishes
|
|
4372
|
+
// it — the FSM's own `lastError`, completed with the cause decided where
|
|
4373
|
+
// the failure was decided. An error that already carries a valid cause
|
|
4374
|
+
// keeps it; an artifact-built error takes the runtime's own slot; anything
|
|
4375
|
+
// else is a runtime defect carrying its message. There is no failure
|
|
4376
|
+
// without a cause.
|
|
4377
|
+
function failedStateError(context) {
|
|
4378
|
+
const normalized = normalizeErrorFull(context.lastError);
|
|
4379
|
+
if (normalized === undefined || normalized.cause !== undefined) {
|
|
4380
|
+
return normalized;
|
|
4381
|
+
}
|
|
4382
|
+
return {
|
|
4383
|
+
...normalized,
|
|
4384
|
+
cause: lastFailureCause ?? runtimeDefectCause(normalized.message),
|
|
4385
|
+
};
|
|
4386
|
+
}
|
|
4387
|
+
// DR-063 §2: a rejected player port or a result the runtime cannot read is
|
|
4388
|
+
// a `player-failed` failure named by role; an abort is `aborted`.
|
|
4389
|
+
function decidePlayerCallFailure(error, roleId, playerId, signal) {
|
|
4390
|
+
const cause = isAbortFailure(error, signal)
|
|
4391
|
+
? ABORTED_FAILURE_CAUSE
|
|
4392
|
+
: playerFailureCause({
|
|
4393
|
+
roleId,
|
|
4394
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
4395
|
+
error,
|
|
4396
|
+
});
|
|
4397
|
+
rememberFailureCause(cause);
|
|
4398
|
+
return attachPlaybookFailureCause(error, cause);
|
|
4399
|
+
}
|
|
4400
|
+
function completeFailedStatuses(statuses, state, context) {
|
|
4401
|
+
if (state.stateId !== 'failed')
|
|
4402
|
+
return statuses;
|
|
4403
|
+
const lastError = failedStateError(context);
|
|
4404
|
+
if (lastError?.cause === undefined)
|
|
4405
|
+
return statuses;
|
|
4406
|
+
const cause = lastError.cause;
|
|
4407
|
+
return statuses.map((status) => {
|
|
4408
|
+
const data = status.data;
|
|
4409
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
4410
|
+
return status;
|
|
4411
|
+
}
|
|
4412
|
+
const recorded = data;
|
|
4413
|
+
const recordedError = recorded.lastError;
|
|
4414
|
+
if (recordedError === undefined ||
|
|
4415
|
+
recordedError === null ||
|
|
4416
|
+
typeof recordedError !== 'object' ||
|
|
4417
|
+
Array.isArray(recordedError)) {
|
|
4418
|
+
return status;
|
|
4419
|
+
}
|
|
4420
|
+
return {
|
|
4421
|
+
...status,
|
|
4422
|
+
data: snapshotJsonValue({ ...recorded, lastError: { ...recordedError, cause } }, 'failed status data'),
|
|
4423
|
+
};
|
|
4424
|
+
});
|
|
4425
|
+
}
|
|
4088
4426
|
function structuredStateTelemetryPayload(previousState, state, event, context) {
|
|
4089
4427
|
const payload = {
|
|
4090
4428
|
from: previousState?.value ?? null,
|
|
@@ -4099,7 +4437,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4099
4437
|
payload.pendingBossQuestion = pendingBossQuestion;
|
|
4100
4438
|
}
|
|
4101
4439
|
if (state.stateId === 'failed') {
|
|
4102
|
-
const lastError =
|
|
4440
|
+
const lastError = failedStateError(context);
|
|
4103
4441
|
if (lastError !== undefined)
|
|
4104
4442
|
payload.lastError = lastError;
|
|
4105
4443
|
}
|
|
@@ -4269,7 +4607,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4269
4607
|
validateBoundQuestionProjection();
|
|
4270
4608
|
}
|
|
4271
4609
|
const payload = structuredStateTelemetryPayload(previousState, state, inspectionEvent.event, context);
|
|
4272
|
-
const stateStatuses = statusesForState(state, context, inspectionEvent.event);
|
|
4610
|
+
const stateStatuses = completeFailedStatuses(statusesForState(state, context, inspectionEvent.event), state, context);
|
|
4273
4611
|
const outcomeStatuses = usesDefaultStatuses
|
|
4274
4612
|
? acceptedOutcomes.map(({ acceptedOutcome }) => ({
|
|
4275
4613
|
message: `→ ${acceptedOutcome}`,
|
|
@@ -4349,10 +4687,17 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4349
4687
|
: outcome === 'aborted'
|
|
4350
4688
|
? activeSignal?.reason
|
|
4351
4689
|
: undefined);
|
|
4690
|
+
if (failure === undefined)
|
|
4691
|
+
return { outcome, state };
|
|
4692
|
+
// DR-063 §2: a `failed` settlement publishes the same cause-completed
|
|
4693
|
+
// error the failed state's status line and telemetry publish.
|
|
4694
|
+
const normalized = outcome === 'failed'
|
|
4695
|
+
? failedStateError({ lastError: failure })
|
|
4696
|
+
: normalizeError(failure);
|
|
4352
4697
|
return {
|
|
4353
4698
|
outcome,
|
|
4354
4699
|
state,
|
|
4355
|
-
...(
|
|
4700
|
+
...(normalized === undefined ? {} : { error: normalized }),
|
|
4356
4701
|
};
|
|
4357
4702
|
}
|
|
4358
4703
|
function settledOutcome(signal) {
|
|
@@ -4561,10 +4906,36 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4561
4906
|
action: {
|
|
4562
4907
|
id: `retry:${retryEvent.type}`,
|
|
4563
4908
|
label: `Retry: ${description}`,
|
|
4909
|
+
// DR-063 §3: retrying a failed call is `ready` — the runtime cannot
|
|
4910
|
+
// see whether the Boss changed the outside world since it failed.
|
|
4911
|
+
standing: 'ready',
|
|
4564
4912
|
},
|
|
4565
4913
|
event: retryEvent,
|
|
4566
4914
|
};
|
|
4567
4915
|
}
|
|
4916
|
+
/**
|
|
4917
|
+
* DR-063 §3: reconciliation only re-reads the host's ledger, which the
|
|
4918
|
+
* control view has just done. Where no checkpoint restoration is eligible
|
|
4919
|
+
* and every unresolved envelope's boundary already holds a complete
|
|
4920
|
+
* receipt, re-reading can resolve nothing, so the action is a no-op.
|
|
4921
|
+
*/
|
|
4922
|
+
function reconciliationStanding(deferredRestoreOperationId) {
|
|
4923
|
+
if (deferredRestoreOperationId !== undefined)
|
|
4924
|
+
return { standing: 'ready' };
|
|
4925
|
+
const current = effectLedgerMirror;
|
|
4926
|
+
const envelopes = projectUnresolvedEffectEnvelopes(current);
|
|
4927
|
+
if (envelopes.length === 0)
|
|
4928
|
+
return { standing: 'ready' };
|
|
4929
|
+
const boundaryIds = envelopes.flatMap((envelope) => envelope.kind === 'boundary'
|
|
4930
|
+
? [envelope.boundaryId]
|
|
4931
|
+
: (current.logicalOperations.find(({ operationId }) => operationId === envelope.operationId)?.boundaryIds ?? []));
|
|
4932
|
+
if (boundaryIds.length === 0)
|
|
4933
|
+
return { standing: 'ready' };
|
|
4934
|
+
const complete = boundaryIds.every((boundaryId) => current.boundaries.find((candidate) => candidate.boundaryId === boundaryId)?.physicalReceipt !== undefined);
|
|
4935
|
+
return complete
|
|
4936
|
+
? { standing: 'no-op', reason: 'receipt-complete' }
|
|
4937
|
+
: { standing: 'ready' };
|
|
4938
|
+
}
|
|
4568
4939
|
function deriveControlActions(snapshot) {
|
|
4569
4940
|
// Actions derive only at the safe point the parked snapshot also
|
|
4570
4941
|
// uses — quiescent actor with status `active` and no pending nested
|
|
@@ -4596,6 +4967,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4596
4967
|
action: {
|
|
4597
4968
|
id: UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID,
|
|
4598
4969
|
label: 'Retry unresolved effect reconciliation',
|
|
4970
|
+
...reconciliationStanding(deferredRestoreOperationId),
|
|
4599
4971
|
},
|
|
4600
4972
|
unresolvedEffectAction: 'reconcile',
|
|
4601
4973
|
...(deferredRestoreOperationId === undefined
|
|
@@ -4605,6 +4977,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4605
4977
|
action: {
|
|
4606
4978
|
id: UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID,
|
|
4607
4979
|
label: 'Abandon unresolved workflow attempt',
|
|
4980
|
+
// DR-063 §3: abandonment always changes the session's standing.
|
|
4981
|
+
standing: 'ready',
|
|
4608
4982
|
},
|
|
4609
4983
|
unresolvedEffectAction: 'abandon',
|
|
4610
4984
|
});
|
|
@@ -4631,6 +5005,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4631
5005
|
action: {
|
|
4632
5006
|
id: `jump:${targetId}`,
|
|
4633
5007
|
label: `Resume from: ${description}`,
|
|
5008
|
+
// DR-063 §3: a jump always moves the machine.
|
|
5009
|
+
standing: 'ready',
|
|
4634
5010
|
},
|
|
4635
5011
|
event,
|
|
4636
5012
|
});
|
|
@@ -5017,8 +5393,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5017
5393
|
signal,
|
|
5018
5394
|
operationId: operation.operationId,
|
|
5019
5395
|
effectBoundary,
|
|
5020
|
-
operation: async (
|
|
5396
|
+
operation: async (context) => {
|
|
5397
|
+
const { playerContinuation } = context ?? {};
|
|
5021
5398
|
validateBinding(playerContinuation);
|
|
5399
|
+
// DR-062 §4: the continuation's own baseline, captured before the
|
|
5400
|
+
// FSM re-enters the bound player state that reads it.
|
|
5401
|
+
continuation.baseline = context?.baseline;
|
|
5022
5402
|
continuation.playerContinuation = selectPlayerResume(effectBoundary.roleId, boundPlayerId);
|
|
5023
5403
|
continuationStarted = true;
|
|
5024
5404
|
actor.send(event);
|
|
@@ -5407,7 +5787,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5407
5787
|
const pending = !hasUnresolvedReconciliation()
|
|
5408
5788
|
? pendingBossQuestionForState(state, context)
|
|
5409
5789
|
: undefined;
|
|
5410
|
-
const lastError =
|
|
5790
|
+
const lastError = state.stateId === 'failed'
|
|
5791
|
+
? failedStateError(context)
|
|
5792
|
+
: normalizeErrorFull(context.lastError);
|
|
5411
5793
|
const projectedContext = projectControlContext(context);
|
|
5412
5794
|
const stateDescription = !hasUnresolvedReconciliation()
|
|
5413
5795
|
? stateDescriptionFor(state)
|
|
@@ -5746,6 +6128,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5746
6128
|
activeAborts = abortReasonClassifier(signal);
|
|
5747
6129
|
activeAbortEmission = undefined;
|
|
5748
6130
|
controlPlaneError = undefined;
|
|
6131
|
+
// DR-063 §2: each turn decides its own failures, so the slot never
|
|
6132
|
+
// carries a previous turn's cause into this one.
|
|
6133
|
+
lastFailureCause = undefined;
|
|
5749
6134
|
let result;
|
|
5750
6135
|
let operationError;
|
|
5751
6136
|
// The boundary sentinel releases on every exit: a settlement defect
|
|
@@ -5779,7 +6164,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5779
6164
|
deferredPending = pendingBossQuestionForState(normalizePlaybookSnapshot(snapshot), snapshotContext);
|
|
5780
6165
|
pendingRequiresDeferredBinding =
|
|
5781
6166
|
deferredPending !== undefined &&
|
|
5782
|
-
outcomeAuthority?.governedPlayerStates[deferredPending.resumeStateId]?.needsBossReply?.repositoryDisposition === 'deferred'
|
|
6167
|
+
outcomeAuthority?.governedPlayerStates[deferredPending.resumeStateId]?.needsBossReply?.repositoryDisposition === 'deferred' &&
|
|
6168
|
+
!pendingHasExactUnchangedOrigin(deferredPending);
|
|
5783
6169
|
deferredOperation =
|
|
5784
6170
|
!pendingRequiresDeferredBinding
|
|
5785
6171
|
? undefined
|