@sublang/playbook 15.1.0 → 17.0.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 (102) hide show
  1. package/README.md +2 -0
  2. package/docs/cli.md +36 -18
  3. package/docs/configuration.md +7 -5
  4. package/docs/embedding.md +28 -7
  5. package/package.json +4 -4
  6. package/reference/sdlc/branch.playbook/branch.fsm.d.ts +63 -33
  7. package/reference/sdlc/branch.playbook/branch.fsm.js +190 -108
  8. package/reference/sdlc/branch.playbook/branch.fsm.ts +269 -147
  9. package/reference/sdlc/branch.playbook/branch.gears.md +7 -17
  10. package/reference/sdlc/branch.playbook/branch.playbook.d.ts +12 -16
  11. package/reference/sdlc/branch.playbook/branch.playbook.js +112 -101
  12. package/reference/sdlc/branch.playbook/branch.playbook.ts +128 -171
  13. package/reference/sdlc/branch.playbook/branch.registry.d.ts +3 -2
  14. package/reference/sdlc/branch.playbook/branch.registry.ts +6 -2
  15. package/reference/sdlc/captain.md +19 -10
  16. package/reference/sdlc/captain.playbook/captain.fsm.d.ts +6 -2
  17. package/reference/sdlc/captain.playbook/captain.fsm.js +19 -7
  18. package/reference/sdlc/captain.playbook/captain.fsm.ts +25 -10
  19. package/reference/sdlc/captain.playbook/captain.gears.md +16 -8
  20. package/reference/sdlc/captain.playbook/captain.playbook.d.ts +1 -1
  21. package/reference/sdlc/captain.playbook/captain.playbook.js +3 -2
  22. package/reference/sdlc/captain.playbook/captain.playbook.ts +4 -3
  23. package/reference/sdlc/code.playbook/bin/interactive-session.js +14 -1
  24. package/reference/sdlc/code.playbook/bin/portable-codec.js +3 -0
  25. package/reference/sdlc/code.playbook/bin/repository-effects.js +1 -0
  26. package/reference/sdlc/code.playbook/bin/run.js +128 -85
  27. package/reference/sdlc/code.playbook/bin/session-host.js +40 -8
  28. package/reference/sdlc/code.playbook/bin/session-store.js +162 -34
  29. package/reference/sdlc/code.playbook/code.fsm.d.ts +146 -93
  30. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +8 -8
  31. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +13 -13
  32. package/reference/sdlc/code.playbook/code.fsm.js +609 -649
  33. package/reference/sdlc/code.playbook/code.fsm.ts +890 -821
  34. package/reference/sdlc/code.playbook/code.gears.md +39 -29
  35. package/reference/sdlc/code.playbook/code.playbook.d.ts +13 -16
  36. package/reference/sdlc/code.playbook/code.playbook.js +166 -147
  37. package/reference/sdlc/code.playbook/code.playbook.ts +180 -229
  38. package/reference/sdlc/code.playbook/code.registry.d.ts +3 -2
  39. package/reference/sdlc/code.playbook/code.registry.ts +6 -2
  40. package/reference/sdlc/code.playbook/host-capabilities.d.ts +3 -0
  41. package/reference/sdlc/code.playbook/playbook-captain.d.ts +74 -2
  42. package/reference/sdlc/code.playbook/playbook-captain.js +1548 -468
  43. package/reference/sdlc/code.playbook/playbook-captain.ts +1993 -573
  44. package/reference/sdlc/code.playbook/session-host.d.ts +4 -1
  45. package/reference/sdlc/code.playbook/session-store.d.ts +17 -0
  46. package/reference/sdlc/code.playbook/session-store.js +1 -0
  47. package/reference/sdlc/decide.md +1 -0
  48. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +273 -157
  49. package/reference/sdlc/decide.playbook/decide.fsm.js +765 -732
  50. package/reference/sdlc/decide.playbook/decide.fsm.ts +1167 -945
  51. package/reference/sdlc/decide.playbook/decide.gears.md +28 -21
  52. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -59
  53. package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3696
  54. package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5244
  55. package/reference/sdlc/decide.playbook/decide.registry.d.ts +8 -4
  56. package/reference/sdlc/decide.playbook/decide.registry.js +2 -2
  57. package/reference/sdlc/decide.playbook/decide.registry.ts +13 -6
  58. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +116 -134
  59. package/reference/sdlc/dev.playbook/dev.fsm.js +517 -575
  60. package/reference/sdlc/dev.playbook/dev.fsm.ts +667 -787
  61. package/reference/sdlc/dev.playbook/dev.gears.md +52 -54
  62. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +13 -10
  63. package/reference/sdlc/dev.playbook/dev.playbook.js +128 -120
  64. package/reference/sdlc/dev.playbook/dev.playbook.ts +143 -193
  65. package/reference/sdlc/dev.playbook/dev.registry.d.ts +3 -2
  66. package/reference/sdlc/dev.playbook/dev.registry.ts +6 -2
  67. package/reference/sdlc/pr.playbook/pr.fsm.d.ts +95 -88
  68. package/reference/sdlc/pr.playbook/pr.fsm.js +461 -518
  69. package/reference/sdlc/pr.playbook/pr.fsm.ts +533 -663
  70. package/reference/sdlc/pr.playbook/pr.gears.md +47 -63
  71. package/reference/sdlc/pr.playbook/pr.playbook.d.ts +13 -23
  72. package/reference/sdlc/pr.playbook/pr.playbook.js +118 -112
  73. package/reference/sdlc/pr.playbook/pr.playbook.ts +136 -190
  74. package/reference/sdlc/pr.playbook/pr.registry.d.ts +3 -2
  75. package/reference/sdlc/pr.playbook/pr.registry.ts +4 -2
  76. package/reference/sdlc/review.md +3 -3
  77. package/reference/sdlc/review.playbook/review.fsm.d.ts +150 -116
  78. package/reference/sdlc/review.playbook/review.fsm.js +479 -550
  79. package/reference/sdlc/review.playbook/review.fsm.ts +717 -662
  80. package/reference/sdlc/review.playbook/review.gears.md +32 -29
  81. package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -12
  82. package/reference/sdlc/review.playbook/review.playbook.js +180 -155
  83. package/reference/sdlc/review.playbook/review.playbook.ts +190 -230
  84. package/reference/sdlc/review.playbook/review.registry.d.ts +6 -5
  85. package/reference/sdlc/review.playbook/review.registry.js +3 -3
  86. package/reference/sdlc/review.playbook/review.registry.ts +9 -5
  87. package/slc/gears2fsm.md +77 -8
  88. package/slc/link.md +127 -60
  89. package/slc/materialize-link.mjs +51 -13
  90. package/slc/optimize.md +4 -1
  91. package/slc/prefix-prompts.mjs +29 -28
  92. package/slc/prefix.md +27 -24
  93. package/slc/slc.pin-inputs.json +11 -2
  94. package/slc/text2gears.md +71 -11
  95. package/src/runtime.d.ts +46 -2
  96. package/src/runtime.ts +42 -2
  97. package/src/xstate-playbook-runtime.d.ts +63 -23
  98. package/src/xstate-playbook-runtime.js +1637 -475
  99. package/src/xstate-playbook-runtime.ts +2105 -532
  100. package/src/xstate-runtime.d.ts +3 -4
  101. package/src/xstate-runtime.js +52 -0
  102. package/src/xstate-runtime.ts +59 -4
