@sublang/playbook 17.6.0 → 17.8.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.
@@ -20,9 +20,9 @@ import { randomUUID } from 'node:crypto';
20
20
  import { isDeepStrictEqual } from 'node:util';
21
21
  import PQueue from 'p-queue';
22
22
  import { createActor, fromPromise } from 'xstate';
23
- import { createAcceptedOutcomeConsumer, } from './accepted-outcome.js';
23
+ import { ACCEPTED_OUTCOME_ACTION_TYPE, createAcceptedOutcomeConsumer, } from './accepted-outcome.js';
24
24
  import { assertPlaybookFailureCause } from './runtime.js';
25
- import { assertPlaybookRuntimeSnapshot, assertPlaybookEffectLedger, attachPlaybookFailureCause, combineAbortSignals, createNestedPlaybookBridge, detachPersistedMachineSnapshot, normalizeError, normalizePlaybookSnapshot, snapshotJsonValue, snapshotPlaybookSession, isPlaybookEffectLedgerMonotonicExtension, PlaybookSemanticCandidateStructureError, reconcilePlaybookSemanticEvidence, validateCaptainResult, validatePlayerResult, waitForPlaybookQuiescence, } from './xstate-runtime.js';
25
+ import { assertPlaybookRuntimeSnapshot, assertPlaybookEffectLedger, attachPlaybookFailureCause, combineAbortSignals, createNestedPlaybookBridge, describePlaybookSemanticValueShape, detachPersistedMachineSnapshot, normalizeError, normalizePlaybookSnapshot, snapshotJsonValue, snapshotPlaybookSession, isPlaybookEffectLedgerMonotonicExtension, PlaybookSemanticCandidateStructureError, reconcilePlaybookSemanticEvidence, validateCaptainResult, validatePlayerResult, waitForPlaybookQuiescence, } from './xstate-runtime.js';
26
26
  export const BOSS_REPLY_ERRORS = {
27
27
  missingQuestion: "needsBossReply outcome missing 'question' field",
28
28
  unregisteredState: (stateId) => `state ${stateId} declared needsBossReply but is not registered as resumable`,
@@ -906,7 +906,7 @@ export function defaultBuildJudgePrompt(input, finalText) {
906
906
  * self-inflicted defect. Exported so linked machinery of an artifact's own
907
907
  * renders the identical contract instead of restating it.
908
908
  */
909
- export function renderGovernedOutcomeContract(guard, description, outcome) {
909
+ export function renderGovernedOutcomeContract(guard, description, outcome, shapes) {
910
910
  const { meaning, clause } = splitOutputClause(description);
911
911
  // Each field's authored segment runs from its token to the next token:
912
912
  // the annotated `name: <placeholder>` form, or the bare name followed by
@@ -934,11 +934,20 @@ export function renderGovernedOutcomeContract(guard, description, outcome) {
934
934
  }
935
935
  const replyMembers = [`"guard": ${JSON.stringify(guard)}`];
936
936
  const semanticAsAuthored = [];
937
+ const valueShapes = [];
937
938
  const runtimeSupplied = [];
938
939
  for (const [field, authority] of Object.entries(outcome?.fields ?? {})) {
939
940
  if (authority === 'semantic') {
940
941
  const entry = authored.get(field);
941
- replyMembers.push(`${JSON.stringify(field)}: ${entry?.placeholder ?? '<string>'}`);
942
+ // DR-089: the value has the shape the compiled machine admits.
943
+ const shape = shapes !== undefined && Object.prototype.hasOwnProperty.call(shapes, field)
944
+ ? shapes[field]
945
+ : 'string';
946
+ const words = describePlaybookSemanticValueShape(shape);
947
+ replyMembers.push(`${JSON.stringify(field)}: ${entry?.placeholder ??
948
+ (shape === 'string' ? '<string>' : `<${words.replace(/^a /, '')}>`)}`);
949
+ if (shape !== 'string')
950
+ valueShapes.push(`\`${field}\` is ${words}`);
942
951
  semanticAsAuthored.push(entry?.segment ?? `\`${field}\``);
943
952
  }
944
953
  else {
@@ -949,6 +958,9 @@ export function renderGovernedOutcomeContract(guard, description, outcome) {
949
958
  meaning.length === 0 ? `- \`${guard}\`` : `- \`${guard}\` — ${meaning}`,
950
959
  ` Reply exactly: { ${replyMembers.join(', ')} }`,
951
960
  ];
961
+ if (valueShapes.length > 0) {
962
+ lines.push(` JSON value shape: ${valueShapes.join('; ')}`);
963
+ }
952
964
  if (semanticAsAuthored.length > 0) {
953
965
  lines.push(` Semantic fields as authored: ${semanticAsAuthored.join('; ')}`);
954
966
  }
@@ -957,7 +969,7 @@ export function renderGovernedOutcomeContract(guard, description, outcome) {
957
969
  }
958
970
  return lines;
959
971
  }
960
- function buildGovernedJudgePrompt(input, finalText, outcomes, correction) {
972
+ function buildGovernedJudgePrompt(input, finalText, outcomes, correction, shapes) {
961
973
  const lines = [
962
974
  'This is hidden control work. Do not call tools, inspect files, or seek external evidence.',
963
975
  'Decide only from the supplied player output and declared outcomes.',
@@ -976,7 +988,7 @@ function buildGovernedJudgePrompt(input, finalText, outcomes, correction) {
976
988
  '',
977
989
  ];
978
990
  for (const [guard, description] of Object.entries(input.result)) {
979
- lines.push(...renderGovernedOutcomeContract(guard, description, outcomes[guard]));
991
+ lines.push(...renderGovernedOutcomeContract(guard, description, outcomes[guard], shapes?.[guard]));
980
992
  }
981
993
  if (correction !== undefined) {
982
994
  lines.push('', 'Your first reply was structurally invalid:', '', '```', correction.reply, '```', '', `Validation error: ${correction.error}`, 'Correct only that structure using the same player output and outcome schema.');
@@ -1133,9 +1145,15 @@ export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onCon
1133
1145
  /**
1134
1146
  * Default direct-Captain adjudicator prompt (DR-025). The single statement of
1135
1147
  * the `{ guard, …structuralPayloadFields }` reply contract, shared with the
1136
- * compiled default Captain artifact so the wording cannot drift.
1148
+ * compiled default Captain artifact so the wording cannot drift. Each declared
1149
+ * key keeps its description verbatim and gains the exact reply it takes
1150
+ * (DR-089): `guard` plus the structural fields its `Output shall include`
1151
+ * clause names, each with its authored placeholder, while the visible
1152
+ * `question` and `response` fields are named as runtime-supplied. A
1153
+ * `correction` restates a structurally invalid first reply and its
1154
+ * validation error for the one corrective adjudication.
1137
1155
  */
1138
- export function defaultBuildCaptainJudgePrompt(input, finalText) {
1156
+ export function defaultBuildCaptainJudgePrompt(input, finalText, correction) {
1139
1157
  const lines = [];
1140
1158
  lines.push('Adjudicate the direct Captain output for this FSM state.');
1141
1159
  lines.push(`State id: ${input.stateId}`);
@@ -1149,25 +1167,71 @@ export function defaultBuildCaptainJudgePrompt(input, finalText) {
1149
1167
  lines.push('Result keys and descriptions:');
1150
1168
  for (const [key, description] of Object.entries(input.result)) {
1151
1169
  lines.push(`- \`${key}\` — ${description}`);
1170
+ const { clause } = splitOutputClause(description);
1171
+ const replyMembers = [`"guard": ${JSON.stringify(key)}`];
1172
+ const runtimeSupplied = [];
1173
+ const seen = new Set();
1174
+ for (const m of clause === undefined
1175
+ ? []
1176
+ : clause.matchAll(PAYLOAD_FIELD_PATTERN)) {
1177
+ const field = m[1];
1178
+ if (seen.has(field))
1179
+ continue;
1180
+ seen.add(field);
1181
+ if (field === 'question' || field === 'response') {
1182
+ runtimeSupplied.push(`\`${field}\``);
1183
+ continue;
1184
+ }
1185
+ const placeholder = m[2]?.trim();
1186
+ replyMembers.push(`${JSON.stringify(field)}: ${placeholder !== undefined && placeholder.length > 0
1187
+ ? placeholder
1188
+ : '<value>'}`);
1189
+ }
1190
+ lines.push(` Reply exactly: { ${replyMembers.join(', ')} }`);
1191
+ if (runtimeSupplied.length > 0) {
1192
+ lines.push(` Runtime-supplied, do not include: ${runtimeSupplied.join(', ')}`);
1193
+ }
1152
1194
  }
1153
1195
  lines.push('');
1154
1196
  lines.push('Pick exactly one outcome by `guard` and return JSON ' +
1155
1197
  '`{ guard, …structuralPayloadFields }`. Do not include `question` or ' +
1156
1198
  '`response`; the runtime injects the visible text.');
1199
+ lines.push('Reply with exactly that outcome\'s reply shape above and no other field.');
1200
+ if (correction !== undefined) {
1201
+ lines.push('', 'Your first reply was structurally invalid:', '', '```', correction.reply, '```', '', `Validation error: ${correction.error}`, 'Correct only that structure using the same Captain output and outcome schema.');
1202
+ }
1157
1203
  return lines.join('\n');
1158
1204
  }
1205
+ /**
1206
+ * A direct-Captain adjudication reply that fails structural validation
1207
+ * (DR-089): it earns one corrective adjudication, and a second such reply
1208
+ * parks the step as a decided `judge-failed` failure rather than escaping as
1209
+ * a control-plane error.
1210
+ */
1211
+ class CaptainAdjudicationStructureError extends Error {
1212
+ constructor(message) {
1213
+ super(message);
1214
+ this.name = 'CaptainAdjudicationStructureError';
1215
+ }
1216
+ }
1159
1217
  function adjudicateCaptainOutput(extractFields, input, finalText, judgeText) {
1160
- const parsed = parseJudgeJson(judgeText);
1218
+ let parsed;
1219
+ try {
1220
+ parsed = parseJudgeJson(judgeText);
1221
+ }
1222
+ catch (error) {
1223
+ throw new CaptainAdjudicationStructureError(`adjudicate: ${error instanceof Error ? error.message : 'judge response is not valid JSON'}`);
1224
+ }
1161
1225
  if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
1162
- throw new Error('adjudicate: judge response is not a JSON object');
1226
+ throw new CaptainAdjudicationStructureError('adjudicate: judge response is not a JSON object');
1163
1227
  }
1164
1228
  const obj = parsed;
1165
1229
  const guard = obj.guard;
1166
1230
  if (typeof guard !== 'string') {
1167
- throw new Error('adjudicate: judge response missing string "guard" field');
1231
+ throw new CaptainAdjudicationStructureError('adjudicate: judge response missing string "guard" field');
1168
1232
  }
1169
1233
  if (!Object.prototype.hasOwnProperty.call(input.result, guard)) {
1170
- throw new Error(`adjudicate: unknown guard "${guard}" — declared guards: ${Object.keys(input.result).join(', ')}`);
1234
+ throw new CaptainAdjudicationStructureError(`adjudicate: unknown guard "${guard}" — declared guards: ${Object.keys(input.result).join(', ')}`);
1171
1235
  }
1172
1236
  const required = extractFields(input.result[guard]);
1173
1237
  const allowed = new Set(['guard']);
@@ -1177,14 +1241,14 @@ function adjudicateCaptainOutput(extractFields, input, finalText, judgeText) {
1177
1241
  }
1178
1242
  for (const key of Object.keys(obj)) {
1179
1243
  if (!allowed.has(key)) {
1180
- throw new Error(`adjudicate: judge response supplied undeclared field "${key}" for guard "${guard}"`);
1244
+ throw new CaptainAdjudicationStructureError(`adjudicate: judge response supplied undeclared field "${key}" for guard "${guard}"`);
1181
1245
  }
1182
1246
  }
1183
1247
  for (const field of required) {
1184
1248
  if (field === 'question' || field === 'response')
1185
1249
  continue;
1186
1250
  if (obj[field] === undefined || obj[field] === null) {
1187
- throw new Error(`adjudicate: judge response missing required field "${field}" for guard "${guard}"`);
1251
+ throw new CaptainAdjudicationStructureError(`adjudicate: judge response missing required field "${field}" for guard "${guard}"`);
1188
1252
  }
1189
1253
  }
1190
1254
  const output = { ...obj, guard };
@@ -1934,6 +1998,156 @@ function snapshotOutcomeAuthority(descriptor, label, playerStates, verbatimPaylo
1934
1998
  governedPlayerStates: Object.freeze(normalizedStates),
1935
1999
  });
1936
2000
  }
2001
+ // ---------------------------------------------------------------------------
2002
+ // DR-089: one type contract for semantic-owned fields. The compiled machine's
2003
+ // own validator decides which JSON value it admits for each field, so the
2004
+ // runtime asks it once at construction: each governed outcome is presented to
2005
+ // the machine's transition selection — guards only, no action runs — with
2006
+ // sample values, and the first combination of shapes whose arm carries that
2007
+ // outcome's accepted-outcome marker is the contract the judge is shown and the
2008
+ // reconciler enforces. A field whose shape the machine does not reveal, or
2009
+ // that it admits as a string, stays a string.
2010
+ // ---------------------------------------------------------------------------
2011
+ const SEMANTIC_SHAPE_SAMPLES = [
2012
+ ['string', 'sample'],
2013
+ ['string-array', ['sample']],
2014
+ ['number-array', [1]],
2015
+ ['number', 1],
2016
+ ['boolean', true],
2017
+ ];
2018
+ const SHAPE_PROBE_TEXT = 'sample';
2019
+ const SHAPE_PROBE_COMMIT = '0123456789abcdef0123456789abcdef01234567';
2020
+ const SHAPE_PROBE_FIELD_LIMIT = 4;
2021
+ function isAcceptedOutcomeMarker(action, stateId, guard) {
2022
+ if (!isPlainObject(action) || action.type !== ACCEPTED_OUTCOME_ACTION_TYPE) {
2023
+ return false;
2024
+ }
2025
+ const params = action.params;
2026
+ return (isPlainObject(params) &&
2027
+ params.source === stateId &&
2028
+ params.acceptedOutcome === guard);
2029
+ }
2030
+ function shapeCombinations(fieldCount) {
2031
+ const combinations = [[]];
2032
+ for (let field = 0; field < fieldCount; field += 1) {
2033
+ const next = [];
2034
+ for (const prefix of combinations) {
2035
+ for (let sample = 0; sample < SEMANTIC_SHAPE_SAMPLES.length; sample += 1) {
2036
+ next.push([...prefix, sample]);
2037
+ }
2038
+ }
2039
+ combinations.splice(0, combinations.length, ...next);
2040
+ }
2041
+ // Fewest non-string fields first, then the sample order, so a machine that
2042
+ // admits strings keeps them.
2043
+ return combinations.sort((left, right) => left.filter((index) => index !== 0).length -
2044
+ right.filter((index) => index !== 0).length ||
2045
+ left.join(',').localeCompare(right.join(',')));
2046
+ }
2047
+ /**
2048
+ * DR-089: the non-string JSON value shape the compiled machine admits for
2049
+ * each semantic-owned field of each governed outcome, keyed by state id and
2050
+ * then by outcome. Exported so linked machinery of an artifact's own reads
2051
+ * the same contract the shared factory enforces.
2052
+ */
2053
+ export function inferSemanticValueShapes(machine, authority) {
2054
+ const shapesByState = new Map();
2055
+ const playerNodes = new Map();
2056
+ const visit = (node) => {
2057
+ if (node.invoke.some(({ src }) => src === 'player')) {
2058
+ const playbookMeta = isPlainObject(node.config.meta)
2059
+ ? node.config.meta.playbook
2060
+ : undefined;
2061
+ const stateId = isPlainObject(playbookMeta) && typeof playbookMeta.stateId === 'string'
2062
+ ? playbookMeta.stateId
2063
+ : typeof node.config.id === 'string'
2064
+ ? node.config.id
2065
+ : node.key;
2066
+ if (!playerNodes.has(stateId))
2067
+ playerNodes.set(stateId, node);
2068
+ }
2069
+ for (const child of Object.values(node.states))
2070
+ visit(child);
2071
+ };
2072
+ try {
2073
+ visit(machine.root);
2074
+ }
2075
+ catch {
2076
+ return shapesByState;
2077
+ }
2078
+ const configContext = machine
2079
+ .config?.context;
2080
+ const context = isPlainObject(configContext) ? configContext : {};
2081
+ for (const [stateId, outcomes] of Object.entries(authority.governedPlayerStates)) {
2082
+ const node = playerNodes.get(stateId);
2083
+ const invokeId = node?.invoke.find(({ src }) => src === 'player')?.id;
2084
+ if (node === undefined || invokeId === undefined)
2085
+ continue;
2086
+ let snapshot;
2087
+ try {
2088
+ const value = [...node.path].reduceRight((inner, key) => (inner === undefined ? key : { [key]: inner }), undefined);
2089
+ snapshot = machine.resolveState({ value: value, context });
2090
+ }
2091
+ catch {
2092
+ continue;
2093
+ }
2094
+ const stateShapes = {};
2095
+ for (const [guard, outcome] of Object.entries(outcomes)) {
2096
+ const semanticFields = Object.entries(outcome.fields)
2097
+ .filter(([, fieldAuthority]) => fieldAuthority === 'semantic')
2098
+ .map(([field]) => field);
2099
+ if (semanticFields.length === 0 ||
2100
+ semanticFields.length > SHAPE_PROBE_FIELD_LIMIT) {
2101
+ continue;
2102
+ }
2103
+ const base = { guard };
2104
+ for (const [field, fieldAuthority] of Object.entries(outcome.fields)) {
2105
+ if (fieldAuthority === 'effect')
2106
+ base[field] = SHAPE_PROBE_COMMIT;
2107
+ else if (fieldAuthority !== 'semantic')
2108
+ base[field] = SHAPE_PROBE_TEXT;
2109
+ }
2110
+ const admits = (values) => {
2111
+ try {
2112
+ const transitions = machine.getTransitionData(snapshot, {
2113
+ type: `xstate.done.actor.${invokeId}`,
2114
+ output: { ...base, ...values },
2115
+ actorId: invokeId,
2116
+ });
2117
+ return transitions.some((transition) => transition.actions.some((action) => isAcceptedOutcomeMarker(action, stateId, guard)));
2118
+ }
2119
+ catch {
2120
+ return false;
2121
+ }
2122
+ };
2123
+ for (const combination of shapeCombinations(semanticFields.length)) {
2124
+ const values = {};
2125
+ semanticFields.forEach((field, index) => {
2126
+ values[field] = SEMANTIC_SHAPE_SAMPLES[combination[index]][1];
2127
+ });
2128
+ if (!admits(values))
2129
+ continue;
2130
+ const shapes = {};
2131
+ semanticFields.forEach((field, index) => {
2132
+ let shape = SEMANTIC_SHAPE_SAMPLES[combination[index]][0];
2133
+ if (shape === 'string-array' && admits({ ...values, [field]: [1] })) {
2134
+ shape = 'string-or-number-array';
2135
+ }
2136
+ if (shape !== 'string')
2137
+ shapes[field] = shape;
2138
+ });
2139
+ if (Object.keys(shapes).length > 0) {
2140
+ stateShapes[guard] = Object.freeze(shapes);
2141
+ }
2142
+ break;
2143
+ }
2144
+ }
2145
+ if (Object.keys(stateShapes).length > 0) {
2146
+ shapesByState.set(stateId, Object.freeze(stateShapes));
2147
+ }
2148
+ }
2149
+ return shapesByState;
2150
+ }
1937
2151
  function sameStringSet(left, right) {
1938
2152
  if (left.length !== right.length)
1939
2153
  return false;
@@ -2513,6 +2727,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
2513
2727
  verbatimPayloadFields,
2514
2728
  };
2515
2729
  const outcomeAuthority = snapshotOutcomeAuthority(specDescriptors.outcomeAuthority, label, roleStates, verbatimPayloadFields);
2730
+ const semanticValueShapes = inferSemanticValueShapes(machine, outcomeAuthority);
2516
2731
  for (const [stateId, outcomes] of Object.entries(outcomeAuthority.governedPlayerStates)) {
2517
2732
  if (Object.values(outcomes).some(({ repositoryDisposition }) => repositoryDisposition === 'deferred') &&
2518
2733
  !resumableStateIds.has(stateId)) {
@@ -2900,6 +3115,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
2900
3115
  semanticCandidate: candidate.semanticCandidate,
2901
3116
  finalText: candidate.finalText,
2902
3117
  receipt,
3118
+ semanticValueShapes: semanticValueShapes.get(candidate.sourceStateId),
2903
3119
  });
2904
3120
  if (awaitingLogicalReceipt &&
2905
3121
  reconciliation.status !== 'deferred') {
@@ -3745,7 +3961,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
3745
3961
  }
3746
3962
  let raw;
3747
3963
  try {
3748
- raw = await boundary.callJudge('player-output-adjudication', input.stateId, buildGovernedJudgePrompt(input, finalText, outcomes), signal);
3964
+ raw = await boundary.callJudge('player-output-adjudication', input.stateId, buildGovernedJudgePrompt(input, finalText, outcomes, undefined, semanticValueShapes.get(input.stateId)), signal);
3749
3965
  }
3750
3966
  catch (error) {
3751
3967
  governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, unresolvedGovernedSettlement('judge transport failed', error, signal));
@@ -3779,6 +3995,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
3779
3995
  semanticCandidate: candidate,
3780
3996
  finalText,
3781
3997
  receipt: completion.outcomeReceipt,
3998
+ semanticValueShapes: semanticValueShapes.get(input.stateId),
3782
3999
  });
3783
4000
  }
3784
4001
  catch (error) {
@@ -3811,7 +4028,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
3811
4028
  correctiveRaw = await boundary.callJudge('player-output-adjudication', input.stateId, buildGovernedJudgePrompt(input, finalText, outcomes, {
3812
4029
  reply: raw,
3813
4030
  error: structuralError.message,
3814
- }), signal);
4031
+ }, semanticValueShapes.get(input.stateId)), signal);
3815
4032
  }
3816
4033
  catch (error) {
3817
4034
  governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, unresolvedGovernedSettlement('corrective judge failed', error, signal));
@@ -3825,6 +4042,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
3825
4042
  semanticCandidate: candidate,
3826
4043
  finalText,
3827
4044
  receipt: completion.outcomeReceipt,
4045
+ semanticValueShapes: semanticValueShapes.get(input.stateId),
3828
4046
  });
3829
4047
  }
