@sublang/playbook 13.0.0 → 13.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 (37) hide show
  1. package/package.json +2 -2
  2. package/reference/sdlc/code.md +7 -0
  3. package/reference/sdlc/code.playbook/code.fsm.js +5 -5
  4. package/reference/sdlc/code.playbook/code.fsm.ts +5 -5
  5. package/reference/sdlc/code.playbook/code.gears.md +5 -5
  6. package/reference/sdlc/code.playbook/code.playbook.d.ts +1 -1
  7. package/reference/sdlc/code.playbook/code.playbook.js +6 -16
  8. package/reference/sdlc/code.playbook/code.playbook.ts +7 -17
  9. package/reference/sdlc/code.playbook/playbook-captain.js +4 -1
  10. package/reference/sdlc/code.playbook/playbook-captain.ts +4 -1
  11. package/reference/sdlc/decide.md +2 -0
  12. package/reference/sdlc/decide.playbook/decide.fsm.js +1 -1
  13. package/reference/sdlc/decide.playbook/decide.fsm.ts +1 -1
  14. package/reference/sdlc/decide.playbook/decide.gears.md +2 -2
  15. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +1 -2
  16. package/reference/sdlc/decide.playbook/decide.playbook.js +8 -22
  17. package/reference/sdlc/decide.playbook/decide.playbook.ts +8 -25
  18. package/reference/sdlc/dev.md +21 -1
  19. package/reference/sdlc/dev.playbook/dev.fsm.js +10 -10
  20. package/reference/sdlc/dev.playbook/dev.fsm.ts +13 -13
  21. package/reference/sdlc/dev.playbook/dev.gears.md +15 -15
  22. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +1 -1
  23. package/reference/sdlc/dev.playbook/dev.playbook.js +7 -17
  24. package/reference/sdlc/dev.playbook/dev.playbook.ts +8 -20
  25. package/reference/sdlc/review.md +18 -0
  26. package/reference/sdlc/review.playbook/review.fsm.js +8 -8
  27. package/reference/sdlc/review.playbook/review.fsm.ts +8 -8
  28. package/reference/sdlc/review.playbook/review.gears.md +8 -8
  29. package/reference/sdlc/review.playbook/review.playbook.d.ts +1 -1
  30. package/reference/sdlc/review.playbook/review.playbook.js +5 -15
  31. package/reference/sdlc/review.playbook/review.playbook.ts +6 -16
  32. package/slc/link.md +12 -24
  33. package/src/runtime.d.ts +2 -0
  34. package/src/runtime.ts +2 -0
  35. package/src/xstate-playbook-runtime.d.ts +4 -2
  36. package/src/xstate-playbook-runtime.js +18 -13
  37. package/src/xstate-playbook-runtime.ts +26 -14
