@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.
Files changed (56) hide show
  1. package/README.md +5 -5
  2. package/docs/cli.md +14 -14
  3. package/docs/configuration.md +10 -2
  4. package/docs/embedding.md +20 -1
  5. package/package.json +6 -1
  6. package/reference/sdlc/branch.playbook/branch.fsm.d.ts +1 -1
  7. package/reference/sdlc/code.playbook/bin/launch-config.js +88 -5
  8. package/reference/sdlc/code.playbook/bin/playbook.js +4 -0
  9. package/reference/sdlc/code.playbook/bin/repository-effects.js +198 -17
  10. package/reference/sdlc/code.playbook/bin/run.js +4 -0
  11. package/reference/sdlc/code.playbook/bin/session-store.js +2 -2
  12. package/reference/sdlc/code.playbook/code.fsm.d.ts +1 -1
  13. package/reference/sdlc/code.playbook/host-capabilities.d.ts +6 -0
  14. package/reference/sdlc/code.playbook/playbook-captain.d.ts +3 -1
  15. package/reference/sdlc/code.playbook/playbook-captain.js +374 -20
  16. package/reference/sdlc/code.playbook/playbook-captain.ts +478 -38
  17. package/reference/sdlc/code.playbook/session-host.d.ts +6 -3
  18. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +1 -1
  19. package/reference/sdlc/decide.playbook/decide.playbook.js +25 -1
  20. package/reference/sdlc/decide.playbook/decide.playbook.ts +35 -2
  21. package/reference/sdlc/dev.md +15 -8
  22. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +1 -1
  23. package/reference/sdlc/dev.playbook/dev.fsm.js +18 -12
  24. package/reference/sdlc/dev.playbook/dev.fsm.ts +18 -13
  25. package/reference/sdlc/dev.playbook/dev.gears.md +19 -12
  26. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +3 -10
  27. package/reference/sdlc/dev.playbook/dev.playbook.js +6 -6
  28. package/reference/sdlc/dev.playbook/dev.playbook.ts +8 -9
  29. package/reference/sdlc/dev.playbook/dev.registry.js +1 -1
  30. package/reference/sdlc/dev.playbook/dev.registry.ts +1 -1
  31. package/reference/sdlc/pr.md +17 -4
  32. package/reference/sdlc/pr.playbook/pr.fsm.d.ts +18 -4
  33. package/reference/sdlc/pr.playbook/pr.fsm.js +49 -18
  34. package/reference/sdlc/pr.playbook/pr.fsm.ts +63 -20
  35. package/reference/sdlc/pr.playbook/pr.gears.md +20 -7
  36. package/reference/sdlc/pr.playbook/pr.playbook.js +2 -1
  37. package/reference/sdlc/pr.playbook/pr.playbook.ts +2 -1
  38. package/reference/sdlc/review.playbook/review.fsm.d.ts +1 -1
  39. package/slc/experiments/fsm-scaffold-guidance.md +31 -0
  40. package/slc/gears2fsm.md +145 -58
  41. package/slc/link.md +190 -7
  42. package/slc/materialize-link.mjs +460 -0
  43. package/slc/optimize.md +46 -6
  44. package/slc/scaffold-fsm.mjs +225 -0
  45. package/slc/slc.pin-inputs.json +13 -0
  46. package/slc/text2gears.md +38 -3
  47. package/slc/workflow-contracts.json +371 -0
  48. package/src/runtime.d.ts +70 -0
  49. package/src/runtime.js +232 -1
  50. package/src/runtime.ts +358 -1
  51. package/src/xstate-playbook-runtime.d.ts +9 -10
  52. package/src/xstate-playbook-runtime.js +407 -21
  53. package/src/xstate-playbook-runtime.ts +514 -20
  54. package/src/xstate-runtime.d.ts +9 -1
  55. package/src/xstate-runtime.js +299 -19
  56. package/src/xstate-runtime.ts +362 -21
@@ -2,6 +2,7 @@
2
2
  // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