3830
4048
  catch (error) {
@@ -4202,12 +4420,13 @@ export function createXStatePlaybookRuntime(machine, spec) {
4202
4420
  const actorInput = input.snapshot.input;
4203
4421
  const outcomes = governedOutcomesForBoundary(saved);
4204
4422
  const candidate = saved.semanticCandidate ??
4205
- parseGovernedSemanticCandidate(await boundary.callJudge('player-output-adjudication', saved.sourceStateId, buildGovernedJudgePrompt(actorInput, saved.finalText, outcomes), signal));
4423
+ parseGovernedSemanticCandidate(await boundary.callJudge('player-output-adjudication', saved.sourceStateId, buildGovernedJudgePrompt(actorInput, saved.finalText, outcomes, undefined, semanticValueShapes.get(saved.sourceStateId)), signal));
4206
4424
  const result = reconcilePlaybookSemanticEvidence({
4207
4425
  outcomes,
4208
4426
  semanticCandidate: candidate,
4209
4427
  finalText: saved.finalText,
4210
4428
  receipt: saved.physicalReceipt,
4429
+ semanticValueShapes: semanticValueShapes.get(saved.sourceStateId),
4211
4430
  });
4212
4431
  signal.throwIfAborted();
4213
4432
  const next = {
@@ -5334,7 +5553,37 @@ export function createXStatePlaybookRuntime(machine, spec) {
5334
5553
  }
5335
5554
  const judgePrompt = defaultBuildCaptainJudgePrompt(input, finalText);
5336
5555
  const raw = await boundary.callJudge('captain-output-adjudication', input.stateId, judgePrompt, active);
5337
- const output = adjudicateCaptainOutput(extractFields, input, finalText, raw);
5556
+ let output;
5557
+ try {
5558
+ output = adjudicateCaptainOutput(extractFields, input, finalText, raw);
5559
+ }
5560
+ catch (error) {
5561
+ if (!(error instanceof CaptainAdjudicationStructureError)) {
5562
+ throw error;
5563
+ }
5564
+ // DR-089: one corrective adjudication over the same visible
5565
+ // output, restating the reply and its validation error; an
5566
+ // abort before it begins starts none.
5567
+ active.throwIfAborted();
5568
+ const correctiveRaw = await boundary.callJudge('captain-output-adjudication', input.stateId, defaultBuildCaptainJudgePrompt(input, finalText, {
5569
+ reply: raw,
5570
+ error: error.message,
5571
+ }), active);
5572
+ try {
5573
+ output = adjudicateCaptainOutput(extractFields, input, finalText, correctiveRaw);
5574
+ }
5575
+ catch (corrective) {
5576
+ if (!(corrective instanceof CaptainAdjudicationStructureError)) {
5577
+ throw corrective;
5578
+ }
5579
+ // A second invalid reply parks the step for Boss as a
5580
+ // decided adjudication failure, never a control-plane error.
5581
+ const failure = markFsmResultFailure(new Error(`${label} Captain outcome stayed undecided: ${corrective.message}`));
5582
+ const cause = governedSettlementCause('corrective semantic candidate is invalid', undefined, false, undefined);
5583
+ failureCauses.retain(failure, cause);
5584
+ throw attachPlaybookFailureCause(failure, cause);
5585
+ }
5586
+ }
5338
5587
  validateBossReplyOutput(input, output, resumableStateIds);
5339
5588
  return output;
5340
5589
  }