@@ -7,7 +7,7 @@
7
7
  import { createRequire } from 'node:module';
8
8
  import { lstat, open, realpath, rename, unlink } from 'node:fs/promises';
9
9
  import { randomUUID } from 'node:crypto';
10
- import { dirname, extname, relative, resolve } from 'node:path';
10
+ import { basename, dirname, extname, relative, resolve } from 'node:path';
11
11
  import { pathToFileURL } from 'node:url';
12
12
 
13
13
  // Compile-time source only; emitted modules depend only on the shared engine.
@@ -78,6 +78,10 @@ const DESCRIPTOR_KEYS = [
78
78
  'transitionEventFields', 'verbatimPayloadFields', 'resumableStateIds',
79
79
  'unfinishedFinalStateIds', 'controlContextFields',
80
80
  ];
81
+ // Optional: the runtime specifier of the FSM's emitted JavaScript sibling, for
82
+ // a module inside a package that compiles and ships `.js` beside its sources.
83
+ const OPTIONAL_KEYS = ['fsmSpecifier'];
84
+ const JS_SPECIFIER = /^\.\.?\/.+\.js$/;
81
85
  const LABELLED_KEYS = ['playerInputExport', 'omitEmptyRelayLines', 'identityPlaceholders'];
82
86
  const TOKEN = /^(#|[A-Za-z_$][A-Za-z0-9_$-]*)$/;
83
87
  const CONTRACT_TYPES = [
@@ -126,7 +130,10 @@ function validateDescriptor(value, engine) {
126
130
  const descriptor = engine.snapshotJsonValue(value, 'link descriptor');
127
131
  const labelled = descriptor?.profile === 'flat-labelled-relays';
128
132
  const keys = labelled ? [...DESCRIPTOR_KEYS, ...LABELLED_KEYS] : DESCRIPTOR_KEYS;
129
- record(descriptor, 'descriptor', keys, keys);
133
+ record(descriptor, 'descriptor', [...keys, ...OPTIONAL_KEYS], keys);
134
+ if (descriptor.fsmSpecifier !== undefined && (typeof descriptor.fsmSpecifier !== 'string' || !JS_SPECIFIER.test(descriptor.fsmSpecifier))) {
135
+ throw new TypeError('fsmSpecifier must be a relative runtime specifier ending in .js');
136
+ }
130
137
  if (descriptor.schema !== SCHEMA) throw new TypeError(`descriptor.schema must equal ${SCHEMA}`);
131
138
  if (!['flat-defaults', 'flat-quoted-relays', 'flat-labelled-relays'].includes(descriptor.profile)) throw new UnsupportedLinkProfile('profile requires ordinary normative linking');
132
139
  if (labelled) {
@@ -228,27 +235,46 @@ function validateDescriptor(value, engine) {
228
235
  function inspectMachine(machine, engine, labelled = false) {
229
236
  const config = machine?.config;
230
237
  if (!config || typeof config !== 'object' || !config.states) throw new TypeError('FSM export must be an XState machine');
231
- if (config.type === 'parallel' || config.invoke) throw new UnsupportedLinkProfile('root parallel states or invocations require ordinary linking');
238
+ if (config.type === 'parallel' || config.invoke) throw new UnsupportedLinkProfile('a parallel machine root or root invocation requires ordinary linking');
232
239
  const descriptions = engine.stateDescriptionsFromMachine(machine);
233
240
  const roleStates = Object.create(null);
234
241
  let hasScript = false;
235
- for (const [key, state] of Object.entries(config.states)) {
242
+ const children = (value) => value && typeof value === 'object' ? Object.entries(value) : [];
243
+ const inspectState = (key, state) => {
236
244
  if (RESERVED_KEYS.has(key)) throw new UnsupportedLinkProfile(`state ${key} requires ordinary linking`);
237
245
  if (!state || typeof state !== 'object') throw new TypeError(`state ${key} must be an object`);
238
- if (state.states || (state.type !== undefined && !['atomic', 'final'].includes(state.type))) {
239
- throw new UnsupportedLinkProfile(`state ${key} is outside the flat single-region profile`);
240
- }
241
246
  const invokes = Array.isArray(state.invoke) ? state.invoke : state.invoke ? [state.invoke] : [];
242
247
  if (invokes.length > 1) throw new UnsupportedLinkProfile(`state ${key} has multiple actors`);
243
248
  for (const invoke of invokes) {
244
249
  if (!(labelled ? ['player', 'script', 'playbook'] : ['player', 'script']).includes(invoke.src)) throw new UnsupportedLinkProfile(`state ${key} actor ${String(invoke.src)} requires ordinary linking`);
245
250
  if (invoke.src === 'script') hasScript = true;
246
251
  else if (invoke.src === 'player') {
247
- const role = nonempty(state.meta?.playbook?.role, `state ${key} role`);
248
- const label = nonempty(descriptions.get(key), `state ${key} description`);
249
- roleStates[key] = { role, label };
252
+ // The engine keys a player state by its stable playbook id, which a
253
+ // parallel region's leaf carries distinct from its local key.
254
+ const stateId = typeof state.meta?.playbook?.stateId === 'string' ? state.meta.playbook.stateId
255
+ : typeof state.id === 'string' ? state.id : key;
256
+ if (RESERVED_KEYS.has(stateId)) throw new UnsupportedLinkProfile(`state ${stateId} requires ordinary linking`);
257
+ const role = nonempty(state.meta?.playbook?.role, `state ${stateId} role`);
258
+ const label = nonempty(descriptions.get(stateId), `state ${stateId} description`);
259
+ roleStates[stateId] = { role, label };
260
+ }
261
+ }
262
+ };
263
+ for (const [key, state] of Object.entries(config.states)) {
264
+ if (RESERVED_KEYS.has(key)) throw new UnsupportedLinkProfile(`state ${key} requires ordinary linking`);
265
+ if (!state || typeof state !== 'object') throw new TypeError(`state ${key} must be an object`);
266
+ if (state.type === 'parallel') {
267
+ // DR-067: walk a root parallel state's region leaves; the engine's
268
+ // construction preflight below owns validation of the compiled shape.
269
+ for (const [, region] of children(state.states)) {
270
+ for (const [leafKey, leaf] of children(region?.states)) inspectState(leafKey, leaf);
250
271
  }
272
+ continue;
273
+ }
274
+ if (state.states || (state.type !== undefined && !['atomic', 'final'].includes(state.type))) {
275
+ throw new UnsupportedLinkProfile(`state ${key} is outside the flat single-region profile`);
251
276
  }
277
+ inspectState(key, state);
252
278
  }
253
279
  return { roleStates, hasScript };
254
280
  }
@@ -355,6 +381,7 @@ const OPTION_SCHEMA = ${json(options)} as const;
355
381
  const INPUT_MAPPING = ${json(descriptor.inputMapping)} as const;
356
382
  const RESUMABLE_STATE_IDS: ReadonlySet<string> = new Set(${json(descriptor.resumableStateIds)});
357
383
  const UNFINISHED_FINAL_STATE_IDS: ReadonlySet<string> = new Set(${json(descriptor.unfinishedFinalStateIds)});
384
+ const VERBATIM_PAYLOAD_FIELDS: ReadonlySet<string> = new Set(${json(descriptor.verbatimPayloadFields)});
358
385
  export function validateOptions(value: unknown): PlaybookRuntimeOptions {
359
386
  const captured = snapshotJsonValue(value === undefined ? {} : value, ${JSON.stringify(`${descriptor.label} runtime options`)});
360
387
  if (captured === null || typeof captured !== 'object' || Array.isArray(captured)) {
@@ -380,7 +407,7 @@ ${quotedPlayer || labelledPlayer ? ' composePlayerPrompt,\n' : ''} ...${json(d
380
407
  Object.entries(INPUT_MAPPING).filter(([, key]) => options[key as keyof PlaybookRuntimeOptions] !== undefined)
381
408
  .map(([field, key]) => [field, options[key as keyof PlaybookRuntimeOptions]]),
382
409
  ),
383
- verbatimPayloadFields: new Set<string>(${json(descriptor.verbatimPayloadFields)}),
410
+ verbatimPayloadFields: VERBATIM_PAYLOAD_FIELDS,
384
411
  resumableStateIds: RESUMABLE_STATE_IDS,
385
412
  unfinishedFinalStateIds: UNFINISHED_FINAL_STATE_IDS,
386
413
  } satisfies XStatePlaybookRuntimeSpecV3<PlaybookRuntimeOptions>;
@@ -389,6 +416,7 @@ export const _internal = {
389
416
  ${quotedPlayer || labelledPlayer ? 'composePlayerPrompt,\n ' : hasPlayer ? `composePlayerPrompt: (input: PlaybookPlayerInput, _identity?: unknown${hasContinuationMode ? ', resuming = false' : ''}) =>
390
417
  defaultComposePlayerPrompt(input, ${json(descriptor.placeholderFields)}${hasContinuationMode ? ', resuming' : ''}),\n ` : ''}RESUMABLE_STATE_IDS,
391
418
  UNFINISHED_FINAL_STATE_IDS,
419
+ VERBATIM_PAYLOAD_FIELDS,
392
420
  };
393
421
  const createPlaybookRuntime: XStatePlaybookRuntimeFactory<
394
422
  XStatePlaybookRuntimeConstruction<PlaybookRuntimeOptions, PlaybookHostCapabilities>, 3
@@ -428,9 +456,19 @@ export async function runMaterializer(argv, input) {
428
456
  // Validate the export name before selecting it from the imported module.
429
457
  validateDescriptor(descriptor, engine);
430
458
  const module = await import(pathToFileURL(source).href);
431
- const specifier = relative(dirname(out), fsm).replaceAll('\\', '/');
459
+ const derived = relative(dirname(out), fsm).replaceAll('\\', '/');
460
+ let specifier = derived.startsWith('.') ? derived : `./${derived}`;
461
+ if (descriptor.fsmSpecifier !== undefined) {
462
+ // The declared `.js` sibling must be the build output of exactly this FSM:
463
+ // same directory and same basename, differing only in the extension.
464
+ const declared = resolve(dirname(out), descriptor.fsmSpecifier);
465
+ if (dirname(declared) !== dirname(fsm) || basename(declared, '.js') !== basename(fsm, extname(fsm))) {
466
+ throw new TypeError('fsmSpecifier must name the .js sibling of the --fsm file');
467
+ }
468
+ specifier = descriptor.fsmSpecifier;
469
+ }
432
470
  const code = materializeLink({ machine: module[descriptor.machineExport], descriptor,
433
- fsmSpecifier: specifier.startsWith('.') ? specifier : `./${specifier}`, engine });
471
+ fsmSpecifier: specifier, engine });
434
472
  const temporary = `${out}.${randomUUID()}.tmp`;
435
473
  try {
436
474
  const file = await open(temporary, 'wx', 0o644);
package/slc/optimize.md CHANGED
@@ -116,7 +116,10 @@ shell script are language-independent.
116
116
 
117
117
  The pass shall append one `## Optimizations` section at the end of the target
118
118
  listing every rewritten item, one bullet per item:
119
- `- <ITEM-ID>: <original behavior kind> → script`.
119
+ `- <ITEM-ID>: captain → script`, where `captain` is the fixed token for the
120
+ direct-Captain behavior kind the pass rewrites — the only kind eligible — so
121
+ the section reads the same in every target and a consumer can match it
122
+ exactly.
120
123
  When no item is eligible, the target shall be the source content unchanged,
121
124
  with no `## Optimizations` section.
122
125
 
@@ -4,10 +4,10 @@
4
4
 
5
5
  // Deterministic realization of the prompt-prefix pass (slc/prefix.md): every
6
6
  // standalone relay block of an eligible item moves after its last instruction
7
- // line, byte-for-byte, and one `## Prefixed prompts` section at the end lists
8
- // the items this and every earlier application rewrote. `--keep <ITEM-ID>`
9
- // excludes an item the pass judged ineligible; the tool never modifies the
10
- // source.
7
+ // line, byte-for-byte, and nothing records the rewrite beside the prompts; a
8
+ // `## Prefixed prompts` section an earlier version of the pass appended is
9
+ // removed. `--keep <ITEM-ID>` excludes an item the pass judged ineligible; the
10
+ // tool never modifies the source.
11
11
 
12
12
  import { readFile, writeFile } from 'node:fs/promises';
13
13
  import { resolve } from 'node:path';
@@ -16,10 +16,7 @@ import { fileURLToPath } from 'node:url';
16
16
  const HEADING = /^#{1,3}\s/;
17
17
  const ITEM_HEADING = /^###\s+([^\s]+)\s*$/;
18
18
  const PROMPT_LINE = /^>\s?(.*)$/;
19
- const SECTION = '## Prefixed prompts';
20
- const SECTION_HEADING = /^##\s+Prefixed prompts\s*$/;
21
- const LISTED = /^-\s+([^\s:]+):\s+relays → tail\s*$/;
22
- const bullet = (id) => `- ${id}: relays → tail`;
19
+ const LEGACY_SECTION = /^##\s+Prefixed prompts\s*$/;
23
20
 
24
21
  /** Classify one prompt line by its content inside the outer blockquote marker. */
25
22
  function classify(line) {
@@ -97,9 +94,9 @@ function prefixBlockquote(quote) {
97
94
  }
98
95
 
99
96
  /**
100
- * Rewrite every eligible item of a GEARS package not named by `keep`.
101
- * Returns the rewritten text with its provenance section, or the source text
102
- * unchanged when no item was eligible, plus the rewritten item ids.
97
+ * Rewrite every eligible item of a GEARS package not named by `keep` and
98
+ * remove any legacy `## Prefixed prompts` section. Returns the target text —
99
+ * the source text itself when neither applies — and the rewritten item ids.
103
100
  */
104
101
  export function prefixPrompts(text, { keep = [] } = {}) {
105
102
  // Keep the source's own line-ending convention so untouched lines stay bytes.
@@ -130,24 +127,25 @@ export function prefixPrompts(text, { keep = [] } = {}) {
130
127
  cursor = quoteEnd;
131
128
  rewritten.push(start.id);
132
129
  }
133
- if (rewritten.length === 0) return { text, rewritten };
134
130
  out.push(...lines.slice(cursor));
135
- // One provenance section follows every other section, ending in one newline;
136
- // it replaces every section an earlier application appended and lists, in
137
- // item order, each item this or that application rewrote.
138
- const listed = new Set(rewritten);
131
+ // A legacy section runs to the next heading; at the end of the file it
132
+ // takes the blank lines before it too, so the file ends as it did before
133
+ // the section was appended.
134
+ let removed = false;
139
135
  for (let index = out.length - 1; index >= 0; index--) {
140
- if (!SECTION_HEADING.test(out[index])) continue;
136
+ if (!LEGACY_SECTION.test(out[index])) continue;
137
+ let start = index;
141
138
  let end = index + 1;
142
- for (; end < out.length && !HEADING.test(out[end]); end++) {
143
- const entry = LISTED.exec(out[end]);
144
- if (entry !== null) listed.add(entry[1]);
139
+ while (end < out.length && !HEADING.test(out[end])) end++;
140
+ if (end === out.length) {
141
+ while (start > 0 && out[start - 1] === '') start--;
142
+ out.splice(start, end - start, '');
143
+ } else {
144
+ out.splice(start, end - start);
145
145
  }
146
- out.splice(index, end - index);
146
+ removed = true;
147
147
  }
148
- while (out.length > 0 && out[out.length - 1] === '') out.pop();
149
- const ids = starts.map((start) => start.id).filter((id) => listed.has(id));
150
- out.push('', SECTION, '', ...ids.map(bullet), '');
148
+ if (rewritten.length === 0 && !removed) return { text, rewritten };
151
149
  return { text: out.join(newline), rewritten };
152
150
  }
153
151
 
@@ -172,12 +170,15 @@ async function main(argv) {
172
170
  'usage: prefix-prompts.mjs --source <gears.md> --target <gears.md> [--keep <ITEM-ID>]...',
173
171
  );
174
172
  }
175
- const { text, rewritten } = prefixPrompts(await readFile(resolve(source), 'utf8'), { keep });
173
+ const input = await readFile(resolve(source), 'utf8');
174
+ const { text, rewritten } = prefixPrompts(input, { keep });
176
175
  await writeFile(resolve(target), text);
177
176
  process.stdout.write(
178
- rewritten.length === 0
179
- ? 'no eligible item; target equals source\n'
180
- : `${rewritten.join('\n')}\n`,
177
+ rewritten.length > 0
178
+ ? `${rewritten.join('\n')}\n`
179
+ : text === input
180
+ ? 'no eligible item; target equals source\n'
181
+ : 'no eligible item; legacy ## Prefixed prompts section removed\n',
181
182
  );
182
183
  }
183
184
 
package/slc/prefix.md CHANGED
@@ -79,8 +79,8 @@ rewrite, merge, split, or drop any line.
79
79
  Source prose that places a relay after an instruction, or an instruction at
80
80
  the end of the prompt, tells [text2gears](text2gears.md) how to compose the
81
81
  prompt faithfully; it does not make the item ineligible.
82
- This pass overrides that composition order by design, records the override in
83
- its provenance section, and `--no-optimize` restores it.
82
+ This pass overrides that composition order by design, and `--no-optimize`
83
+ restores it.
84
84
 
85
85
  ## Rewriting
86
86
 
@@ -98,7 +98,8 @@ For each eligible item, the pass shall rewrite only the blockquote:
98
98
  - Keep the item's ID, heading, condition, acting clause, `Results:`, and any
99
99
  other metadata unchanged.
100
100
  - Preserve every other item and every non-item section byte-for-byte,
101
- including an `## Optimizations` section the optimize pass appended.
101
+ including an `## Optimizations` section the optimize pass appended, except
102
+ a legacy `## Prefixed prompts` section (see [Provenance](#provenance)).
102
103
 
103
104
  Because no line changes, the target stays in the Source language and every
104
105
  authored fragment remains intact; only the order of relay blocks relative to
@@ -142,22 +143,24 @@ Results:
142
143
 
143
144
  ## Provenance
144
145
 
145
- The target shall end with one `## Prefixed prompts` section — after an
146
- `## Optimizations` section when one is present — listing every rewritten
147
- item, one bullet per item in item order: `- <ITEM-ID>: relays → tail`.
148
- A section an earlier application left in the source is replaced, and the
149
- items it listed stay listed.
150
- When no item is eligible, the target shall be the source content unchanged;
151
- a package the pass never rewrote carries no `## Prefixed prompts` section.
152
-
153
- The section is what lets a Source-to-GEARS fidelity checker accept the new
154
- order: for a listed item it requires each instruction and relay block of the
155
- fragments the item carries intact, blank lines inside a block included, and
156
- used exactly once, the instruction blocks in Source order, the relay blocks in
157
- Source order, and every relay after the last instruction, a relay Source joined
158
- directly to an instruction staying beside it with its authored boundary, a bare
159
- relay the prose authored free to trail, and one occurrence standing for one
160
- authored fragment; for any other item it requires Source order throughout.
146
+ The pass shall record nothing beside the rewritten prompts, whose layout
147
+ shows the rewrite.
148
+ A `## Prefixed prompts` section an earlier version of this pass appended shall
149
+ be removed — up to the next heading, and at the end of the file with the blank
150
+ lines before it; when no item is eligible and no such section is present, the
151
+ target shall be the source content unchanged.
152
+ Re-running the pass is a no-op by layout: an item whose relays already trail
153
+ is not eligible.
154
+
155
+ A Source-to-GEARS fidelity checker therefore reads the layout from each prompt:
156
+ it accepts an item either in Source order throughout or in the prefix-first
157
+ layout, which requires each instruction and relay block of the fragments the
158
+ item carries intact, blank lines inside a block included, and used exactly
159
+ once, the instruction blocks in Source order, the relay blocks in Source order,
160
+ and every relay after the last instruction, a relay Source joined directly to
161
+ an instruction staying beside it with its authored boundary, a bare relay the
162
+ prose authored free to trail, and one occurrence standing for one authored
163
+ fragment.
161
164
 
162
165
  ## Deterministic rewriting
163
166
 
@@ -168,9 +171,8 @@ node "<definition-directory>/prefix-prompts.mjs" --source "<source.gears.md>" --
168
171
  ```
169
172
 
170
173
  It reads the source, rewrites every item eligible by the mechanical rules
171
- above, writes the one provenance section and the target, and prints the
172
- rewritten item IDs — or reports that no item was eligible and the target
173
- equals the source.
174
+ above, removes a legacy `## Prefixed prompts` section, writes the target, and
175
+ prints the rewritten item IDs — or reports that no item was eligible.
174
176
  `--keep` excludes an item the tool would otherwise rewrite.
175
177
 
176
178
  The pass shall use the tool rather than rewrite by hand:
@@ -179,8 +181,9 @@ The pass shall use the tool rather than rewrite by hand:
179
181
  instruction line, whether an instruction depends on a relay standing before
180
182
  it; that judgment is the pass's only non-mechanical step.
181
183
  2. Run the tool with `--keep` for each item so excluded.
182
- 3. Read the target and confirm that every listed item's instructions and
183
- relays are intact and that nothing else changed.
184
+ 3. Read the target and confirm that every rewritten item's instructions and
185
+ relays are intact and that nothing changed beyond the rewritten items and
186
+ the removal of a legacy `## Prefixed prompts` section.
184
187
 
185
188
  The tool writes only the declared target and never modifies the source.
186
189
 
@@ -1,8 +1,17 @@
1
1
  {
2
2
  "schema": "sublang.slc.pin-inputs.v1",
3
3
  "closures": {
4
- "gears2fsm": ["text2gears.md", "link.md", "workflow-contracts.json"],
5
- "prefix": ["prefix-prompts.mjs"],
4
+ "text2gears": [
5
+ "workflow-contracts.json"
6
+ ],
7
+ "gears2fsm": [
8
+ "text2gears.md",
9
+ "link.md",
10
+ "workflow-contracts.json"
11
+ ],
12
+ "prefix": [
13
+ "prefix-prompts.mjs"
14
+ ],
6
15
  "link": [
7
16
  "text2gears.md",
8
17
  "gears2fsm.md",
package/slc/text2gears.md CHANGED
@@ -157,13 +157,37 @@ Keep each declaration outside plain-text parentheses; explanatory symbols in
157
157
  parenthetical guidance use plain text, never separate backticks, because those
158
158
  backticks would declare extra required fields. Guidance may instead occur
159
159
  inside a field's complete annotation, including any parentheses there.
160
- For example, use `` `codeCommit` (new code-owned commit) `` or
161
- `` `codeCommit: <new code-owned commit>` ``, without backticks around code.
160
+ For example, use `` `latestCommit` (new code-owned commit) `` or
161
+ `` `latestCommit: <commit identity>` ``, without backticks around code.
162
+ The commit a call itself creates is always declared as `latestCommit`, the
163
+ canonical effect-owned property every linked runtime fills from the
164
+ repository receipt and every terminal output projects from.
162
165
  An output property name shall match the same ASCII identifier pattern as a
163
166
  guard name: a kebab-case Source placeholder such as `<coder-output>` names the
164
167
  property `coderOutput` through the canonical kebab-token-to-camel-field mapping
165
168
  of [link](link.md), never a quoted kebab-case key, because downstream artifacts
166
169
  and calling playbooks consume these properties by name.
170
+ A result shall declare an output property only where a consumer requires it:
171
+ a later item's `<placeholder>`, the workflow's terminal return, or the
172
+ workflow's declared public interface below.
173
+ A detail the acting agent reports only within its final text — a reason, a
174
+ summary, a list — travels in the result's verbatim final-text property and is
175
+ not a separate judge-authored property.
176
+
177
+ Where the selected pipeline supplies the
178
+ [workflow contracts](workflow-contracts.json) catalog and the Source's basename
179
+ is one of its `literalTargetBindings`, the compiled workflow is held to that
180
+ builtin's declared output interface, because callers compiled against the
181
+ catalog address it by that id: a result property whose value that interface
182
+ returns shall take the interface's property name — `branch`, `issueSummary`,
183
+ and `coderOutput` for `branch`; `evaluatedRevision` for the revision a clean
184
+ review round evaluated — whether the value is semantic, presentation, or
185
+ effect-owned, while the commit a call itself creates keeps the canonical
186
+ `latestCommit` for the FSM to project into the interface's field.
187
+ A Source placeholder that names such a value otherwise is an inconsistency
188
+ between the Source and the catalog: leave the Target unwritten and report it
189
+ as an incompatible compiler input rather than rename or invent.
190
+ A Source outside those ids is named from its own words as above.
167
191
 
168
192
  A produced value consumed later shall have a declared producer: where any
169
193
  later item's blockquote reads a value through a `<placeholder>`, the item
@@ -172,14 +196,37 @@ whose relevant description names the produced output property, using the
172
196
  placeholder's exact identifier — this is what lets the FSM thread the value
173
197
  through typed context.
174
198
  A single-outcome producer then declares exactly one bullet naming the
175
- property; this consumed-output case is the sole one in which a
176
- single-outcome behavior carries a `Results:` label.
199
+ property; this consumed-output case and the qualified-outcome case below are
200
+ the two in which a single-outcome behavior carries a `Results:` label.
201
+ The commit a call itself creates is the one exception to naming the property
202
+ after the placeholder: its producer declares `latestCommit: <commit identity>`
203
+ whatever placeholder a later prompt reads it through — `<code-commit>` or
204
+ `<decide-commit>`, for instance — because the linked runtime fills that
205
+ property from the repository receipt rather than from the player, and
206
+ [gears2fsm](gears2fsm.md#context-and-prompts) binds the Source's placeholder
207
+ to the retained commit.
208
+ A placeholder the Source defines as a labelled section of another relayed
209
+ value, naming the label that opens the section and the labels that end it —
210
+ `<original-intent>` as the `Original intent:` section of the caller's request,
211
+ which runs to the `Review scope:` line or to the end of the request, for
212
+ instance — is derived, not produced: text2gears shall keep that defining
213
+ sentence verbatim in the package introduction, or in the item's prose where the
214
+ Source states it there, name the placeholder as the Source does, and declare no
215
+ result property for it, because no player produces the value and
216
+ [gears2fsm](gears2fsm.md#context-and-prompts) derives it from the relayed text.
177
217
 
178
218
  Where a later prompt relays a delegated player's whole final response as quoted context, the producer shall declare that property in the exact annotated form `` `<field>: <verbatim final text>` ``.
179
219
  The annotation makes the field runtime-owned: the adjudicator selects the result guard, while the linked runtime carries the player's canonical final text into that field instead of asking a judge to reproduce it.
180
220
  A distinct typed field extracted from that response remains judge-authored even when a later prompt quotes its exact value; quoting a field does not turn it into the player's whole final response.
181
221
  One property name shall not be annotated as verbatim in one result contract and judge-authored in another; text2gears shall choose distinct properties or report that the Source cannot be represented by the current contract.
182
222
 
223
+ A Source statement that qualifies an outcome's evidence — what affirmatively
224
+ supports it, what supports no outcome, such as "a progress report, status
225
+ update, or promise of a later result supports no review outcome" — shall be
226
+ carried in that outcome's result description, the only text the adjudicator
227
+ reads when it selects the guard; left in the item's prose, it reaches no
228
+ judge.
229
+
183
230
  Result metadata is compiler control data, not part of the acting agent's
184
231
  prompt.
185
232
  text2gears shall not put guard names, result-property schema, JSON control
@@ -194,12 +241,16 @@ Where Source restricts an initial Captain to routing, text2gears shall preserve
194
241
  only the authored question and delegation outcomes and shall not infer a
195
242
  direct-answer or terminal result merely because Captain is the acting agent.
196
243
 
197
- A single-outcome behavior whose output no later item consumes carries no
198
- `Results:` label; downstream,
244
+ A single-outcome behavior whose output no later item consumes and whose
245
+ outcome Source leaves unqualified carries no `Results:` label; downstream,
199
246
  [gears2fsm](gears2fsm.md#setup) gives its state the default single-outcome
200
247
  contract, so text2gears shall not invent a one-bullet `Results:` block for it.
201
248
  When a later item does consume its output, the produced-value rule above
202
- applies instead.
249
+ applies instead; when Source qualifies the one outcome's evidence — what
250
+ affirmatively supports it, what supports no outcome — the behavior carries
251
+ exactly one `Results:` bullet naming that outcome and carrying the
252
+ qualification, with an output property only where a consumer requires one,
253
+ because the adjudicator reads the qualification nowhere else.
203
254
 
204
255
  When Source requires a terminal return to the caller, preserve every returned value or fact and its return condition as an explicit workflow output obligation in GEARS.
205
256
  Merely naming a value in a completion predicate or an acting result does not state that the workflow returns it.
@@ -215,10 +266,16 @@ when the answer changes its complete runtime prompt. It shall not emit a
215
266
  second item solely for "Boss answers," "after the question," or clearing the
216
267
  consumed question/reply. The FSM and linker own the same-leaf suspension,
217
268
  continuation blocks, and consumed-context cleanup.
218
- When such an authored result asks Boss and waits, its `Results:` description
219
- shall declare `question: <verbatim final text>` as an output property; the
220
- result name or prose saying that a question is asked is not the field
221
- declaration.
269
+ Outside the decide-call-observe routing contract, an authored Boss question
270
+ is the framework-owned `needsBossReply` outcome that
271
+ [gears2fsm](gears2fsm.md#boss-reply-suspension) adds to every Captain- or
272
+ player-invoking state: text2gears shall keep the question's conditions in the
273
+ prompt and its continuation in the item's prose and shall declare no result
274
+ for it, because a second guard for the same wait would give the adjudicator
275
+ two equivalent outcomes.
276
+ Only the routing contract's own `question` and `followUpQuestion` results
277
+ declare the question, as `question: <verbatim final text>`; there, the result
278
+ name or prose saying that a question is asked is not the field declaration.
222
279
 
223
280
  This rule is an exception to splitting by accumulated prompt content below.
224
281
  Split only when Source requires a genuinely different acting behavior after
@@ -395,6 +452,9 @@ Markdown escaping is Source syntax, not content: extraction shall resolve escape
395
452
 
396
453
  Partition items by every variable that determines prompt content — including accumulated state when the trigger alone doesn't.
397
454
 
455
+ Conversely, do not split by trigger alone: where two or more triggers lead to one behavior with the same acting clause, prompt, and result contract — a nested call Source says a later path makes "with the same input as" an earlier path's, for instance — text2gears shall emit one item whose condition joins the triggers and shall keep each path's continuation as that item's prose.
456
+ An item per trigger duplicates an identical prompt and multiplies the FSM's states without a content difference.
457
+
398
458
  ### Prune dead disjuncts
399
459
 
400
460
  Drop disjunctive branches incompatible with the rest of an item's condition or prompt.
package/src/runtime.d.ts CHANGED
@@ -173,10 +173,13 @@ export type PlaybookRunResult = {
173
173
  pendingCall: PlaybookPendingCall;
174
174
  };
175
175
  export interface PlaybookPorts {
176
+ /** Acknowledge the durable start before work and its result before transition. */
177
+ recordStep?(step: PlaybookStepRecord, position?: PlaybookRuntimeSnapshot): Promise<void>;
176
178
  callPlayer(roleId: string, prompt: string, signal: AbortSignal, options: PlayerCallOptions): Promise<PlayerResult>;
177
179
  callCaptain(prompt: string, signal: AbortSignal, options: CaptainCallOptions): Promise<CaptainResult>;
178
180
  callJudge(prompt: string, signal: AbortSignal): Promise<string>;
179
181
  callPlaybook(request: PlaybookCallRequest, signal: AbortSignal): Promise<PlaybookCallStart>;
182
+ /** Mark question presentation with data.kind = "boss-question"; Captain relays it from pendingQuestions. */
180
183
  emitStatus(message: string, data?: unknown): Promise<void>;
181
184
  emitTelemetry(event: {
182
185
  topic: string;
@@ -275,6 +278,8 @@ export interface PlaybookEffectBoundary {
275
278
  readonly baseline: PlaybookRepositoryObservation;
276
279
  readonly after?: PlaybookRepositoryObservation;
277
280
  readonly physicalReceipt?: PlaybookRepositoryReceipt;
281
+ /** A later exact restoration permits replay of this standalone read-only call. */
282
+ readonly restored?: PlaybookRepositoryObservation;
278
283
  readonly finalText?: string;
279
284
  readonly semanticCandidate?: JsonValue;
280
285
  readonly initialSemanticCandidate?: JsonValue;
@@ -286,7 +291,7 @@ export interface PlaybookEffectBoundary {
286
291
  readonly logicalOperationId?: string;
287
292
  }
288
293
  /** One physical boundary before the host assigns attempt and sequence data. */
289
- export type PlaybookEffectBoundaryStart = Omit<PlaybookEffectBoundary, 'sequence' | 'attemptId' | 'attemptNumber' | 'after' | 'physicalReceipt' | 'finalText' | 'semanticCandidate' | 'initialSemanticCandidate'>;
294
+ export type PlaybookEffectBoundaryStart = Omit<PlaybookEffectBoundary, 'sequence' | 'attemptId' | 'attemptNumber' | 'after' | 'physicalReceipt' | 'restored' | 'finalText' | 'semanticCandidate' | 'initialSemanticCandidate'>;
290
295
  /** One deferred logical operation spanning its ordered physical boundaries. */
291
296
  export interface PlaybookEffectLogicalOperation {
292
297
  readonly sequence: number;
@@ -388,8 +393,26 @@ export interface PlaybookRuntimeSnapshot {
388
393
  readonly boundaryPrefix: number;
389
394
  readonly attemptId: string | null;
390
395
  };
396
+ /** Interrupted invocation, captured before its external call (DR-069). */
397
+ recoveryCheckpoint?: PlaybookRecoveryCheckpoint;
391
398
  suspendedCall?: PlaybookSuspendedCall;
392
399
  }
400
+ export interface PlaybookRecoveryCheckpoint {
401
+ readonly id?: string;
402
+ readonly stateId: string;
403
+ readonly prompt: string;
404
+ readonly machine: JsonValue;
405
+ readonly boundaryPrefix: number;
406
+ /** Completed actor output supplied by the execution journal on restore. */
407
+ readonly result?: JsonValue;
408
+ readonly delivered?: true;
409
+ }
410
+ export interface PlaybookStepRecord {
411
+ readonly id: string;
412
+ readonly kind: 'player' | 'captain' | 'script';
413
+ readonly stateId: string;
414
+ readonly result?: JsonValue;
415
+ }
393
416
  export type PlaybookControlStanding = 'ready' | 'no-op' | 'blocked';
394
417
  export type PlaybookControlActionReason = 'receipt-complete';
395
418
  export interface PlaybookControlAction {
@@ -398,6 +421,20 @@ export interface PlaybookControlAction {
398
421
  standing?: PlaybookControlStanding;
399
422
  reason?: PlaybookControlActionReason;
400
423
  }
424
+ export interface PlaybookRecoveryOffer {
425
+ prompt: string;
426
+ description?: string;
427
+ /** Runtime-owned conditions to satisfy; these never authorize task completion. */
428
+ preparation?: string;
429
+ /** Saved observations and result choices for diagnosing the stopped step. */
430
+ evidence?: JsonValue;
431
+ continuation: {
432
+ kind: 'reply';
433
+ } | {
434
+ kind: 'runtime';
435
+ actionId: string;
436
+ };
437
+ }
401
438
  export interface PlaybookControlView {
402
439
  state: PlaybookState;
403
440
  stateDescription?: string;
@@ -405,6 +442,8 @@ export interface PlaybookControlView {
405
442
  pendingQuestions: readonly PlaybookPendingBossQuestion[];
406
443
  lastError?: NormalizedError;
407
444
  actions: readonly PlaybookControlAction[];
445
+ /** Context only for an explicitly authorized Captain preparation call. */
446
+ recovery?: PlaybookRecoveryOffer;
408
447
  }
409
448
  export type PlaybookControlReceipt = {
410
449
  disposition: 'rejected';
@@ -415,13 +454,16 @@ export type PlaybookControlReceipt = {
415
454
  } | {
416
455
  disposition: 'failed';
417
456
  error: NormalizedError;
457
+ run?: PlaybookRunResult;
418
458
  };
419
459
  export interface PlaybookRetainedGenerationMetadata {
420
460
  readonly unfinishedFinalStateIds: readonly string[];
421
461
  }
422
462
  export interface PlaybookRuntime {
423
463
  init(session: PlaybookSession): Promise<void>;
424
- exportSnapshot?(): PlaybookRuntimeSnapshot | undefined;
464
+ exportSnapshot?(checkpoint?: {
465
+ child?: PlaybookPendingCall;
466
+ }): PlaybookRuntimeSnapshot | undefined;
425
467
  restore?(session: PlaybookSession, snapshot: PlaybookRuntimeSnapshot): Promise<void>;
426
468
  adopt?(session: PlaybookSession, snapshot: PlaybookRuntimeSnapshot, context: PlaybookAdoptionContext): Promise<void>;
427
469
  readonly retainedGenerationMetadata?: PlaybookRetainedGenerationMetadata;
@@ -447,6 +489,8 @@ export interface PlaybookRuntime {
447
489
  handleBossInput(turn: {
448
490
  text: string;
449
491
  signal: AbortSignal;
492
+ /** Called synchronously just before accepting the event, at most once. */
493
+ onAccepted?: () => void;
450
494
  }): Promise<PlaybookRunResult>;
451
495
  resumePlaybookCall(input: {
452
496
  callId: string;