@@ -329,7 +329,7 @@ interface XStatePlaybookRuntimeSpecBase<TOptions> {
329
329
  /** Complete FSM-derived Boss-facing metadata for every `player` state. */
330
330
  roleStates?: Readonly<Record<string, XStateRoleStateStatus>>;
331
331
  /** Compose the player prompt. Default: continuation blocks + `<field>` placeholder substitution. */
332
- composePlayerPrompt?: (input: PlaybookPlayerInput, promptIdentity: XStatePromptIdentity) => string;
332
+ composePlayerPrompt?: (input: PlaybookPlayerInput, promptIdentity: XStatePromptIdentity, resuming?: boolean) => string;
333
333
  /** Compose the direct-Captain prompt. Default: continuation blocks + placeholder substitution with deterministic JSON rendering. */
334
334
  composeCaptainPrompt?: (input: PlaybookCaptainInput) => string;
335
335
  /** Linker-known exceptions to the default kebab-token → camel-field mapping. */
@@ -388,6 +388,8 @@ export declare function normalizeErrorFull(err: unknown): {
388
388
  } | undefined;
389
389
  /** Read the FSM context's single pending Boss question, when well-formed. */
390
390
  export declare function pendingBossQuestionFromContext(context: Record<string, unknown>): PlaybookPendingBossQuestionContext | undefined;
391
+ /** Add clarification context without repeating the question in a live conversation. */
392
+ export declare function composePlayerContinuation(input: Pick<PlaybookPlayerInput, 'pendingBossQuestion' | 'bossReply'>, body: string, resuming?: boolean): string;
391
393
  /**
392
394
  * Default player-prompt composer (slc/link.md §Player prompt composition).
393
395
  * One callback-based pass substitutes each `<fieldName>` placeholder whose
@@ -395,7 +397,7 @@ export declare function pendingBossQuestionFromContext(context: Record<string, u
395
397
  * placeholder-looking text inside a value is never re-substituted. The
396
398
  * continuation preamble and Q/A blocks precede the domain body on resume.
397
399
  */
398
- export declare function defaultComposePlayerPrompt(input: PlaybookPlayerInput, placeholderFields?: Readonly<Record<string, string>>): string;
400
+ export declare function defaultComposePlayerPrompt(input: PlaybookPlayerInput, placeholderFields?: Readonly<Record<string, string>>, resuming?: boolean): string;
399
401
  /**
400
402
  * Default direct-Captain prompt composer (slc/link.md §Captain prompt
401
403
  * composition). Placeholder substitution is presence-based: string fields
@@ -402,17 +402,21 @@ export function pendingBossQuestionFromContext(context) {
402
402
  // ---------------------------------------------------------------------------
403
403
  // Generic strategy defaults.
404
404
  // ---------------------------------------------------------------------------
405
- const CONTINUATION_PREAMBLE = 'You previously paused this task to ask Boss a question; Boss has now replied. Continue the same task using the reply below.';
406
- function continuationBlocks(input) {
405
+ const CONTINUATION_PREAMBLE = 'Continue the same task using Boss’s reply below.';
406
+ function continuationBlocks(input, resuming = false) {
407
407
  if (input.pendingBossQuestion === undefined || input.bossReply === undefined) {
408
408
  return [];
409
409
  }
410
410
  return [
411
411
  CONTINUATION_PREAMBLE,
412
- `Boss question:\n${input.pendingBossQuestion.question}`,
412
+ ...(resuming ? [] : [`Your previous question:\n${input.pendingBossQuestion.question}`]),
413
413
  `Boss reply:\n${input.bossReply}`,
414
414
  ];
415
415
  }
416
+ /** Add clarification context without repeating the question in a live conversation. */
417
+ export function composePlayerContinuation(input, body, resuming = false) {
418
+ return [...continuationBlocks(input, resuming), body].join('\n\n');
419
+ }
416
420
  const PLACEHOLDER_PATTERN = /<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>/g;
417
421
  function placeholderFieldName(token, fields) {
418
422
  const explicit = fields[token];
@@ -429,15 +433,13 @@ function placeholderFieldName(token, fields) {
429
433
  * placeholder-looking text inside a value is never re-substituted. The
430
434
  * continuation preamble and Q/A blocks precede the domain body on resume.
431
435
  */
432
- export function defaultComposePlayerPrompt(input, placeholderFields = {}) {
433
- const blocks = continuationBlocks(input);
436
+ export function defaultComposePlayerPrompt(input, placeholderFields = {}, resuming = false) {
434
437
  const fields = input;
435
438
  const body = input.prompt.replace(PLACEHOLDER_PATTERN, (match, token) => {
436
439
  const value = fields[placeholderFieldName(token, placeholderFields)];
437
440
  return typeof value === 'string' ? value : match;
438
441
  });
439
- blocks.push(body);
440
- return blocks.join('\n\n');
442
+ return composePlayerContinuation(input, body, resuming);
441
443
  }
442
444
  function sortJson(value) {
443
445
  if (Array.isArray(value))
@@ -1901,7 +1903,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
1901
1903
  }
1902
1904
  }
1903
1905
  const composePlayerPrompt = spec.composePlayerPrompt ??
1904
- ((input) => defaultComposePlayerPrompt(input, spec.placeholderFields));
1906
+ ((input, _identity, resuming = false) => defaultComposePlayerPrompt(input, spec.placeholderFields, resuming));
1905
1907
  const composeCaptainPrompt = spec.composeCaptainPrompt ??
1906
1908
  ((input) => defaultComposeCaptainPrompt(input, spec.placeholderFields));
1907
1909
  const extractFields = spec.extractRequiredFields ?? defaultExtractRequiredFields;
@@ -2522,7 +2524,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
2522
2524
  }
2523
2525
  return session?.roleBindings?.[roleId]?.promptIdentity ?? roleId;
2524
2526
  }
2525
- function composeBoundPlayerPrompt(input) {
2527
+ function composeBoundPlayerPrompt(input, resuming = false) {
2526
2528
  let active = true;
2527
2529
  const lookup = (roleId) => {
2528
2530
  if (!active) {
@@ -2531,7 +2533,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
2531
2533
  return promptIdentity(roleId);
2532
2534
  };
2533
2535
  try {
2534
- return composePlayerPrompt(input, lookup);
2536
+ return composePlayerPrompt(input, lookup, resuming);
2535
2537
  }
2536
2538
  finally {
2537
2539
  active = false;
@@ -3372,7 +3374,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
3372
3374
  failedGovernedAttemptId = attemptIds.values().next().value;
3373
3375
  }
3374
3376
  const boundary = {
3375
- async callPlayer(input, roleId, prompt, signal) {
3377
+ async callPlayer(input, roleId, freshPrompt, signal) {
3376
3378
  // State-entry telemetry/status must precede the call they describe.
3377
3379
  await drainEmissions();
3378
3380
  signal.throwIfAborted();
@@ -3399,6 +3401,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
3399
3401
  controlPlaneError ??= error;
3400
3402
  throw error;
3401
3403
  }
3404
+ const prompt = selectedResume === false
3405
+ ? freshPrompt : composeBoundPlayerPrompt(input, true);
3402
3406
  const callId = deferredContinuation?.effectBoundary.callId ??
3403
3407
  `player-${++playerCallSequence}`;
3404
3408
  const callIdentity = (resume) => ({
@@ -3427,7 +3431,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
3427
3431
  try {
3428
3432
  const runTracedPlayerCall = async (resume = selectedResume) => {
3429
3433
  const identity = callIdentity(resume);
3430
- await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt }, position, signal);
3434
+ const callPrompt = resume === false ? freshPrompt : prompt;
3435
+ await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt: callPrompt }, position, signal);
3431
3436
  let rawResult;
3432
3437
  try {
3433
3438
  // An abort may land while the awaited started emission drains
@@ -3435,7 +3440,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
3435
3440
  // never start after abort, so settle the already-started pair
3436
3441
  // as `aborted` through the catch below.
3437
3442
  signal.throwIfAborted();
3438
- rawResult = await requireHostPorts().callPlayer(roleId, prompt, signal, { resume });
3443
+ rawResult = await requireHostPorts().callPlayer(roleId, callPrompt, signal, { resume, ...(callPrompt === freshPrompt ? {} : { freshPrompt }) });
3439
3444
  // A host promise is not required to honor cancellation. Do not
3440
3445
  // let a late result mutate continuity or publish a successful
3441
3446
  // finish.
@@ -776,6 +776,7 @@ interface XStatePlaybookRuntimeSpecBase<TOptions> {
776
776
  composePlayerPrompt?: (
777
777
  input: PlaybookPlayerInput,
778
778
  promptIdentity: XStatePromptIdentity,
779
+ resuming?: boolean,
779
780
  ) => string;
780
781
  /** Compose the direct-Captain prompt. Default: continuation blocks + placeholder substitution with deterministic JSON rendering. */
781
782
  composeCaptainPrompt?: (input: PlaybookCaptainInput) => string;
@@ -1064,22 +1065,31 @@ export function pendingBossQuestionFromContext(
1064
1065
  // ---------------------------------------------------------------------------
1065
1066
 
1066
1067
  const CONTINUATION_PREAMBLE =
1067
- 'You previously paused this task to ask Boss a question; Boss has now replied. Continue the same task using the reply below.';
1068
+ 'Continue the same task using Boss’s reply below.';
1068
1069
 
1069
1070
  function continuationBlocks(input: {
1070
1071
  pendingBossQuestion?: { readonly question: string };
1071
1072
  bossReply?: string;
1072
- }): string[] {
1073
+ }, resuming = false): string[] {
1073
1074
  if (input.pendingBossQuestion === undefined || input.bossReply === undefined) {
1074
1075
  return [];
1075
1076
  }
1076
1077
  return [
1077
1078
  CONTINUATION_PREAMBLE,
1078
- `Boss question:\n${input.pendingBossQuestion.question}`,
1079
+ ...(resuming ? [] : [`Your previous question:\n${input.pendingBossQuestion.question}`]),
1079
1080
  `Boss reply:\n${input.bossReply}`,
1080
1081
  ];
1081
1082
  }
1082
1083
 
1084
+ /** Add clarification context without repeating the question in a live conversation. */
1085
+ export function composePlayerContinuation(
1086
+ input: Pick<PlaybookPlayerInput, 'pendingBossQuestion' | 'bossReply'>,
1087
+ body: string,
1088
+ resuming = false,
1089
+ ): string {
1090
+ return [...continuationBlocks(input, resuming), body].join('\n\n');
1091
+ }
1092
+
1083
1093
  const PLACEHOLDER_PATTERN = /<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>/g;
1084
1094
 
1085
1095
  function placeholderFieldName(
@@ -1104,16 +1114,15 @@ function placeholderFieldName(
1104
1114
  export function defaultComposePlayerPrompt(
1105
1115
  input: PlaybookPlayerInput,
1106
1116
  placeholderFields: Readonly<Record<string, string>> = {},
1117
+ resuming = false,
1107
1118
  ): string {
1108
- const blocks = continuationBlocks(input);
1109
1119
  const fields = input as unknown as Record<string, unknown>;
1110
1120
  const body = input.prompt.replace(PLACEHOLDER_PATTERN, (match, token) => {
1111
1121
  const value =
1112
1122
  fields[placeholderFieldName(token as string, placeholderFields)];
1113
1123
  return typeof value === 'string' ? value : match;
1114
1124
  });
1115
- blocks.push(body);
1116
- return blocks.join('\n\n');
1125
+ return composePlayerContinuation(input, body, resuming);
1117
1126
  }
1118
1127
 
1119
1128
  function sortJson(value: JsonValue): JsonValue {
@@ -3158,8 +3167,8 @@ export function createXStatePlaybookRuntime<
3158
3167
  }
3159
3168
  const composePlayerPrompt =
3160
3169
  spec.composePlayerPrompt ??
3161
- ((input: PlaybookPlayerInput) =>
3162
- defaultComposePlayerPrompt(input, spec.placeholderFields));
3170
+ ((input: PlaybookPlayerInput, _identity: XStatePromptIdentity, resuming = false) =>
3171
+ defaultComposePlayerPrompt(input, spec.placeholderFields, resuming));
3163
3172
  const composeCaptainPrompt =
3164
3173
  spec.composeCaptainPrompt ??
3165
3174
  ((input: PlaybookCaptainInput) =>
@@ -4074,7 +4083,7 @@ export function createXStatePlaybookRuntime<
4074
4083
  return session?.roleBindings?.[roleId]?.promptIdentity ?? roleId;
4075
4084
  }
4076
4085
 
4077
- function composeBoundPlayerPrompt(input: PlaybookPlayerInput): string {
4086
+ function composeBoundPlayerPrompt(input: PlaybookPlayerInput, resuming = false): string {
4078
4087
  let active = true;
4079
4088
  const lookup: XStatePromptIdentity = (roleId) => {
4080
4089
  if (!active) {
@@ -4085,7 +4094,7 @@ export function createXStatePlaybookRuntime<
4085
4094
  return promptIdentity(roleId);
4086
4095
  };
4087
4096
  try {
4088
- return composePlayerPrompt(input, lookup);
4097
+ return composePlayerPrompt(input, lookup, resuming);
4089
4098
  } finally {
4090
4099
  active = false;
4091
4100
  }
@@ -5321,7 +5330,7 @@ export function createXStatePlaybookRuntime<
5321
5330
  async callPlayer(
5322
5331
  input,
5323
5332
  roleId,
5324
- prompt,
5333
+ freshPrompt,
5325
5334
  signal,
5326
5335
  ): Promise<PlayerResult> {
5327
5336
  // State-entry telemetry/status must precede the call they describe.
@@ -5356,6 +5365,8 @@ export function createXStatePlaybookRuntime<
5356
5365
  if (!isAbortFailure(error, signal)) controlPlaneError ??= error;
5357
5366
  throw error;
5358
5367
  }
5368
+ const prompt = selectedResume === false
5369
+ ? freshPrompt : composeBoundPlayerPrompt(input, true);
5359
5370
  const callId =
5360
5371
  deferredContinuation?.effectBoundary.callId ??
5361
5372
  `player-${++playerCallSequence}`;
@@ -5401,10 +5412,11 @@ export function createXStatePlaybookRuntime<
5401
5412
  resume: string | false = selectedResume,
5402
5413
  ): Promise<PlayerResult> => {
5403
5414
  const identity = callIdentity(resume);
5415
+ const callPrompt = resume === false ? freshPrompt : prompt;
5404
5416
  await emitCallStarted(
5405
5417
  'player.call.started',
5406
5418
  'player.call.finished',
5407
- { ...identity, prompt },
5419
+ { ...identity, prompt: callPrompt },
5408
5420
  position,
5409
5421
  signal,
5410
5422
  );
@@ -5418,9 +5430,9 @@ export function createXStatePlaybookRuntime<
5418
5430
  signal.throwIfAborted();
5419
5431
  rawResult = await requireHostPorts().callPlayer(
5420
5432
  roleId,
5421
- prompt,
5433
+ callPrompt,
5422
5434
  signal,
5423
- { resume },
5435
+ { resume, ...(callPrompt === freshPrompt ? {} : { freshPrompt }) },
5424
5436
  );
5425
5437
  // A host promise is not required to honor cancellation. Do not
5426
5438
  // let a late result mutate continuity or publish a successful