@sublang/playbook 14.1.0 → 15.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.
Files changed (32) hide show
  1. package/README.md +2 -2
  2. package/docs/configuration.md +15 -9
  3. package/docs/embedding.md +7 -5
  4. package/package.json +3 -1
  5. package/reference/sdlc/code.md +1 -1
  6. package/reference/sdlc/code.playbook/bin/launch-config.js +55 -16
  7. package/reference/sdlc/code.playbook/bin/playbook.js +14 -9
  8. package/reference/sdlc/code.playbook/bin/repository-effects.js +54 -13
  9. package/reference/sdlc/code.playbook/bin/run.js +12 -8
  10. package/reference/sdlc/code.playbook/code.fsm.js +1 -1
  11. package/reference/sdlc/code.playbook/code.fsm.ts +1 -1
  12. package/reference/sdlc/code.playbook/code.gears.md +2 -2
  13. package/reference/sdlc/decide.md +1 -1
  14. package/reference/sdlc/decide.playbook/decide.fsm.js +1 -1
  15. package/reference/sdlc/decide.playbook/decide.fsm.ts +1 -1
  16. package/reference/sdlc/decide.playbook/decide.gears.md +1 -1
  17. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +1 -9
  18. package/reference/sdlc/decide.playbook/decide.playbook.js +158 -32
  19. package/reference/sdlc/decide.playbook/decide.playbook.ts +229 -46
  20. package/reference/sdlc/review.md +1 -1
  21. package/reference/sdlc/review.playbook/review.fsm.js +1 -1
  22. package/reference/sdlc/review.playbook/review.fsm.ts +1 -1
  23. package/reference/sdlc/review.playbook/review.gears.md +1 -1
  24. package/slc/prefix-prompts.mjs +190 -0
  25. package/slc/prefix.md +200 -0
  26. package/slc/slc.pin-inputs.json +1 -0
  27. package/src/xstate-playbook-runtime.d.ts +35 -1
  28. package/src/xstate-playbook-runtime.js +82 -47
  29. package/src/xstate-playbook-runtime.ts +107 -60
  30. package/src/xstate-runtime.d.ts +3 -1
  31. package/src/xstate-runtime.js +9 -2
  32. package/src/xstate-runtime.ts +12 -2
@@ -28,18 +28,27 @@ import {
28
28
  } from '../../../src/accepted-outcome.js';
29
29
 