3
 
4
4
  import { createHash } from 'node:crypto';
5
+ import { isDeepStrictEqual } from 'node:util';
5
6
  import {
6
7
  fromPromise,
7
8
  waitFor,
@@ -10,10 +11,13 @@ import {
10
11
  type SnapshotFrom,
11
12
  } from 'xstate';
12
13
 
14
+ import { assertPlaybookFailureCause } from './runtime.js';
13
15
  import type {
14
16
  CaptainResult,
15
17
  JsonValue,
16
18
  NormalizedError,
19
+ PlaybookFailureCause,
20
+ PlaybookFailurePaths,
17
21
  PlaybookCallRequest,
18
22
  PlaybookCallResult,
19
23
  PlaybookCallStart,
@@ -634,7 +638,74 @@ export function hiddenControlEnvelope(prompt: string): string {
634
638
  ].join('\n\n');
635
639
  }
636
640
 
641
+ /**
642
+ * DR-063 §2: the marker property the runtime writes on the error it marks as
643
+ * the FSM failure. It is non-enumerable, so decorating a host-owned error
644
+ * changes nothing a host can observe by enumeration or serialization.
645
+ */
646
+ const PLAYBOOK_FAILURE_CAUSE_PROPERTY = 'playbookCause';
647
+
648
+ /**
649
+ * Attach one validated failure cause to an error object, returning it. A cause
650
+ * the closed validator rejects is dropped rather than published, and an error
651
+ * that refuses the property keeps its own cause-free identity (DR-063 §1).
652
+ */
653
+ export function attachPlaybookFailureCause<E>(error: E, cause: unknown): E {
654
+ if (typeof error !== 'object' || error === null) return error;
655
+ let validated: PlaybookFailureCause;
656
+ try {
657
+ validated = assertPlaybookFailureCause(cause);
658
+ } catch {
659
+ return error;
660
+ }
661
+ try {
662
+ Object.defineProperty(error, PLAYBOOK_FAILURE_CAUSE_PROPERTY, {
663
+ value: validated,
664
+ enumerable: false,
665
+ writable: true,
666
+ configurable: true,
667
+ });
668
+ } catch {
669
+ // A frozen or hostile error keeps no cause; the runtime's own slot still
670
+ // supplies one at the failed state.
671
+ }
672
+ return error;
673
+ }
674
+
675
+ /**
676
+ * DR-063 §2: a normalized error carries `cause` exactly when the underlying
677
+ * error carries a valid one. An `Error` carries it on the marker property the
678
+ * runtime writes; an already-normalized record carries it as `cause`, so the
679
+ * cause survives every re-normalization along the durable round trip.
680
+ */
681
+ function playbookFailureCauseOf(
682
+ error: unknown,
683
+ ): PlaybookFailureCause | undefined {
684
+ if (typeof error !== 'object' || error === null) return undefined;
685
+ let candidate: unknown;
686
+ try {
687
+ if (Object.hasOwn(error, PLAYBOOK_FAILURE_CAUSE_PROPERTY)) {
688
+ candidate = (error as Record<string, unknown>)[
689
+ PLAYBOOK_FAILURE_CAUSE_PROPERTY
690
+ ];
691
+ } else if (!(error instanceof Error) && Object.hasOwn(error, 'cause')) {
692
+ candidate = (error as Record<string, unknown>).cause;
693
+ }
694
+ } catch {
695
+ return undefined;
696
+ }
697
+ if (candidate === undefined) return undefined;
698
+ try {
699
+ return assertPlaybookFailureCause(candidate);
700
+ } catch {
701
+ return undefined;
702
+ }
703
+ }
704
+
637
705
  export function normalizeError(error: unknown): NormalizedError {
706
+ const cause = playbookFailureCauseOf(error);
707
+ const withCause = (normalized: NormalizedError): NormalizedError =>
708
+ cause === undefined ? normalized : { ...normalized, cause };
638
709
  if (error instanceof Error) {
639
710
  let name = 'Error';
640
711
  let message = 'Unknown error';
@@ -656,11 +727,11 @@ export function normalizeError(error: unknown): NormalizedError {
656
727
  } catch {
657
728
  // A stack is optional at the public boundary.
658
729
  }
659
- return {
730
+ return withCause({
660
731
  name,
661
732
  message,
662
733
  ...(stack ? { stack } : {}),
663
- };
734
+ });
664
735
  }
665
736
  if (typeof error === 'string') {
666
737
  return { name: 'Error', message: error };
@@ -668,21 +739,21 @@ export function normalizeError(error: unknown): NormalizedError {
668
739
  try {
669
740
  assertJsonSafe(error);
670
741
  if (isRecord(error) && typeof error.message === 'string') {
671
- return {
742
+ return withCause({
672
743
  name:
673
744
  typeof error.name === 'string' && error.name.length > 0
674
745
  ? error.name
675
746
  : 'Error',
676
747
  message: error.message,
677
748
  ...(typeof error.stack === 'string' ? { stack: error.stack } : {}),
678
- };
749
+ });
679
750
  }
680
- return { name: 'Error', message: JSON.stringify(error) };
751
+ return withCause({ name: 'Error', message: JSON.stringify(error) });
681
752
  } catch {
682
753
  try {
683
- return { name: 'Error', message: String(error) };
754
+ return withCause({ name: 'Error', message: String(error) });
684
755
  } catch {
685
- return { name: 'Error', message: 'Unknown error' };
756
+ return withCause({ name: 'Error', message: 'Unknown error' });
686
757
  }
687
758
  }
688
759
  }
@@ -951,17 +1022,76 @@ function projectionsEqual(
951
1022
  );
952
1023
  }
953
1024
 
954
- function projectionPreservesBaseline(
955
- baseline: PlaybookRepositoryObservation,
956
- after: PlaybookRepositoryObservation,
1025
+ function projectionEntriesEqual(
1026
+ left: PlaybookRepositoryObservation,
1027
+ right: PlaybookRepositoryObservation,
1028
+ path: string,
957
1029
  ): boolean {
958
- return Object.entries(baseline.projection).every(
959
- ([path, entry]) =>
960
- own(after.projection, path) &&
961
- JSON.stringify(entry) === JSON.stringify(after.projection[path]),
1030
+ return (
1031
+ own(right.projection, path) &&
1032
+ JSON.stringify(left.projection[path]) ===
1033
+ JSON.stringify(right.projection[path])
962
1034
  );
963
1035
  }
964
1036
 
1037
+ const EFFECT_PRE_EXISTING_LISTS = ['absorbed', 'altered', 'lost'] as const;
1038
+
1039
+ const EMPTY_PRE_EXISTING: Readonly<
1040
+ Record<(typeof EFFECT_PRE_EXISTING_LISTS)[number], readonly string[]>
1041
+ > = Object.freeze({
1042
+ absorbed: Object.freeze([]),
1043
+ altered: Object.freeze([]),
1044
+ lost: Object.freeze([]),
1045
+ });
1046
+
1047
+ /**
1048
+ * The pre-existing accounting a physical or logical receipt records
1049
+ * (DR-062 §3): sorted unique baseline paths, pairwise disjoint, present
1050
+ * exactly when one list is nonempty.
1051
+ */
1052
+ function effectPreExisting(
1053
+ value: unknown,
1054
+ path: string,
1055
+ baseline: PlaybookRepositoryObservation,
1056
+ ): Readonly<Record<(typeof EFFECT_PRE_EXISTING_LISTS)[number], readonly string[]>> {
1057
+ if (!isRecord(value)) throw new TypeError(`${path} must be an object`);
1058
+ rejectUnknownKeys(value, EFFECT_PRE_EXISTING_LISTS, path);
1059
+ const claimed = new Set<string>();
1060
+ const lists = EFFECT_PRE_EXISTING_LISTS.map((name) => {
1061
+ const entries = value[name];
1062
+ if (!Array.isArray(entries)) {
1063
+ throw new TypeError(`${path}.${name} must be an array of baseline paths`);
1064
+ }
1065
+ let previous: string | undefined;
1066
+ for (const entry of entries) {
1067
+ if (typeof entry !== 'string' || entry.length === 0) {
1068
+ throw new TypeError(`${path}.${name} must hold nonempty baseline paths`);
1069
+ }
1070
+ const member = entry;
1071
+ if (previous !== undefined && member <= previous) {
1072
+ throw new TypeError(`${path}.${name} must be sorted and unique`);
1073
+ }
1074
+ previous = member;
1075
+ if (!own(baseline.projection, member)) {
1076
+ throw new TypeError(
1077
+ `${path}.${name} names a path outside the baseline projection`,
1078
+ );
1079
+ }
1080
+ if (claimed.has(member)) {
1081
+ throw new TypeError(`${path} lists ${JSON.stringify(member)} twice`);
1082
+ }
1083
+ claimed.add(member);
1084
+ }
1085
+ return [name, entries as readonly string[]] as const;
1086
+ });
1087
+ if (claimed.size === 0) {
1088
+ throw new TypeError(`${path} must name at least one baseline path`);
1089
+ }
1090
+ return Object.fromEntries(lists) as Readonly<
1091
+ Record<(typeof EFFECT_PRE_EXISTING_LISTS)[number], readonly string[]>
1092
+ >;
1093
+ }
1094
+
965
1095
  function effectObservation(
966
1096
  value: unknown,
967
1097
  path: string,
@@ -1026,7 +1156,7 @@ function effectReceipt(
1026
1156
  if (!isRecord(value)) throw new TypeError(`${path} must be an object`);
1027
1157
  rejectUnknownKeys(
1028
1158
  value,
1029
- ['classification', 'baseline', 'after', 'commitOid'],
1159
+ ['classification', 'baseline', 'after', 'commitOid', 'preExisting'],
1030
1160
  path,
1031
1161
  );
1032
1162
  if (
@@ -1096,14 +1226,40 @@ function effectReceipt(
1096
1226
  ) {
1097
1227
  throw new TypeError(`${path} classified unchanged observations that differ`);
1098
1228
  }
1229
+ // DR-062 §3: a receipt records the fate of every baseline entry, so the
1230
+ // accounting and the two observations must agree exactly.
1231
+ if (
1232
+ own(value, 'preExisting') &&
1233
+ classification !== 'one-descendant-commit' &&
1234
+ classification !== 'worktree-only-change' &&
1235
+ classification !== 'observation-ambiguous'
1236
+ ) {
1237
+ throw new TypeError(
1238
+ `${path}.preExisting is not permitted for ${classification}`,
1239
+ );
1240
+ }
1241
+ const preExisting = own(value, 'preExisting')
1242
+ ? effectPreExisting(value.preExisting, `${path}.preExisting`, baseline)
1243
+ : EMPTY_PRE_EXISTING;
1244
+ const consumed = new Set([...preExisting.absorbed, ...preExisting.altered]);
1099
1245
  if (
1100
1246
  classification === 'one-descendant-commit' &&
1101
1247
  after !== undefined &&
1102
1248
  (baseline.head === after.head ||
1103
- !projectionsEqual(baseline, after))
1249
+ preExisting.lost.length > 0 ||
1250
+ !Object.keys(after.projection).every(
1251
+ (member) =>
1252
+ own(baseline.projection, member) &&
1253
+ projectionEntriesEqual(baseline, after, member),
1254
+ ) ||
1255
+ !Object.keys(baseline.projection).every(
1256
+ (member) =>
1257
+ own(after.projection, member) !== consumed.has(member),
1258
+ ) ||
1259
+ (consumed.size === 0) !== projectionsEqual(baseline, after))
1104
1260
  ) {
1105
1261
  throw new TypeError(
1106
- `${path} one-descendant-commit must change HEAD and preserve the projection`,
1262
+ `${path} one-descendant-commit must change HEAD and account for its complete baseline projection`,
1107
1263
  );
1108
1264
  }
1109
1265
  if (
@@ -1111,10 +1267,17 @@ function effectReceipt(
1111
1267
  after !== undefined &&
1112
1268
  (baseline.head !== after.head ||
1113
1269
  projectionsEqual(baseline, after) ||
1114
- !projectionPreservesBaseline(baseline, after))
1270
+ preExisting.absorbed.length > 0 ||
1271
+ preExisting.lost.length > 0 ||
1272
+ !Object.keys(baseline.projection).every(
1273
+ (member) =>
1274
+ own(after.projection, member) &&
1275
+ projectionEntriesEqual(baseline, after, member) !==
1276
+ consumed.has(member),
1277
+ ))
1115
1278
  ) {
1116
1279
  throw new TypeError(
1117
- `${path} worktree-only-change must preserve HEAD and change the projection`,
1280
+ `${path} worktree-only-change must preserve HEAD, change the projection, and account for every altered baseline entry`,
1118
1281
  );
1119
1282
  }
1120
1283
  if (
@@ -1215,6 +1378,8 @@ export type PlaybookSemanticReconciliation =
1215
1378
  | {
1216
1379
  readonly status: 'unresolved';
1217
1380
  readonly reason: PlaybookSemanticReconciliationReason;
1381
+ /** DR-063 §1: why it stays unresolved, as a closed structured cause. */
1382
+ readonly cause: PlaybookFailureCause;
1218
1383
  readonly evidence: PlaybookRetainedSemanticEvidence;
1219
1384
  };
1220
1385
 
@@ -1302,11 +1467,172 @@ function retainedSemanticEvidence(
1302
1467
  });
1303
1468
  }
1304
1469
 
1470
+ // ---------------------------------------------------------------------------
1471
+ // DR-063 §1: the evidence builder for a repository-decided failure. Every list
1472
+ // is sorted, unique, and bounded; `truncated` carries the count the bound
1473
+ // omitted. Nothing but repository paths, dispositions, classifications, and
1474
+ // revision identities enters a cause.
1475
+ // ---------------------------------------------------------------------------
1476
+
1477
+ const FAILURE_PATH_BOUND = 32;
1478
+
1479
+ function boundedFailurePaths(
1480
+ lists: Readonly<Record<string, readonly string[]>>,
1481
+ ): PlaybookFailurePaths {
1482
+ const bounded: Record<string, unknown> = {};
1483
+ let omitted = 0;
1484
+ for (const [member, paths] of Object.entries(lists)) {
1485
+ bounded[member] = paths.slice(0, FAILURE_PATH_BOUND);
1486
+ omitted += Math.max(0, paths.length - FAILURE_PATH_BOUND);
1487
+ }
1488
+ if (omitted > 0) bounded.truncated = omitted;
1489
+ return bounded as PlaybookFailurePaths;
1490
+ }
1491
+
1492
+ function sortedUniquePaths(paths: Iterable<string>): readonly string[] {
1493
+ return [...new Set(paths)].sort();
1494
+ }
1495
+
1496
+ /** After-projection entries the baseline did not hold, or holds differently. */
1497
+ function newOrChangedProjectionPaths(
1498
+ baseline: PlaybookRepositoryObservation,
1499
+ after: PlaybookRepositoryObservation,
1500
+ ): readonly string[] {
1501
+ return sortedUniquePaths(
1502
+ Object.keys(after.projection).filter(
1503
+ (path) =>
1504
+ !Object.hasOwn(baseline.projection, path) ||
1505
+ !isDeepStrictEqual(baseline.projection[path], after.projection[path]),
1506
+ ),
1507
+ );
1508
+ }
1509
+
1510
+ /** Entries that differ or are missing on either side of the two projections. */
1511
+ function divergentProjectionPaths(
1512
+ baseline: PlaybookRepositoryObservation,
1513
+ after: PlaybookRepositoryObservation,
1514
+ ): readonly string[] {
1515
+ return sortedUniquePaths(
1516
+ [
1517
+ ...Object.keys(baseline.projection),
1518
+ ...Object.keys(after.projection),
1519
+ ].filter(
1520
+ (path) =>
1521
+ !Object.hasOwn(baseline.projection, path) ||
1522
+ !Object.hasOwn(after.projection, path) ||
1523
+ !isDeepStrictEqual(baseline.projection[path], after.projection[path]),
1524
+ ),
1525
+ );
1526
+ }
1527
+
1528
+ /**
1529
+ * DR-063 §1: the cause of an unresolved repository-disposition mismatch, read
1530
+ * from the disposition the accepted arm required and the receipt the host
1531
+ * proved. The receipt states the failure; nothing here infers one.
1532
+ */
1533
+ function playbookRepositoryFailureCause(
1534
+ required: PlaybookRepositoryDisposition,
1535
+ receipt: PlaybookRepositoryReceipt,
1536
+ ): PlaybookFailureCause {
1537
+ const observed = receipt.classification;
1538
+ const baselineHead = receipt.baseline.head;
1539
+ const after = receipt.after;
1540
+ const common = { required, observed, baselineHead };
1541
+ const ambiguous = (): PlaybookFailureCause =>
1542
+ assertPlaybookFailureCause({
1543
+ code: 'attribution-ambiguous',
1544
+ evidence: {
1545
+ ...common,
1546
+ ...(after === undefined ? {} : { afterHead: after.head }),
1547
+ },
1548
+ });
1549
+ const lost = receipt.preExisting?.lost ?? [];
1550
+ if (lost.length > 0) {
1551
+ return assertPlaybookFailureCause({
1552
+ code: 'pre-existing-lost',
1553
+ evidence: {
1554
+ ...common,
1555
+ ...(after === undefined ? {} : { afterHead: after.head }),
1556
+ paths: boundedFailurePaths({ lost: sortedUniquePaths(lost) }),
1557
+ },
1558
+ });
1559
+ }
1560
+ if (observed === 'observation-ambiguous' && after === undefined) {
1561
+ return assertPlaybookFailureCause({
1562
+ code: 'observation-unstable',
1563
+ evidence: common,
1564
+ });
1565
+ }
1566
+ if (after === undefined) return ambiguous();
1567
+ const afterHead = after.head;
1568
+ if (observed === 'multiple-commits') {
1569
+ return assertPlaybookFailureCause({
1570
+ code: 'commits-more-than-one',
1571
+ evidence: { ...common, afterHead },
1572
+ });
1573
+ }
1574
+ if (observed === 'rewritten-or-non-descendant') {
1575
+ return assertPlaybookFailureCause({
1576
+ code: 'history-rewritten',
1577
+ evidence: { ...common, afterHead },
1578
+ });
1579
+ }
1580
+ if (observed === 'concurrent-or-foreign-change') {
1581
+ return assertPlaybookFailureCause({
1582
+ code: 'foreign-change',
1583
+ evidence: {
1584
+ ...common,
1585
+ afterHead,
1586
+ paths: boundedFailurePaths({
1587
+ changed: divergentProjectionPaths(receipt.baseline, after),
1588
+ }),
1589
+ },
1590
+ });
1591
+ }
1592
+ const uncommitted = newOrChangedProjectionPaths(receipt.baseline, after);
1593
+ const altered = sortedUniquePaths(receipt.preExisting?.altered ?? []);
1594
+ if (
1595
+ observed === 'observation-ambiguous' &&
1596
+ afterHead !== baselineHead &&
1597
+ (uncommitted.length > 0 || altered.length > 0)
1598
+ ) {
1599
+ return assertPlaybookFailureCause({
1600
+ code: 'commit-residual',
1601
+ evidence: {
1602
+ ...common,
1603
+ afterHead,
1604
+ ...(receipt.commitOid === undefined
1605
+ ? {}
1606
+ : { commitOid: receipt.commitOid }),
1607
+ paths: boundedFailurePaths({ uncommitted, altered }),
1608
+ },
1609
+ });
1610
+ }
1611
+ if (
1612
+ required === 'one-descendant-commit' &&
1613
+ (observed === 'unchanged' || observed === 'worktree-only-change')
1614
+ ) {
1615
+ return assertPlaybookFailureCause({
1616
+ code: 'commit-missing',
1617
+ evidence: {
1618
+ ...common,
1619
+ afterHead,
1620
+ paths: boundedFailurePaths({ uncommitted }),
1621
+ },
1622
+ });
1623
+ }
1624
+ return ambiguous();
1625
+ }
1626
+
1305
1627
  function unresolvedSemanticEvidence(
1306
1628
  reason: PlaybookSemanticReconciliationReason,
1307
1629
  evidence: PlaybookRetainedSemanticEvidence,
1630
+ cause: PlaybookFailureCause = assertPlaybookFailureCause({
1631
+ code: 'runtime-defect',
1632
+ evidence: { reason },
1633
+ }),
1308
1634
  ): PlaybookSemanticReconciliation {
1309
- return Object.freeze({ status: 'unresolved', reason, evidence });
1635
+ return Object.freeze({ status: 'unresolved', reason, cause, evidence });
1310
1636
  }
1311
1637
 
1312
1638
  /**
@@ -1372,6 +1698,7 @@ export function reconcilePlaybookSemanticEvidence(
1372
1698
  return unresolvedSemanticEvidence(
1373
1699
  'repository-disposition-mismatch',
1374
1700
  evidence,
1701
+ playbookRepositoryFailureCause(disposition, receipt),
1375
1702
  );
1376
1703
  }
1377
1704
 
@@ -2609,6 +2936,17 @@ export class NestedPlaybookCallError extends Error {
2609
2936
  this.name = normalized?.name ?? 'NestedPlaybookCallError';
2610
2937
  if (normalized?.stack) this.stack = normalized.stack;
2611
2938
  this.result = result;
2939
+ // DR-063 §2: the child's failure reaching its parent is `child-failed`,
2940
+ // carrying the child's own cause when its normalized error published one.
2941
+ attachPlaybookFailureCause(this, {
2942
+ code: 'child-failed',
2943
+ evidence: {
2944
+ playbookId: result.playbookId,
2945
+ ...(normalized?.cause === undefined
2946
+ ? {}
2947
+ : { cause: normalized.cause }),
2948
+ },
2949
+ });
2612
2950
  }
2613
2951
  }
2614
2952
 
@@ -2791,7 +3129,7 @@ function validateNormalizedError(error: unknown, path: string): void {
2791
3129
  if (!isRecord(error)) {
2792
3130
  throw new TypeError(`${path} must be a normalized error`);
2793
3131
  }
2794
- rejectUnknownKeys(error, ['name', 'message', 'stack'], path);
3132
+ rejectUnknownKeys(error, ['name', 'message', 'stack', 'cause'], path);
2795
3133
  requireNonEmptyString(error.name, `${path}.name`);
2796
3134
  if (typeof error.message !== 'string') {
2797
3135
  throw new TypeError(`${path}.message must be a string`);
@@ -2799,6 +3137,9 @@ function validateNormalizedError(error: unknown, path: string): void {
2799
3137
  if (error.stack !== undefined && typeof error.stack !== 'string') {
2800
3138
  throw new TypeError(`${path}.stack must be a string`);
2801
3139
  }
3140
+ // DR-063 §2: a normalized error may carry its structured cause, and an
3141
+ // unrecognized shape is not a cause.
3142
+ if (error.cause !== undefined) assertPlaybookFailureCause(error.cause);
2802
3143
  }
2803
3144
 
2804
3145
  // DR-048: the completed child's compiled terminal record. It is runtime-owned