30
30
  import {
31
+ ABORTED_FAILURE_CAUSE,
31
32
  assertJsonSafe,
32
33
  assertPlaybookEffectLedger,
33
34
  assertPlaybookRuntimeSnapshot,
35
+ attachPlaybookFailureCause,
34
36
  combineAbortSignals,
35
37
  composePlayerContinuation,
38
+ createFailureCauseRetention,
36
39
  createNestedPlaybookBridge,
37
40
  detachPersistedMachineSnapshot,
41
+ effectAuthorizedPreExistingBlock,
42
+ governedSettlementCause,
38
43
  normalizeError,
44
+ normalizeErrorCompact,
45
+ normalizeErrorFull,
39
46
  normalizePlaybookSnapshot,
40
47
  PlaybookSemanticCandidateStructureError,
48
+ playerFailureCause,
41
49
  reconcilePlaybookSemanticEvidence,
42
50
  renderGovernedOutcomeContract,
51
+ runtimeDefectCause,
43
52
  snapshotJsonValue,
44
53
  snapshotPlaybookSession,
45
54
  terminalOutcomesFromMachine,
@@ -80,6 +89,7 @@ import type {
80
89
  PlaybookEffectBoundaryStart,
81
90
  PlaybookEffectLedger,
82
91
  PlaybookEffectLedgerCapability,
92
+ PlaybookFailureCause,
83
93
  PlaybookPendingBossQuestion,
84
94
  PlaybookPendingCall,
85
95
  PlaybookPorts,
@@ -699,20 +709,6 @@ function combineSignals(
699
709
  return combineAbortSignals(a, b);
700
710
  }
701
711
 
702
- function normalizeErrorCompact(
703
- err: unknown,
704
- ): { name: string; message: string } | undefined {
705
- if (err === undefined || err === null) return undefined;
706
- const normalized = normalizeError(err);
707
- return { name: normalized.name, message: normalized.message };
708
- }
709
-
710
- function normalizeErrorFull(
711
- err: unknown,
712
- ): { name: string; message: string; stack?: string } | undefined {
713
- return err === undefined || err === null ? undefined : normalizeError(err);
714
- }
715
-
716
712
  // DR-028's unified empty predicate: a missing, empty, or whitespace-only
717
713
  // `finalText` on an `ok` player result is the one shape that earns exactly
718
714
  // one corrective re-ask before the existing failure path applies. Mirrors
@@ -1240,6 +1236,7 @@ function telemetryPayload(
1240
1236
  event: unknown,
1241
1237
  context: Record<string, unknown>,
1242
1238
  hiddenQuestionId?: string,
1239
+ failedError?: NormalizedError,
1243
1240
  ): JsonValue {
1244
1241
  const pendingBossQuestions = pendingQuestionsForState(
1245
1242
  state,
@@ -1254,7 +1251,7 @@ function telemetryPayload(
1254
1251
  state,
1255
1252
  ...(pendingBossQuestions.length > 0 ? { pendingBossQuestions } : {}),
1256
1253
  ...(state.activeStateIds.includes('failed')
1257
- ? { lastError: normalizeErrorFull(context.lastError) ?? null }
1254
+ ? { lastError: failedError ?? normalizeErrorFull(context.lastError) ?? null }
1258
1255
  : {}),
1259
1256
  } satisfies Record<string, unknown>;
1260
1257
  assertJsonSafe(payload);
@@ -1278,6 +1275,7 @@ interface ActiveDeferredContinuation {
1278
1275
  readonly result: DeferredValue<PlayerResult>;
1279
1276
  readonly acknowledged: DeferredValue<PlayerResult>;
1280
1277
  playerContinuation?: string | false;
1278
+ baseline?: PlaybookRepositoryReceipt['baseline'];
1281
1279
  input?: PlayerInput;
1282
1280
  playerId?: string;
1283
1281
  }
@@ -1350,6 +1348,85 @@ function createDecidePlaybookRuntime(
1350
1348
  const semanticCompletionQueue = new PQueue({ concurrency: 1 });
1351
1349
  const governedOutputsByBoundaryId = new Map<string, PlayerOutput>();
1352
1350
  const governedFailuresByBoundaryId = new Map<string, Error>();
1351
+ // DR-063 §2: the causes decided for thrown values that cannot carry the
1352
+ // marker — a string, a frozen error — kept by identity for as long as the
1353
+ // failure stands, so every surface that reads the FSM's own `lastError`
1354
+ // publishes the cause decided for exactly that value (mirrors the shared
1355
+ // factory).
1356
+ const failureCauses = createFailureCauseRetention();
1357
+ // DR-063 §2: each decided failure is one occurrence in decision order, kept
1358
+ // by the player call or governed boundary it was decided for, so a cohort
1359
+ // names the member that failed first whatever value its siblings threw.
1360
+ type FailureOccurrence = {
1361
+ readonly cause: PlaybookFailureCause;
1362
+ readonly sequence: number;
1363
+ };
1364
+ let failureSequence = 0;
1365
+ const playerCallFailures = new Map<string, FailureOccurrence>();
1366
+ const governedFailureOccurrences = new Map<string, FailureOccurrence>();
1367
+ const recordFailure = (
1368
+ occurrences: Map<string, FailureOccurrence>,
1369
+ key: string,
1370
+ cause: PlaybookFailureCause,
1371
+ ): void => {
1372
+ occurrences.set(key, { cause, sequence: ++failureSequence });
1373
+ };
1374
+ const failedStateError = (
1375
+ context: Record<string, unknown>,
1376
+ ): NormalizedError | undefined => {
1377
+ const normalized = normalizeErrorFull(context.lastError);
1378
+ if (normalized === undefined || normalized.cause !== undefined) {
1379
+ return normalized;
1380
+ }
1381
+ return {
1382
+ ...normalized,
1383
+ cause:
1384
+ failureCauses.causeOf(context.lastError) ??
1385
+ runtimeDefectCause(normalized.message),
1386
+ };
1387
+ };
1388
+ const playerResultFailureCause = (
1389
+ roleId: RoleId,
1390
+ playerId: string | undefined,
1391
+ result: PlayerResult,
1392
+ ): PlaybookFailureCause =>
1393
+ result.status === 'aborted'
1394
+ ? ABORTED_FAILURE_CAUSE
1395
+ : playerFailureCause({
1396
+ roleId,
1397
+ ...(playerId === undefined ? {} : { playerId }),
1398
+ error:
1399
+ result.error ??
1400
+ `callPlayer status ${JSON.stringify(result.status)}`,
1401
+ });
1402
+ // A rejected player port or a result the runtime cannot read is a
1403
+ // `player-failed` failure named by role; the turn's own abort is `aborted`.
1404
+ // A cancellation the runtime itself issued — a proposal cancelled because
1405
+ // its sibling failed — is not this player's failure and decides nothing, so
1406
+ // the originator's cause stands; every other failure is decided afresh and
1407
+ // recorded as this call's occurrence, whatever an earlier failure threw.
1408
+ const decidePlayerCallFailure = (
1409
+ error: unknown,
1410
+ roleId: RoleId,
1411
+ playerId: string | undefined,
1412
+ signal: AbortSignal,
1413
+ callId: string,
1414
+ ): void => {
1415
+ const turn = currentSignal;
1416
+ const turnAborted = turn !== undefined && turn.aborted;
1417
+ if (!turnAborted && signal.aborted) return;
1418
+ const cause =
1419
+ turn !== undefined && turn.aborted && Object.is(error, turn.reason)
1420
+ ? ABORTED_FAILURE_CAUSE
1421
+ : playerFailureCause({
1422
+ roleId,
1423
+ ...(playerId === undefined ? {} : { playerId }),
1424
+ error,
1425
+ });
1426
+ attachPlaybookFailureCause(error, cause);
1427
+ failureCauses.retain(error, cause);
1428
+ recordFailure(playerCallFailures, callId, cause);
1429
+ };
1353
1430
  const governedEvidenceByBoundaryId = new Map<
1354
1431
  string,
1355
1432
  { readonly finalText: string; readonly semanticCandidate?: JsonValue }
@@ -2154,6 +2231,7 @@ function createDecidePlaybookRuntime(
2154
2231
  continuation?: {
2155
2232
  readonly resume?: string | false;
2156
2233
  readonly callId: string;
2234
+ readonly preExistingBlock?: string;
2157
2235
  },
2158
2236
  ): Promise<DeferredContinuationEnvelope> => {
2159
2237
  const aborts = abortReasonClassifier(signal);
@@ -2165,7 +2243,13 @@ function createDecidePlaybookRuntime(
2165
2243
  const roleId = input.role;
2166
2244
  const playerId = resolvedPlayerId(roleId);
2167
2245
  const playerKey = continuationKey(roleId, playerId);
2168
- const freshPrompt = composeInvocationPrompt(input);
2246
+ // DR-062 §4: composed from this call's own baseline observation, so the
2247
+ // prompt the trace records is the prompt the player is sent.
2248
+ const withPreExisting = (text: string): string =>
2249
+ continuation?.preExistingBlock === undefined
2250
+ ? text
2251
+ : `${text}\n\n${continuation.preExistingBlock}`;
2252
+ const freshPrompt = withPreExisting(composeInvocationPrompt(input));
2169
2253
  let resume: PlayerCallOptions['resume'];
2170
2254
  try {
2171
2255
  signal.throwIfAborted();
@@ -2178,7 +2262,10 @@ function createDecidePlaybookRuntime(
2178
2262
  latchControlPlaneError(error, signal);
2179
2263
  throw error;
2180
2264
  }
2181
- const prompt = resume === false ? freshPrompt : composeInvocationPrompt(input, true);
2265
+ const prompt =
2266
+ resume === false
2267
+ ? freshPrompt
2268
+ : withPreExisting(composeInvocationPrompt(input, true));
2182
2269
  const callId =
2183
2270
  continuation?.callId ?? `player-${++playerCallSequence}`;
2184
2271
  const identity = {
@@ -2244,6 +2331,7 @@ function createDecidePlaybookRuntime(
2244
2331
  // A rejected call produced no authoritative result, so the previous
2245
2332
  // token remains untouched. This also covers a coder promise that
2246
2333
  // resolves after its invocation signal was cancelled.
2334
+ decidePlayerCallFailure(error, roleId, playerId, signal, callId);
2247
2335
  latchControlPlaneError(error, signal);
2248
2336
  try {
2249
2337
  await emitFailure(error);
@@ -2257,6 +2345,7 @@ function createDecidePlaybookRuntime(
2257
2345
  try {
2258
2346
  result = validatePlayerResult(rawResult);
2259
2347
  } catch (error) {
2348
+ decidePlayerCallFailure(error, roleId, playerId, signal, callId);
2260
2349
  latchControlPlaneError(error, signal);
2261
2350
  try {
2262
2351
  await emitFailure(error);
@@ -2348,17 +2437,29 @@ function createDecidePlaybookRuntime(
2348
2437
  };
2349
2438
  };
2350
2439
 
2440
+ // DR-063 §1/§2: the cause of an unresolved governed settlement is decided
2441
+ // here — a reconciled mismatch supplies its receipt-read cause, an abort or
2442
+ // a refused adjudication its own code, anything else a runtime defect — and
2443
+ // rides the failure the boundary throws.
2351
2444
  const unresolvedGovernedEvidence = (
2352
2445
  boundaryId: string,
2353
2446
  reason: string,
2354
2447
  error?: unknown,
2448
+ cause?: PlaybookFailureCause,
2355
2449
  ): void => {
2356
- governedFailuresByBoundaryId.set(
2357
- boundaryId,
2450
+ const aborted =
2451
+ error !== undefined &&
2452
+ currentSignal?.aborted === true &&
2453
+ Object.is(error, currentSignal.reason);
2454
+ const decided = governedSettlementCause(reason, error, aborted, cause);
2455
+ const failure =
2358
2456
  error instanceof Error
2359
2457
  ? error
2360
- : new Error(`DECIDE governed outcome remains unresolved: ${reason}`),
2361
- );
2458
+ : new Error(`DECIDE governed outcome remains unresolved: ${reason}`);
2459
+ attachPlaybookFailureCause(failure, decided);
2460
+ failureCauses.retain(failure, decided);
2461
+ recordFailure(governedFailureOccurrences, boundaryId, decided);
2462
+ governedFailuresByBoundaryId.set(boundaryId, failure);
2362
2463
  };
2363
2464
 
2364
2465
  const spendSemanticCorrectionBudget = async (
@@ -2706,6 +2807,8 @@ function createDecidePlaybookRuntime(
2706
2807
  unresolvedGovernedEvidence(
2707
2808
  boundary.boundaryId,
2708
2809
  reconciliation.reason,
2810
+ undefined,
2811
+ reconciliation.cause,
2709
2812
  );
2710
2813
  return { finalText, semanticCandidate, unresolved: true as const };
2711
2814
  }
@@ -2952,11 +3055,23 @@ function createDecidePlaybookRuntime(
2952
3055
  envelope.result.status !== 'ok' &&
2953
3056
  !cohortOperations.signal.aborted
2954
3057
  ) {
2955
- cohortOperations.abort(
3058
+ // DR-063 §2: the cancellation carries the cause of the result
3059
+ // that caused it, recorded as this member's occurrence, so the
3060
+ // cohort fails for this member.
3061
+ const cause = playerResultFailureCause(
3062
+ roleId,
3063
+ resolvedPlayerId(roleId),
3064
+ envelope.result,
3065
+ );
3066
+ const cancellation = attachPlaybookFailureCause(
2956
3067
  new Error(
2957
3068
  `DECIDE proposal cohort ${roleId} returned status ${JSON.stringify(envelope.result.status)}`,
2958
3069
  ),
3070
+ cause,
2959
3071
  );
3072
+ failureCauses.retain(cancellation, cause);
3073
+ recordFailure(playerCallFailures, member.callId, cause);
3074
+ cohortOperations.abort(cancellation);
2960
3075
  }
2961
3076
  return envelope.result;
2962
3077
  } catch (error) {
@@ -3037,12 +3152,43 @@ function createDecidePlaybookRuntime(
3037
3152
  for (const result of acknowledgedResults.values()) {
3038
3153
  governedPlayerOutputs.delete(result);
3039
3154
  }
3040
- throw acknowledgementFailures.length === 1
3041
- ? acknowledgementFailures[0]
3042
- : new AggregateError(
3043
- acknowledgementFailures,
3044
- 'DECIDE proposal cohort reconciliation failed',
3045
- );
3155
+ const failure =
3156
+ acknowledgementFailures.length === 1
3157
+ ? acknowledgementFailures[0]
3158
+ : new AggregateError(
3159
+ acknowledgementFailures,
3160
+ 'DECIDE proposal cohort reconciliation failed',
3161
+ );
3162
+ // DR-063 §2: a cohort fails for the member whose failure was decided
3163
+ // first — a rejected or non-`ok` player call, a refused adjudication,
3164
+ // a mismatched receipt — in decision order, not in the order the
3165
+ // players finished or the values they threw. That cause rides every
3166
+ // error the failure can surface as: the cancellation, each member's
3167
+ // own rejection, and the aggregate of both, never an abort.
3168
+ const origin = cohortOperations.signal.aborted
3169
+ ? cohortOperations.signal.reason
3170
+ : undefined;
3171
+ const first = ROLE_IDS.flatMap((roleId) => {
3172
+ const member = members[roleId];
3173
+ const occurrence =
3174
+ member === undefined
3175
+ ? undefined
3176
+ : (playerCallFailures.get(member.callId) ??
3177
+ governedFailureOccurrences.get(member.effectBoundary.boundaryId));
3178
+ return occurrence === undefined ? [] : [occurrence];
3179
+ }).sort((left, right) => left.sequence - right.sequence)[0];
3180
+ const originCause =
3181
+ first?.cause ??
3182
+ failureCauses.causeOf(failure) ??
3183
+ (origin === undefined ? undefined : failureCauses.causeOf(origin));
3184
+ if (originCause !== undefined) {
3185
+ for (const candidate of [failure, origin, ...acknowledgementFailures]) {
3186
+ if (candidate === undefined) continue;
3187
+ attachPlaybookFailureCause(candidate, originCause);
3188
+ failureCauses.retain(candidate, originCause);
3189
+ }
3190
+ }
3191
+ throw failure;
3046
3192
  }
3047
3193
  for (const roleId of releaseOrder) {
3048
3194
  const member = members[roleId]!;
@@ -3120,6 +3266,10 @@ function createDecidePlaybookRuntime(
3120
3266
  const envelope = await runPlayerCall(input, signal, {
3121
3267
  callId: active.effectBoundary.callId,
3122
3268
  resume: active.playerContinuation,
3269
+ preExistingBlock: effectAuthorizedPreExistingBlock(
3270
+ active.effectBoundary,
3271
+ active.baseline,
3272
+ ),
3123
3273
  });
3124
3274
  active.result.resolve(envelope.result);
3125
3275
  const acknowledgedResult = await active.acknowledged.promise;
@@ -3145,8 +3295,16 @@ function createDecidePlaybookRuntime(
3145
3295
  const settled = await deferredEffects.repository.runExclusive({
3146
3296
  signal,
3147
3297
  effectBoundary,
3148
- operation: async () => {
3149
- envelope = await runPlayerCall(input, signal, { callId });
3298
+ // DR-062 §4: the call's own baseline, supplied by the host that
3299
+ // captured it under this exclusive claim.
3300
+ operation: async (context) => {
3301
+ envelope = await runPlayerCall(input, signal, {
3302
+ callId,
3303
+ preExistingBlock: effectAuthorizedPreExistingBlock(
3304
+ effectBoundary,
3305
+ context?.baseline,
3306
+ ),
3307
+ });
3150
3308
  return envelope.result;
3151
3309
  },
3152
3310
  completeEffectBoundary: completionEvidenceFor(
@@ -3214,12 +3372,18 @@ function createDecidePlaybookRuntime(
3214
3372
  ));
3215
3373
  }
3216
3374
  if (result.status !== 'ok') {
3217
- throw new Error(
3218
- `${roleLabel(roleId)}${
3219
- playerId === undefined ? '' : ` (${playerId})`
3220
- } returned status "${result.status}"${
3221
- result.error ? `: ${result.error}` : ''
3222
- }`,
3375
+ // DR-063 §2: the failure Error built for a non-`ok` result carries
3376
+ // the cause decided for that exact result.
3377
+ const cause = playerResultFailureCause(roleId, playerId, result);
3378
+ throw attachPlaybookFailureCause(
3379
+ new Error(
3380
+ `${roleLabel(roleId)}${
3381
+ playerId === undefined ? '' : ` (${playerId})`
3382
+ } returned status "${result.status}"${
3383
+ result.error ? `: ${result.error}` : ''
3384
+ }`,
3385
+ ),
3386
+ cause,
3223
3387
  );
3224
3388
  }
3225
3389
  const finalText = result.finalText ?? '';
@@ -3365,6 +3529,9 @@ function createDecidePlaybookRuntime(
3365
3529
  hiddenDeferredOperationId === undefined
3366
3530
  ? undefined
3367
3531
  : 'commitCoderProposal',
3532
+ state.activeStateIds.includes('failed')
3533
+ ? failedStateError(context)
3534
+ : undefined,
3368
3535
  );
3369
3536
  const describedFsmPayload = snapshotJsonValue(
3370
3537
  fsmPayload,
@@ -3444,7 +3611,7 @@ function createDecidePlaybookRuntime(
3444
3611
  }
3445
3612
  const lastError =
3446
3613
  activeStateId === 'failed'
3447
- ? normalizeErrorCompact(context.lastError)
3614
+ ? normalizeErrorCompact(failedStateError(context))
3448
3615
  : undefined;
3449
3616
  const description = STATE_DESCRIPTIONS[activeStateId];
3450
3617
  if (description === undefined) continue;
@@ -3456,7 +3623,9 @@ function createDecidePlaybookRuntime(
3456
3623
  ? '◆ workflow failed; awaiting Boss recovery.'
3457
3624
  : `⤷ ${roleLabel(roleState.role)}: ${description}`,
3458
3625
  activeStateId,
3459
- lastError === undefined ? undefined : { lastError },
3626
+ lastError === undefined
3627
+ ? undefined
3628
+ : snapshotJsonValue({ lastError }, 'DECIDE failed status data'),
3460
3629
  );
3461
3630
  }
3462
3631
  } catch (error) {
@@ -3640,7 +3809,7 @@ function createDecidePlaybookRuntime(
3640
3809
  signal,
3641
3810
  operationId,
3642
3811
  effectBoundary,
3643
- operation: async ({ playerContinuation }) => {
3812
+ operation: async ({ playerContinuation, baseline }) => {
3644
3813
  if (
3645
3814
  playerContinuation !== false &&
3646
3815
  (typeof playerContinuation !== 'string' ||
@@ -3651,6 +3820,8 @@ function createDecidePlaybookRuntime(
3651
3820
  );
3652
3821
  }
3653
3822
  active.playerContinuation = playerContinuation;
3823
+ // DR-062 §4: the continuation's own baseline.
3824
+ active.baseline = baseline;
3654
3825
  readiness.resolve({ status: 'ready' });
3655
3826
  return active.result.promise;
3656
3827
  },
@@ -3957,7 +4128,10 @@ function createDecidePlaybookRuntime(
3957
4128
  const actions = deriveControlCandidates().map(({ action }) => ({
3958
4129
  ...action,
3959
4130
  }));
3960
- const lastError = normalizeErrorFull(context.lastError);
4131
+ const lastError =
4132
+ state.stateId === 'failed'
4133
+ ? failedStateError(context)
4134
+ : normalizeErrorFull(context.lastError);
3961
4135
  const stateDescription =
3962
4136
  unresolved || state.stateId === undefined
3963
4137
  ? undefined
@@ -4035,6 +4209,8 @@ function createDecidePlaybookRuntime(
4035
4209
  const turnId = ++turnSequence;
4036
4210
  const callId = `apply-${++applyCallSequence}`;
4037
4211
  currentTurnId = turnId;
4212
+ playerCallFailures.clear();
4213
+ governedFailureOccurrences.clear();
4038
4214
  currentSignal = signal;
4039
4215
  currentAborts = abortReasonClassifier(signal);
4040
4216
  controlPlaneError = undefined;
@@ -4306,7 +4482,7 @@ function createDecidePlaybookRuntime(
4306
4482
  return abortedResult(signal);
4307
4483
  }
4308
4484
  if (state.activeStateIds.includes('failed')) {
4309
- const error = normalizeErrorFull(context.lastError);
4485
+ const error = failedStateError(context);
4310
4486
  return {
4311
4487
  outcome: 'failed',
4312
4488
  state,
@@ -4504,12 +4680,17 @@ function createDecidePlaybookRuntime(
4504
4680
  }
4505
4681
  const state = currentState();
4506
4682
  if (state.status !== 'active' || !state.quiescent) return undefined;
4507
- const machine = detachPersistedMachineSnapshot(
4508
- actor.getPersistedSnapshot(),
4509
- );
4510
4683
  const context = (
4511
4684
  actor.getSnapshot() as SnapshotFrom<typeof decideMachine>
4512
4685
  ).context as unknown as Record<string, unknown>;
4686
+ // DR-063 §2: the failed state's `lastError` is persisted as every live
4687
+ // surface publishes it, completed from the runtime's own cause slot.
4688
+ const machine = detachPersistedMachineSnapshot(
4689
+ actor.getPersistedSnapshot(),
4690
+ state.stateId === 'failed'
4691
+ ? { lastError: failedStateError(context) }
4692
+ : {},
4693
+ );
4513
4694
  if (deferredEffects !== undefined) {
4514
4695
  synchronizeDeferredProjection(readEffectLedger());
4515
4696
  }
@@ -4678,6 +4859,8 @@ function createDecidePlaybookRuntime(
4678
4859
 
4679
4860
  const turnId = ++turnSequence;
4680
4861
  currentTurnId = turnId;
4862
+ playerCallFailures.clear();
4863
+ governedFailureOccurrences.clear();
4681
4864
  currentSignal = turn.signal;
4682
4865
  currentAborts = abortReasonClassifier(turn.signal);
4683
4866
  controlPlaneError = undefined;
@@ -4806,7 +4989,7 @@ function createDecidePlaybookRuntime(
4806
4989
  : {
4807
4990
  outcome: 'failed',
4808
4991
  state,
4809
- error: normalizeErrorFull(effectiveError) ?? {
4992
+ error: failedStateError({ lastError: effectiveError }) ?? {
4810
4993
  name: 'Error',
4811
4994
  message: String(effectiveError),
4812
4995
  },
@@ -4830,7 +5013,7 @@ function createDecidePlaybookRuntime(
4830
5013
  result = {
4831
5014
  outcome: drainAborted ? 'aborted' : 'failed',
4832
5015
  state,
4833
- error: normalizeErrorFull(effectiveError) ?? {
5016
+ error: failedStateError({ lastError: effectiveError }) ?? {
4834
5017
  name: 'Error',
4835
5018
  message: String(effectiveError),
4836
5019
  },
@@ -101,7 +101,7 @@ If you accept any item, make minimal changes and add one new review-fix commit;
101
101
  Follow @specs/packages/git.md.
102
102
  Make the commit message explain concisely what changed and why, including relevant verification.
103
103
  Identify every new commit you make.
104
- Coder is <coder-llm>; Reviewer is <reviewer-llm>.
104
+ Credit every AI that contributed to this commit: Coder <coder-llm> and Reviewer <reviewer-llm>, whose findings it answers.
105
105
 
106
106
  If you reject every item, change nothing and make no commit.
107
107
  Report every disposition, all relevant run results, and every rebuttal.
@@ -67,7 +67,7 @@ const CODER_DISPOSITION_PROMPT = [
67
67
  'Follow @specs/packages/git.md.',
68
68
  'Make the commit message explain concisely what changed and why, including relevant verification.',
69
69
  'Identify every new commit you make.',
70
- 'Coder is <coder-llm>; Reviewer is <reviewer-llm>.',
70
+ 'Credit every AI that contributed to this commit: Coder <coder-llm> and Reviewer <reviewer-llm>, whose findings it answers.',
71
71
  '',
72
72
  'If you reject every item, change nothing and make no commit.',
73
73
  'Report every disposition, all relevant run results, and every rebuttal.',
@@ -156,7 +156,7 @@ const CODER_DISPOSITION_PROMPT = [
156
156
  'Follow @specs/packages/git.md.',
157
157
  'Make the commit message explain concisely what changed and why, including relevant verification.',
158
158
  'Identify every new commit you make.',
159
- 'Coder is <coder-llm>; Reviewer is <reviewer-llm>.',
159
+ 'Credit every AI that contributed to this commit: Coder <coder-llm> and Reviewer <reviewer-llm>, whose findings it answers.',
160
160
  '',
161
161
  'If you reject every item, change nothing and make no commit.',
162
162
  'Report every disposition, all relevant run results, and every rebuttal.',
@@ -65,7 +65,7 @@ When Reviewer raises or keeps any finding, Captain shall relay the caller input
65
65
  > Follow @specs/packages/git.md.
66
66
  > Make the commit message explain concisely what changed and why, including relevant verification.
67
67
  > Identify every new commit you make.
68
- > Coder is <coder-llm>; Reviewer is <reviewer-llm>.
68
+ > Credit every AI that contributed to this commit: Coder <coder-llm> and Reviewer <reviewer-llm>, whose findings it answers.
69
69
  >
70
70
  > If you reject every item, change nothing and make no commit.
71
71
  > Report every disposition, all relevant run results, and every rebuttal.