@sublang/playbook 15.1.0 → 16.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.
- package/docs/embedding.md +2 -2
- package/package.json +1 -1
- package/reference/sdlc/branch.playbook/branch.fsm.d.ts +63 -33
- package/reference/sdlc/branch.playbook/branch.fsm.js +190 -108
- package/reference/sdlc/branch.playbook/branch.fsm.ts +269 -147
- package/reference/sdlc/branch.playbook/branch.gears.md +7 -17
- package/reference/sdlc/branch.playbook/branch.playbook.d.ts +12 -16
- package/reference/sdlc/branch.playbook/branch.playbook.js +112 -101
- package/reference/sdlc/branch.playbook/branch.playbook.ts +128 -171
- package/reference/sdlc/branch.playbook/branch.registry.d.ts +3 -2
- package/reference/sdlc/branch.playbook/branch.registry.ts +6 -2
- package/reference/sdlc/code.playbook/code.fsm.d.ts +143 -90
- package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +8 -8
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +13 -13
- package/reference/sdlc/code.playbook/code.fsm.js +609 -649
- package/reference/sdlc/code.playbook/code.fsm.ts +890 -821
- package/reference/sdlc/code.playbook/code.gears.md +39 -29
- package/reference/sdlc/code.playbook/code.playbook.d.ts +13 -16
- package/reference/sdlc/code.playbook/code.playbook.js +166 -147
- package/reference/sdlc/code.playbook/code.playbook.ts +180 -229
- package/reference/sdlc/code.playbook/code.registry.d.ts +3 -2
- package/reference/sdlc/code.playbook/code.registry.ts +6 -2
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +42 -0
- package/reference/sdlc/code.playbook/playbook-captain.js +829 -194
- package/reference/sdlc/code.playbook/playbook-captain.ts +1227 -220
- package/reference/sdlc/decide.md +1 -0
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +270 -154
- package/reference/sdlc/decide.playbook/decide.fsm.js +765 -732
- package/reference/sdlc/decide.playbook/decide.fsm.ts +1167 -945
- package/reference/sdlc/decide.playbook/decide.gears.md +28 -21
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -59
- package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3696
- package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5244
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +8 -4
- package/reference/sdlc/decide.playbook/decide.registry.js +2 -2
- package/reference/sdlc/decide.playbook/decide.registry.ts +13 -6
- package/reference/sdlc/dev.playbook/dev.fsm.d.ts +113 -131
- package/reference/sdlc/dev.playbook/dev.fsm.js +517 -575
- package/reference/sdlc/dev.playbook/dev.fsm.ts +667 -787
- package/reference/sdlc/dev.playbook/dev.gears.md +52 -54
- package/reference/sdlc/dev.playbook/dev.playbook.d.ts +13 -10
- package/reference/sdlc/dev.playbook/dev.playbook.js +128 -120
- package/reference/sdlc/dev.playbook/dev.playbook.ts +143 -193
- package/reference/sdlc/dev.playbook/dev.registry.d.ts +3 -2
- package/reference/sdlc/dev.playbook/dev.registry.ts +6 -2
- package/reference/sdlc/pr.playbook/pr.fsm.d.ts +92 -85
- package/reference/sdlc/pr.playbook/pr.fsm.js +461 -518
- package/reference/sdlc/pr.playbook/pr.fsm.ts +533 -663
- package/reference/sdlc/pr.playbook/pr.gears.md +47 -63
- package/reference/sdlc/pr.playbook/pr.playbook.d.ts +13 -23
- package/reference/sdlc/pr.playbook/pr.playbook.js +118 -112
- package/reference/sdlc/pr.playbook/pr.playbook.ts +136 -190
- package/reference/sdlc/pr.playbook/pr.registry.d.ts +3 -2
- package/reference/sdlc/pr.playbook/pr.registry.ts +4 -2
- package/reference/sdlc/review.md +3 -3
- package/reference/sdlc/review.playbook/review.fsm.d.ts +150 -116
- package/reference/sdlc/review.playbook/review.fsm.js +479 -550
- package/reference/sdlc/review.playbook/review.fsm.ts +717 -662
- package/reference/sdlc/review.playbook/review.gears.md +32 -29
- package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -12
- package/reference/sdlc/review.playbook/review.playbook.js +180 -155
- package/reference/sdlc/review.playbook/review.playbook.ts +190 -230
- package/reference/sdlc/review.playbook/review.registry.d.ts +6 -5
- package/reference/sdlc/review.playbook/review.registry.js +3 -3
- package/reference/sdlc/review.playbook/review.registry.ts +9 -5
- package/slc/gears2fsm.md +72 -3
- package/slc/link.md +50 -27
- package/slc/materialize-link.mjs +51 -13
- package/slc/optimize.md +4 -1
- package/slc/prefix-prompts.mjs +29 -28
- package/slc/prefix.md +27 -24
- package/slc/slc.pin-inputs.json +11 -2
- package/slc/text2gears.md +71 -11
- package/src/xstate-playbook-runtime.d.ts +54 -15
- package/src/xstate-playbook-runtime.js +1154 -248
- package/src/xstate-playbook-runtime.ts +1603 -317
|
@@ -30,6 +30,8 @@ import type {
|
|
|
30
30
|
PlaybookFailureCause,
|
|
31
31
|
PlaybookFailureCode,
|
|
32
32
|
PlaybookPorts,
|
|
33
|
+
PlaybookRepositoryDisposition,
|
|
34
|
+
PlaybookRepositoryReceipt,
|
|
33
35
|
PlaybookRunResult,
|
|
34
36
|
PlaybookRuntime,
|
|
35
37
|
PlaybookRuntimeSnapshot,
|
|
@@ -44,9 +46,11 @@ import {
|
|
|
44
46
|
emptyPlaybookEffectLedger,
|
|
45
47
|
hiddenControlEnvelope,
|
|
46
48
|
isPlaybookEffectLedgerMonotonicExtension,
|
|
49
|
+
normalizeError,
|
|
47
50
|
registerPlaybookAbortCleanup,
|
|
48
51
|
snapshotJsonValue,
|
|
49
52
|
validatePlayerResult,
|
|
53
|
+
type PlaybookSemanticReconciliationReason,
|
|
50
54
|
} from '../../../src/xstate-runtime.js';
|
|
51
55
|
import createDefaultCaptainRuntime, {
|
|
52
56
|
type CaptainControllerPort,
|
|
@@ -669,10 +673,37 @@ interface ActiveTurn {
|
|
|
669
673
|
presentationAttempted: boolean;
|
|
670
674
|
/** The exact rejected presentation boundary, propagated without retry. */
|
|
671
675
|
presentationError?: unknown;
|
|
672
|
-
/**
|
|
673
|
-
|
|
676
|
+
/**
|
|
677
|
+
* Shell-authored safety text that must accompany the one visible reply, one
|
|
678
|
+
* report per entry in the order appended, each with the settlement facts
|
|
679
|
+
* that state the same thing (CAPTAIN-69).
|
|
680
|
+
*/
|
|
681
|
+
readonly presentationReports: PresentationReport[];
|
|
682
|
+
/** DR-063 §4: the parked failure's report, rendered last at presentation. */
|
|
683
|
+
failureReport?: FailureReport;
|
|
674
684
|
/** Facts accumulated while the selected action runs, including partial work. */
|
|
675
685
|
readonly settlementFacts: string[];
|
|
686
|
+
/**
|
|
687
|
+
* The facts that stated a frame's failure this turn, in the order stated, so
|
|
688
|
+
* the leaf's failure cause restates the one that stated its parked failure
|
|
689
|
+
* instead of stating that failure again (CAPTAIN-67).
|
|
690
|
+
*/
|
|
691
|
+
readonly failureStatements: FailureStatement[];
|
|
692
|
+
/**
|
|
693
|
+
* The reasons the renderer gave this turn's failure statements — a phrase,
|
|
694
|
+
* or a recorded reason in its code span — which the report assembly reads
|
|
695
|
+
* as what each statement holding one says (CAPTAIN-69).
|
|
696
|
+
*/
|
|
697
|
+
readonly statedReasons: Set<string>;
|
|
698
|
+
/**
|
|
699
|
+
* The shell's own words in this turn's statements that name something
|
|
700
|
+
* foreign — a failure's subject, an action's quoted label — which a
|
|
701
|
+
* statement says neutrally where CAPTAIN-9 refuses them (CAPTAIN-69).
|
|
702
|
+
*/
|
|
703
|
+
readonly shellWords: {
|
|
704
|
+
readonly subjects: Set<string>;
|
|
705
|
+
readonly labels: Set<string>;
|
|
706
|
+
};
|
|
676
707
|
report?: OutcomeReport;
|
|
677
708
|
/**
|
|
678
709
|
* A shell-owned control-plane failure — an unusable durable reply or a
|
|
@@ -1088,6 +1119,20 @@ function proseRejection(
|
|
|
1088
1119
|
return 'the reply leaked hidden control syntax or internal control vocabulary';
|
|
1089
1120
|
}
|
|
1090
1121
|
}
|
|
1122
|
+
return identifierRejection(prose, liveSessionIds, liveStateIds, suppliedIds);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* CAPTAIN-9's identifier duty alone: whether `prose` repeats a live session
|
|
1127
|
+
* identifier, a live internal state identifier, or an identifier the shell
|
|
1128
|
+
* supplied for selection only.
|
|
1129
|
+
*/
|
|
1130
|
+
function identifierRejection(
|
|
1131
|
+
prose: string,
|
|
1132
|
+
liveSessionIds: readonly string[],
|
|
1133
|
+
liveStateIds: readonly string[],
|
|
1134
|
+
suppliedIds: readonly string[],
|
|
1135
|
+
): string | undefined {
|
|
1091
1136
|
const caseFoldedProse = prose.toLowerCase();
|
|
1092
1137
|
for (const sessionId of liveSessionIds) {
|
|
1093
1138
|
// UUID hexadecimal is case-insensitive. A model uppercasing A-F has not
|
|
@@ -1109,6 +1154,35 @@ function proseRejection(
|
|
|
1109
1154
|
return undefined;
|
|
1110
1155
|
}
|
|
1111
1156
|
|
|
1157
|
+
/**
|
|
1158
|
+
* CAPTAIN-9: a prose call answered with the routing envelope of a `respond`
|
|
1159
|
+
* selection — exactly `{"action": "respond", "text": <string>}`, with nothing
|
|
1160
|
+
* but whitespace around it — said its prose in the decision shape, so the
|
|
1161
|
+
* prose is that `text`, which then passes the same validation as any reply.
|
|
1162
|
+
* Refusing the envelope instead spends the one corrective re-ask on a shape
|
|
1163
|
+
* whose meaning is unambiguous, and a model that answers the same way twice
|
|
1164
|
+
* costs the Boss the whole reply. Any other JSON stays control JSON.
|
|
1165
|
+
*/
|
|
1166
|
+
function respondEnvelopeText(reply: string | undefined): string | undefined {
|
|
1167
|
+
const trimmed = reply?.trim();
|
|
1168
|
+
if (trimmed === undefined || !trimmed.startsWith('{')) return reply;
|
|
1169
|
+
let parsed: unknown;
|
|
1170
|
+
try {
|
|
1171
|
+
parsed = JSON.parse(trimmed);
|
|
1172
|
+
} catch {
|
|
1173
|
+
return reply;
|
|
1174
|
+
}
|
|
1175
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
1176
|
+
return reply;
|
|
1177
|
+
}
|
|
1178
|
+
const members = parsed as Record<string, unknown>;
|
|
1179
|
+
return Object.keys(members).length === 2 &&
|
|
1180
|
+
members.action === 'respond' &&
|
|
1181
|
+
typeof members.text === 'string'
|
|
1182
|
+
? members.text
|
|
1183
|
+
: reply;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1112
1186
|
type CaptainToolIsolation = 'provider-enforced' | 'prompt-only';
|
|
1113
1187
|
|
|
1114
1188
|
// DR-013 A1: adapters with no provider-enforced tool-restriction surface.
|
|
@@ -1192,6 +1266,24 @@ type ControllerSettlementDraft = Omit<
|
|
|
1192
1266
|
'unresolvedEffects'
|
|
1193
1267
|
>;
|
|
1194
1268
|
|
|
1269
|
+
/**
|
|
1270
|
+
* Each unresolved-effect classification as what the step did (CAPTAIN-58), in
|
|
1271
|
+
* the words the matching failure cause uses, so the report assembly reads a
|
|
1272
|
+
* cause an entry already states as said (CAPTAIN-69).
|
|
1273
|
+
*/
|
|
1274
|
+
const UNRESOLVED_EFFECT_CLAUSES: Readonly<
|
|
1275
|
+
Record<PlaybookCaptainUnresolvedEffect['classification'], string>
|
|
1276
|
+
> = {
|
|
1277
|
+
'one-descendant-commit': 'the step made one new commit',
|
|
1278
|
+
'multiple-commits': 'the step made more than one commit',
|
|
1279
|
+
'rewritten-or-non-descendant': 'the repository history was rewritten',
|
|
1280
|
+
'worktree-only-change': 'the step left changes uncommitted',
|
|
1281
|
+
'concurrent-or-foreign-change': 'the repository changed outside this step',
|
|
1282
|
+
'observation-ambiguous':
|
|
1283
|
+
'the repository could not be observed after the step',
|
|
1284
|
+
incomplete: "the step's effect on the repository was not fully recorded",
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1195
1287
|
function unresolvedEffectReportLines(
|
|
1196
1288
|
unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[],
|
|
1197
1289
|
): readonly string[] {
|
|
@@ -1199,26 +1291,20 @@ function unresolvedEffectReportLines(
|
|
|
1199
1291
|
const merelyPossible =
|
|
1200
1292
|
effect.classification === 'observation-ambiguous' ||
|
|
1201
1293
|
effect.classification === 'incomplete';
|
|
1202
|
-
|
|
1203
|
-
`${index + 1}. ${merelyPossible ? 'Possible repository effect; a change could not be excluded' : 'Observed repository change'} (${effect.classification})`,
|
|
1204
|
-
`baseline HEAD ${effect.baselineHead}`,
|
|
1294
|
+
const heads =
|
|
1205
1295
|
effect.afterHead === undefined
|
|
1206
|
-
?
|
|
1207
|
-
:
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
].join('; ') + '.';
|
|
1296
|
+
? `the repository stood at ${effect.baselineHead} before it; no HEAD was available after it`
|
|
1297
|
+
: effect.afterHead === effect.baselineHead
|
|
1298
|
+
? `HEAD stayed at ${effect.baselineHead}`
|
|
1299
|
+
: `HEAD moved from ${effect.baselineHead} to ${effect.commitOid === undefined ? '' : 'the proven commit '}${effect.afterHead}`;
|
|
1300
|
+
return `${index + 1}. ${merelyPossible ? 'Possible repository effect, which could not be excluded' : 'Observed repository change'}: ${UNRESOLVED_EFFECT_CLAUSES[effect.classification]}; ${heads}.`;
|
|
1212
1301
|
});
|
|
1213
1302
|
}
|
|
1214
1303
|
|
|
1215
|
-
function unresolvedEffectBossReport(
|
|
1216
|
-
unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[],
|
|
1217
|
-
): string | undefined {
|
|
1218
|
-
if (unresolvedEffects.length === 0) return undefined;
|
|
1304
|
+
function unresolvedEffectBossReport(lines: readonly string[]): string {
|
|
1219
1305
|
return [
|
|
1220
1306
|
'Repository-effect evidence:',
|
|
1221
|
-
...
|
|
1307
|
+
...lines.map((line) => `- ${line}`),
|
|
1222
1308
|
'This evidence does not establish workflow completion or attribute any repository change or commit to this workflow.',
|
|
1223
1309
|
].join('\n');
|
|
1224
1310
|
}
|
|
@@ -1297,12 +1383,23 @@ function carriedPreExistingReport(
|
|
|
1297
1383
|
.join('\n\n');
|
|
1298
1384
|
}
|
|
1299
1385
|
|
|
1386
|
+
/**
|
|
1387
|
+
* The carried-changes report as settlement facts, one per commit, saying all
|
|
1388
|
+
* the report says so a reply that lists them need not repeat it.
|
|
1389
|
+
*/
|
|
1300
1390
|
function carriedPreExistingFacts(
|
|
1301
1391
|
carried: readonly CarriedPreExistingChanges[],
|
|
1302
1392
|
): readonly string[] {
|
|
1303
1393
|
return carried.map(({ commitOid, absorbed, altered }) => {
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1394
|
+
const lists = [
|
|
1395
|
+
...(absorbed.length === 0
|
|
1396
|
+
? []
|
|
1397
|
+
: [`absorbed as found ${boundedPathList(absorbed)}`]),
|
|
1398
|
+
...(altered.length === 0
|
|
1399
|
+
? []
|
|
1400
|
+
: [`altered before committing ${boundedPathList(altered)}`]),
|
|
1401
|
+
];
|
|
1402
|
+
return `The commit ${commitOid} carries changes that were uncommitted before the step: ${lists.join('; ')}.`;
|
|
1306
1403
|
});
|
|
1307
1404
|
}
|
|
1308
1405
|
|
|
@@ -1334,63 +1431,498 @@ function causePathList(
|
|
|
1334
1431
|
: printed.join(', ');
|
|
1335
1432
|
}
|
|
1336
1433
|
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1434
|
+
// CAPTAIN-71: one renderer puts every failure into words, for the Boss and for
|
|
1435
|
+
// the turn's settlement facts alike — a failure statement names what failed
|
|
1436
|
+
// and why — so no site composes a failure's words or quotes its message
|
|
1437
|
+
// itself. A cause says why in the Boss phrase of its code, and a reason the
|
|
1438
|
+
// runtime mints from a closed set in its own phrase (CAPTAIN-76); any other
|
|
1439
|
+
// recorded reason or message is quoted where it reads as prose and is
|
|
1440
|
+
// otherwise shown once, whole, as recorded, in a code span, never replaced by
|
|
1441
|
+
// a generic sentence that hides it (CAPTAIN-74).
|
|
1442
|
+
|
|
1443
|
+
const REPOSITORY_EVIDENCE_SENTENCE =
|
|
1444
|
+
"the repository evidence for the step is missing or inconsistent, so the step's outcome was not accepted.";
|
|
1445
|
+
const RESULT_EVIDENCE_SENTENCE =
|
|
1446
|
+
"the step's reported result is incomplete or inconsistent, so its outcome was not accepted.";
|
|
1447
|
+
const UNDECIDED_OUTCOME_SENTENCE = "the step's outcome could not be decided.";
|
|
1448
|
+
const CAPTAIN_INCOMPLETE_SENTENCE = 'the Captain call did not complete.';
|
|
1449
|
+
const PLAYER_ERROR_SENTENCE = 'the player reported an error without a message.';
|
|
1450
|
+
const PLAYER_ABORTED_SENTENCE = 'the player call was aborted.';
|
|
1451
|
+
|
|
1452
|
+
/**
|
|
1453
|
+
* CAPTAIN-76: every reason the runtime records from its own literal text, as
|
|
1454
|
+
* the Boss reads it. These are the runtime's own words for its own states — the
|
|
1455
|
+
* semantic-reconciliation reasons (PBRT-77), the governed settlement's
|
|
1456
|
+
* reasons, the judge's reasons, the player and Captain calls' results that
|
|
1457
|
+
* carried no message or no text, its own invariant reasons behind its
|
|
1458
|
+
* diagnostic label, and the stand-ins for a player result or an error that
|
|
1459
|
+
* carried no message — so no recorded text of one is ever shown as a code the
|
|
1460
|
+
* Boss has to decode. Anything not listed here is foreign text (CAPTAIN-74).
|
|
1461
|
+
*/
|
|
1462
|
+
const SEMANTIC_RECONCILIATION_PHRASES: Readonly<
|
|
1463
|
+
Record<PlaybookSemanticReconciliationReason, string>
|
|
1464
|
+
> = {
|
|
1465
|
+
'missing-repository-receipt': REPOSITORY_EVIDENCE_SENTENCE,
|
|
1466
|
+
'invalid-repository-receipt': REPOSITORY_EVIDENCE_SENTENCE,
|
|
1467
|
+
'repository-disposition-mismatch': REPOSITORY_EVIDENCE_SENTENCE,
|
|
1468
|
+
'missing-effect-evidence': REPOSITORY_EVIDENCE_SENTENCE,
|
|
1469
|
+
'missing-presentation-evidence': RESULT_EVIDENCE_SENTENCE,
|
|
1470
|
+
'missing-runtime-evidence': RESULT_EVIDENCE_SENTENCE,
|
|
1471
|
+
'inconsistent-runtime-evidence': RESULT_EVIDENCE_SENTENCE,
|
|
1472
|
+
};
|
|
1473
|
+
const RUNTIME_REASON_PHRASES: Readonly<Record<string, string>> = {
|
|
1474
|
+
...SEMANTIC_RECONCILIATION_PHRASES,
|
|
1475
|
+
'deferred player result has no semantic evidence': RESULT_EVIDENCE_SENTENCE,
|
|
1476
|
+
'host omitted governed semantic settlement':
|
|
1477
|
+
"the host did not settle the step's outcome.",
|
|
1478
|
+
'deferred question did not receive an eligible durable binding':
|
|
1479
|
+
"the step's question could not be kept for a later reply.",
|
|
1480
|
+
'judge transport failed': UNDECIDED_OUTCOME_SENTENCE,
|
|
1481
|
+
'corrective judge failed': UNDECIDED_OUTCOME_SENTENCE,
|
|
1482
|
+
'corrective semantic candidate is invalid': UNDECIDED_OUTCOME_SENTENCE,
|
|
1483
|
+
'semantic correction budget is unavailable': UNDECIDED_OUTCOME_SENTENCE,
|
|
1484
|
+
'callPlayer status "error"': PLAYER_ERROR_SENTENCE,
|
|
1485
|
+
'callPlayer status "aborted"': PLAYER_ABORTED_SENTENCE,
|
|
1486
|
+
'captainBridge: callPlayer status "error"': PLAYER_ERROR_SENTENCE,
|
|
1487
|
+
'captainBridge: callPlayer status "aborted"': PLAYER_ABORTED_SENTENCE,
|
|
1488
|
+
'captainBridge: callPlayer returned status=ok with no finalText':
|
|
1489
|
+
'the player call returned nothing.',
|
|
1490
|
+
'captainActor: callCaptain status "error"': CAPTAIN_INCOMPLETE_SENTENCE,
|
|
1491
|
+
'captainActor: callCaptain status "aborted"': CAPTAIN_INCOMPLETE_SENTENCE,
|
|
1492
|
+
'captainActor: callCaptain returned status=ok with no finalText':
|
|
1493
|
+
'the Captain call returned nothing.',
|
|
1494
|
+
'apply settled with outcome failed': 'the action did not complete.',
|
|
1495
|
+
'apply settled with outcome aborted': 'the action was stopped.',
|
|
1496
|
+
'unknown runtime defect': 'the runtime failed without saying why.',
|
|
1497
|
+
'Unknown error': 'an unknown error.',
|
|
1498
|
+
};
|
|
1499
|
+
|
|
1500
|
+
/**
|
|
1501
|
+
* CAPTAIN-76: the reasons the runtime mints behind its diagnostic label —
|
|
1502
|
+
* `playbook` unless a playbook names its own — as `<label> <suffix>`, the
|
|
1503
|
+
* parallel cohort's naming its state id as well and the unresolved governed
|
|
1504
|
+
* outcome its own reason; each matches only the whole reason.
|
|
1505
|
+
*/
|
|
1506
|
+
const LABELLED_RUNTIME_REASONS: readonly (readonly [RegExp, string])[] = [
|
|
1507
|
+
[
|
|
1508
|
+
/^.+ retained governed semantic envelope is no longer exact$/,
|
|
1509
|
+
"the step's retained result no longer matches.",
|
|
1510
|
+
],
|
|
1511
|
+
[/^.+ parallel state .+ cohort failed$/, 'a parallel step failed.'],
|
|
1512
|
+
[
|
|
1513
|
+
/^.+ governed semantic reconciliation remains unresolved$/,
|
|
1514
|
+
"the step's outcome could not be reconciled.",
|
|
1515
|
+
],
|
|
1516
|
+
[
|
|
1517
|
+
/^.+ reconstructed governed output changed before FSM acceptance$/,
|
|
1518
|
+
"the step's result changed before it was accepted.",
|
|
1519
|
+
],
|
|
1520
|
+
[
|
|
1521
|
+
/^.+ deferred continuation remains unresolved$/,
|
|
1522
|
+
'the deferred step is still unresolved.',
|
|
1523
|
+
],
|
|
1524
|
+
[
|
|
1525
|
+
/^.+ deferred player returned no result$/,
|
|
1526
|
+
'the deferred player returned nothing.',
|
|
1527
|
+
],
|
|
1528
|
+
[
|
|
1529
|
+
/^.+ governed outcome remains unresolved: .+$/,
|
|
1530
|
+
"the step's outcome could not be settled.",
|
|
1531
|
+
],
|
|
1532
|
+
[/^.+ actor entered error status$/, "the step's actor failed."],
|
|
1533
|
+
];
|
|
1534
|
+
|
|
1535
|
+
/**
|
|
1536
|
+
* The runtime's refusal of an action it does not advertise, which repeats the
|
|
1537
|
+
* selected id as JSON (PBRT-52) and so says nothing the Boss can use but that
|
|
1538
|
+
* the action is no longer offered (CAPTAIN-76).
|
|
1539
|
+
*/
|
|
1540
|
+
const NOT_ADVERTISED_REFUSAL =
|
|
1541
|
+
/^action "(?:[^"\\]|\\.)*" is not currently advertised$/;
|
|
1542
|
+
|
|
1543
|
+
/** The phrase of a reason the runtime mints from a closed set (CAPTAIN-76). */
|
|
1544
|
+
function runtimeReasonPhrase(reason: string): string | undefined {
|
|
1545
|
+
if (Object.hasOwn(RUNTIME_REASON_PHRASES, reason)) {
|
|
1546
|
+
return RUNTIME_REASON_PHRASES[reason];
|
|
1547
|
+
}
|
|
1548
|
+
if (NOT_ADVERTISED_REFUSAL.test(reason)) {
|
|
1549
|
+
return 'that action is no longer offered.';
|
|
1550
|
+
}
|
|
1551
|
+
return LABELLED_RUNTIME_REASONS.find(([minted]) => minted.test(reason))?.[1];
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/** A declared repository disposition, as what the step was to leave. */
|
|
1555
|
+
const REQUIRED_DISPOSITION_PHRASES: Readonly<
|
|
1556
|
+
Record<PlaybookRepositoryDisposition, string>
|
|
1557
|
+
> = {
|
|
1558
|
+
unchanged: 'no change',
|
|
1559
|
+
'one-descendant-commit': 'one new commit',
|
|
1560
|
+
deferred: 'a change deferred to a later step',
|
|
1561
|
+
};
|
|
1562
|
+
|
|
1563
|
+
/** A receipt classification, as what the step left. */
|
|
1564
|
+
const OBSERVED_CLASSIFICATION_PHRASES: Readonly<
|
|
1565
|
+
Record<PlaybookRepositoryReceipt['classification'], string>
|
|
1566
|
+
> = {
|
|
1567
|
+
unchanged: 'no change',
|
|
1568
|
+
'one-descendant-commit': 'one new commit',
|
|
1569
|
+
'multiple-commits': 'more than one new commit',
|
|
1570
|
+
'rewritten-or-non-descendant': 'rewritten history',
|
|
1571
|
+
'worktree-only-change': 'uncommitted changes only',
|
|
1572
|
+
'concurrent-or-foreign-change': 'a change made outside the step',
|
|
1573
|
+
'observation-ambiguous': 'a state that could not be read',
|
|
1574
|
+
};
|
|
1575
|
+
|
|
1576
|
+
/**
|
|
1577
|
+
* An error-class name leading a message (CAPTAIN-74): `Error`, `Exception`,
|
|
1578
|
+
* `Failure`, or `Defect`, alone or ending a CamelCase identifier, as in
|
|
1579
|
+
* `TypeError: …`, followed by `: `.
|
|
1580
|
+
*/
|
|
1581
|
+
const LEADING_ERROR_CLASS =
|
|
1582
|
+
/^(?:[A-Z][A-Za-z0-9]*)?(?:Error|Exception|Failure|Defect): /;
|
|
1583
|
+
|
|
1584
|
+
/**
|
|
1585
|
+
* CAPTAIN-74: prose begins with no error-class name — a message behind one is
|
|
1586
|
+
* machine text by origin, however what follows it reads — holds a space,
|
|
1587
|
+
* begins with a capital letter, ends in a terminal mark, and holds no word
|
|
1588
|
+
* CAPTAIN-9's identifier grammar tells apart from English — the same tokens
|
|
1589
|
+
* and the same test the reply validation applies, so no second notion of
|
|
1590
|
+
* "code-like" exists to disagree with it.
|
|
1591
|
+
*/
|
|
1592
|
+
function readsAsProse(text: string): boolean {
|
|
1593
|
+
return (
|
|
1594
|
+
!LEADING_ERROR_CLASS.test(text) &&
|
|
1595
|
+
text.includes(' ') &&
|
|
1596
|
+
/^\p{Lu}/u.test(text) &&
|
|
1597
|
+
/[.!?]$/.test(text) &&
|
|
1598
|
+
!(text.match(IDENTIFIER_TOKENS) ?? []).some(machineShapedIdentifier)
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
/**
|
|
1603
|
+
* A statement's own end (CAPTAIN-69): a run of terminal marks at its very end
|
|
1604
|
+
* collapses to its first, and a statement with none gains a period. A run
|
|
1605
|
+
* inside it — `main..HEAD`, `../config.yaml` — is evidence and stays.
|
|
1606
|
+
*/
|
|
1607
|
+
function oneTerminalMark(text: string): string {
|
|
1608
|
+
const statement = text.trim().replace(/([.!?])[.!?]+$/, '$1');
|
|
1609
|
+
return /[.!?]$/.test(statement) ? statement : `${statement}.`;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
/** `text` as one Markdown code span, unchanged whatever backticks it holds. */
|
|
1613
|
+
function codeSpan(text: string): string {
|
|
1614
|
+
const longest = Math.max(
|
|
1615
|
+
0,
|
|
1616
|
+
...[...text.matchAll(/`+/g)].map(([run]) => run.length),
|
|
1617
|
+
);
|
|
1618
|
+
const fence = '`'.repeat(longest + 1);
|
|
1619
|
+
const pad = text.startsWith('`') || text.endsWith('`') ? ' ' : '';
|
|
1620
|
+
return `${fence}${pad}${text}${pad}${fence}`;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
/** One Markdown code span of a text: where it stands, and what it shows. */
|
|
1624
|
+
interface CodeSpanAt {
|
|
1625
|
+
readonly start: number;
|
|
1626
|
+
readonly end: number;
|
|
1627
|
+
readonly content: string;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
/**
|
|
1631
|
+
* The code spans of `text` as CommonMark reads them: a backtick run opens one
|
|
1632
|
+
* that the next run of the same length closes, and one space padding both of
|
|
1633
|
+
* its ends is no part of what it shows.
|
|
1634
|
+
*/
|
|
1635
|
+
function codeSpans(text: string): readonly CodeSpanAt[] {
|
|
1636
|
+
const runs = [...text.matchAll(/`+/g)].map((match) => ({
|
|
1637
|
+
at: match.index ?? 0,
|
|
1638
|
+
length: match[0].length,
|
|
1639
|
+
}));
|
|
1640
|
+
const spans: CodeSpanAt[] = [];
|
|
1641
|
+
for (let index = 0; index < runs.length; index += 1) {
|
|
1642
|
+
const open = runs[index]!;
|
|
1643
|
+
const offset = runs
|
|
1644
|
+
.slice(index + 1)
|
|
1645
|
+
.findIndex(({ length }) => length === open.length);
|
|
1646
|
+
if (offset < 0) continue;
|
|
1647
|
+
const close = runs[index + 1 + offset]!;
|
|
1648
|
+
const inner = text.slice(open.at + open.length, close.at);
|
|
1649
|
+
spans.push({
|
|
1650
|
+
start: open.at,
|
|
1651
|
+
end: close.at + close.length,
|
|
1652
|
+
content:
|
|
1653
|
+
inner.length > 2 &&
|
|
1654
|
+
inner.startsWith(' ') &&
|
|
1655
|
+
inner.endsWith(' ') &&
|
|
1656
|
+
inner.trim().length > 0
|
|
1657
|
+
? inner.slice(1, -1)
|
|
1658
|
+
: inner,
|
|
1659
|
+
});
|
|
1660
|
+
index += 1 + offset;
|
|
1661
|
+
}
|
|
1662
|
+
return spans;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
/** `text` with `map` applied to what stands outside its code spans. */
|
|
1666
|
+
function outsideCodeSpans(
|
|
1667
|
+
text: string,
|
|
1668
|
+
map: (segment: string) => string,
|
|
1341
1669
|
): string {
|
|
1670
|
+
let mapped = '';
|
|
1671
|
+
let at = 0;
|
|
1672
|
+
for (const span of codeSpans(text)) {
|
|
1673
|
+
mapped += map(text.slice(at, span.start)) + text.slice(span.start, span.end);
|
|
1674
|
+
at = span.end;
|
|
1675
|
+
}
|
|
1676
|
+
return mapped + map(text.slice(at));
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
function sentenceCase(text: string): string {
|
|
1680
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
/** Why something failed, as a statement says it. */
|
|
1684
|
+
type FailureReason =
|
|
1685
|
+
/** Words the Boss reads as they are, ending in one terminal mark. */
|
|
1686
|
+
| { readonly kind: 'phrase'; readonly text: string }
|
|
1687
|
+
/** A recorded reason that reads as no prose, shown once as recorded. */
|
|
1688
|
+
| { readonly kind: 'code'; readonly text: string };
|
|
1689
|
+
|
|
1690
|
+
/**
|
|
1691
|
+
* Whether words may stand outside a code span in a statement the shell says
|
|
1692
|
+
* (CAPTAIN-9); the pure renderer accepts everything, and the shell passes its
|
|
1693
|
+
* guard.
|
|
1694
|
+
*/
|
|
1695
|
+
type Speakable = (text: string) => boolean;
|
|
1696
|
+
|
|
1697
|
+
const SPEAKABLE_ANYWHERE: Speakable = () => true;
|
|
1698
|
+
|
|
1699
|
+
/**
|
|
1700
|
+
* CAPTAIN-76/74: a recorded reason or message, its control characters spaced,
|
|
1701
|
+
* its ends trimmed, and its length bounded, is said by its phrase where the
|
|
1702
|
+
* runtime minted it from a closed set; otherwise it is quoted where it reads
|
|
1703
|
+
* as prose — and only where the shell may say it outside a code span — and is
|
|
1704
|
+
* otherwise kept whole as recorded; `undefined` where nothing is left of it.
|
|
1705
|
+
*/
|
|
1706
|
+
function recordedReason(
|
|
1707
|
+
text: string,
|
|
1708
|
+
speakable: Speakable = SPEAKABLE_ANYWHERE,
|
|
1709
|
+
): FailureReason | undefined {
|
|
1710
|
+
const reason = compactEvidence(text);
|
|
1711
|
+
if (reason.length === 0) return undefined;
|
|
1712
|
+
const minted = runtimeReasonPhrase(reason);
|
|
1713
|
+
if (minted !== undefined) return { kind: 'phrase', text: minted };
|
|
1714
|
+
return readsAsProse(reason) && speakable(reason)
|
|
1715
|
+
? { kind: 'phrase', text: oneTerminalMark(reason) }
|
|
1716
|
+
: { kind: 'code', text: reason };
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* `<subject> <outcome>: <reason>` for words the Boss reads as they are,
|
|
1721
|
+
* `<subject> <outcome> with the <noun> `<reason>`.` for a reason kept as
|
|
1722
|
+
* recorded, and `<subject> <outcome>.` where there is none.
|
|
1723
|
+
*/
|
|
1724
|
+
function reasonedClause(
|
|
1725
|
+
subject: string,
|
|
1726
|
+
outcome: string,
|
|
1727
|
+
noun: string,
|
|
1728
|
+
reason: FailureReason | undefined,
|
|
1729
|
+
): string {
|
|
1730
|
+
if (reason === undefined) return `${subject} ${outcome}.`;
|
|
1731
|
+
return reason.kind === 'phrase'
|
|
1732
|
+
? `${subject} ${outcome}: ${reason.text}`
|
|
1733
|
+
: `${subject} ${outcome} with the ${noun} ${codeSpan(reason.text)}.`;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
function failureClause(
|
|
1737
|
+
subject: string,
|
|
1738
|
+
reason: FailureReason | undefined,
|
|
1739
|
+
): string {
|
|
1740
|
+
return reasonedClause(subject, 'failed', 'error', reason);
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
/**
|
|
1744
|
+
* What the shell recorded of a failure: an error, which may carry the cause
|
|
1745
|
+
* the runtime decided (DR-063 §2), or that cause alone.
|
|
1746
|
+
*/
|
|
1747
|
+
interface RecordedFailure {
|
|
1748
|
+
readonly message?: string;
|
|
1749
|
+
readonly cause?: unknown;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
type DescribePlaybook = (playbookId: string) => string;
|
|
1753
|
+
|
|
1754
|
+
/** The Boss phrase of each cause code, with its bounded evidence (DR-063 §4). */
|
|
1755
|
+
function causeReason(
|
|
1756
|
+
cause: PlaybookFailureCause,
|
|
1757
|
+
describePlaybook: DescribePlaybook,
|
|
1758
|
+
speakable: Speakable = SPEAKABLE_ANYWHERE,
|
|
1759
|
+
): FailureReason | undefined {
|
|
1760
|
+
const phrase = (text: string): FailureReason => ({ kind: 'phrase', text });
|
|
1342
1761
|
const evidence = cause.evidence;
|
|
1343
1762
|
const paths = evidence.paths;
|
|
1344
1763
|
const code: PlaybookFailureCode = cause.code;
|
|
1345
1764
|
switch (code) {
|
|
1346
1765
|
case 'commit-missing':
|
|
1347
|
-
return (
|
|
1348
|
-
|
|
1349
|
-
|
|
1766
|
+
return phrase(
|
|
1767
|
+
(paths?.uncommitted ?? []).length === 0
|
|
1768
|
+
? 'the step had to commit its work and committed nothing.'
|
|
1769
|
+
: `the step had to commit its work and committed nothing; these changes are uncommitted: ${causePathList(paths?.uncommitted, paths?.truncated)}.`,
|
|
1770
|
+
);
|
|
1350
1771
|
case 'commit-residual': {
|
|
1351
1772
|
const subject =
|
|
1352
1773
|
evidence.commitOid === undefined
|
|
1353
1774
|
? "the step's commit"
|
|
1354
1775
|
: `the commit ${evidence.commitOid}`;
|
|
1355
1776
|
if ((paths?.uncommitted ?? []).length > 0) {
|
|
1356
|
-
return
|
|
1777
|
+
return phrase(
|
|
1778
|
+
`${subject} left changes uncommitted: ${causePathList(paths?.uncommitted, paths?.truncated)}.`,
|
|
1779
|
+
);
|
|
1357
1780
|
}
|
|
1358
1781
|
if ((paths?.altered ?? []).length > 0) {
|
|
1359
|
-
return
|
|
1782
|
+
return phrase(
|
|
1783
|
+
`${subject} left altered changes uncommitted: ${causePathList(paths?.altered, paths?.truncated)}.`,
|
|
1784
|
+
);
|
|
1360
1785
|
}
|
|
1361
|
-
return `${subject} left changes uncommitted beside it
|
|
1786
|
+
return phrase(`${subject} left changes uncommitted beside it.`);
|
|
1362
1787
|
}
|
|
1363
1788
|
case 'pre-existing-lost':
|
|
1364
|
-
return
|
|
1789
|
+
return phrase(
|
|
1790
|
+
`uncommitted changes you had were lost: ${causePathList(paths?.lost, paths?.truncated)}.`,
|
|
1791
|
+
);
|
|
1365
1792
|
case 'commits-more-than-one':
|
|
1366
|
-
return
|
|
1793
|
+
return phrase(
|
|
1794
|
+
`the step made more than one commit; HEAD moved from ${evidence.baselineHead} to ${evidence.afterHead}.`,
|
|
1795
|
+
);
|
|
1367
1796
|
case 'history-rewritten':
|
|
1368
|
-
return
|
|
1797
|
+
return phrase(
|
|
1798
|
+
`the repository history was rewritten; HEAD moved from ${evidence.baselineHead} to ${evidence.afterHead}.`,
|
|
1799
|
+
);
|
|
1369
1800
|
case 'foreign-change':
|
|
1370
|
-
return
|
|
1801
|
+
return phrase(
|
|
1802
|
+
`the repository changed outside this step: ${causePathList(paths?.changed, paths?.truncated)}.`,
|
|
1803
|
+
);
|
|
1371
1804
|
case 'observation-unstable':
|
|
1372
|
-
return
|
|
1373
|
-
|
|
1374
|
-
|
|
1805
|
+
return phrase(
|
|
1806
|
+
`the repository could not be observed after the step; the repository stood at ${evidence.baselineHead} before it.`,
|
|
1807
|
+
);
|
|
1808
|
+
case 'attribution-ambiguous': {
|
|
1809
|
+
const required =
|
|
1810
|
+
evidence.required === undefined
|
|
1811
|
+
? undefined
|
|
1812
|
+
: REQUIRED_DISPOSITION_PHRASES[evidence.required];
|
|
1813
|
+
const observed =
|
|
1814
|
+
evidence.observed === undefined
|
|
1815
|
+
? undefined
|
|
1816
|
+
: OBSERVED_CLASSIFICATION_PHRASES[evidence.observed];
|
|
1817
|
+
return phrase(
|
|
1818
|
+
required === undefined || observed === undefined
|
|
1819
|
+
? 'the repository change could not be attributed to this step.'
|
|
1820
|
+
: `the repository change could not be attributed to this step: it should have left ${required} but left ${observed}.`,
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1375
1823
|
case 'receipt-missing':
|
|
1376
|
-
return
|
|
1824
|
+
return phrase(
|
|
1825
|
+
`the step's effect on the repository was not fully recorded; the repository stood at ${evidence.baselineHead} before it.`,
|
|
1826
|
+
);
|
|
1377
1827
|
case 'judge-failed':
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
: `the hidden adjudication failed: ${evidence.reason} (${evidence.error.name}: ${compactEvidence(evidence.error.message)}).`;
|
|
1828
|
+
// The runtime's own closed reasons stay in the cause's data.
|
|
1829
|
+
return phrase(UNDECIDED_OUTCOME_SENTENCE);
|
|
1381
1830
|
case 'player-failed':
|
|
1382
|
-
|
|
1831
|
+
// The player's message says why, as any recorded message does: by the
|
|
1832
|
+
// runtime's phrase where it minted the message, else as prose or code.
|
|
1833
|
+
return phrase(
|
|
1834
|
+
failureClause(
|
|
1835
|
+
`the ${evidence.roleId} call`,
|
|
1836
|
+
recordedReason(evidence.error?.message ?? '', speakable),
|
|
1837
|
+
),
|
|
1838
|
+
);
|
|
1383
1839
|
case 'aborted':
|
|
1384
|
-
return 'the turn was aborted before the work settled.';
|
|
1385
|
-
case 'child-failed':
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1840
|
+
return phrase('the turn was aborted before the work settled.');
|
|
1841
|
+
case 'child-failed':
|
|
1842
|
+
return phrase(
|
|
1843
|
+
failureClause(
|
|
1844
|
+
`the nested ${describePlaybook(evidence.playbookId ?? '')} run`,
|
|
1845
|
+
evidence.cause === undefined
|
|
1846
|
+
? undefined
|
|
1847
|
+
: causeReason(evidence.cause, describePlaybook, speakable),
|
|
1848
|
+
),
|
|
1849
|
+
);
|
|
1391
1850
|
case 'runtime-defect':
|
|
1392
|
-
|
|
1851
|
+
// No more specific code: a reason the runtime minted is said by its
|
|
1852
|
+
// phrase, and any other reason as it was recorded.
|
|
1853
|
+
return recordedReason(evidence.reason ?? '', speakable);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
/**
|
|
1858
|
+
* Why a recorded failure failed: the phrase of the cause it carries where the
|
|
1859
|
+
* closed validator accepts one, and otherwise its recorded message.
|
|
1860
|
+
*/
|
|
1861
|
+
function failureReason(
|
|
1862
|
+
failure: RecordedFailure | undefined,
|
|
1863
|
+
describePlaybook: DescribePlaybook,
|
|
1864
|
+
speakable: Speakable = SPEAKABLE_ANYWHERE,
|
|
1865
|
+
): FailureReason | undefined {
|
|
1866
|
+
if (failure === undefined) return undefined;
|
|
1867
|
+
if (failure.cause !== undefined) {
|
|
1868
|
+
let cause: PlaybookFailureCause | undefined;
|
|
1869
|
+
try {
|
|
1870
|
+
cause = assertPlaybookFailureCause(failure.cause);
|
|
1871
|
+
} catch {
|
|
1872
|
+
// A cause the closed validator refuses is not a cause.
|
|
1873
|
+
}
|
|
1874
|
+
if (cause !== undefined) {
|
|
1875
|
+
return causeReason(cause, describePlaybook, speakable);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
return failure.message === undefined
|
|
1879
|
+
? undefined
|
|
1880
|
+
: recordedReason(failure.message, speakable);
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
/**
|
|
1884
|
+
* CAPTAIN-71: the one way a failure is put into words — `<Subject> failed:
|
|
1885
|
+
* <reason>.` — `subject` naming what failed in the Boss's words and `failure`
|
|
1886
|
+
* the error or cause the shell recorded.
|
|
1887
|
+
*/
|
|
1888
|
+
function failureStatement(
|
|
1889
|
+
subject: string,
|
|
1890
|
+
failure: RecordedFailure | undefined,
|
|
1891
|
+
describePlaybook: DescribePlaybook = (playbookId) => playbookId,
|
|
1892
|
+
speakable: Speakable = SPEAKABLE_ANYWHERE,
|
|
1893
|
+
): string {
|
|
1894
|
+
return sentenceCase(
|
|
1895
|
+
failureClause(subject, failureReason(failure, describePlaybook, speakable)),
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
/** The runtime's refusal of `action`, its reason said as a failure's is. */
|
|
1900
|
+
function refusalStatement(
|
|
1901
|
+
action: string,
|
|
1902
|
+
reason: FailureReason | undefined,
|
|
1903
|
+
): string {
|
|
1904
|
+
return sentenceCase(
|
|
1905
|
+
reasonedClause('the runtime', `refused ${action}`, 'reason', reason),
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
/**
|
|
1910
|
+
* The `Failure:` line of a parked failure (CAPTAIN-67): the reason's phrase,
|
|
1911
|
+
* or, for a reason kept as recorded, the step's failure statement; and the
|
|
1912
|
+
* reason it states, which a statement carrying the same failure holds.
|
|
1913
|
+
*/
|
|
1914
|
+
function failureLine(reason: FailureReason | undefined): {
|
|
1915
|
+
readonly sentence: string;
|
|
1916
|
+
readonly reason: string;
|
|
1917
|
+
} {
|
|
1918
|
+
if (reason?.kind === 'phrase') {
|
|
1919
|
+
return { sentence: reason.text, reason: reason.text };
|
|
1393
1920
|
}
|
|
1921
|
+
const sentence = failureClause('the step', reason);
|
|
1922
|
+
return {
|
|
1923
|
+
sentence,
|
|
1924
|
+
reason: reason === undefined ? sentence : codeSpan(reason.text),
|
|
1925
|
+
};
|
|
1394
1926
|
}
|
|
1395
1927
|
|
|
1396
1928
|
function failureControlLine(action: {
|
|
@@ -1405,26 +1937,297 @@ function failureControlLine(action: {
|
|
|
1405
1937
|
: `- ${action.label} (${standing}: ${controlReasonPhrase(action.reason)})`;
|
|
1406
1938
|
}
|
|
1407
1939
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1940
|
+
/** A report appended to the visible reply (CAPTAIN-69). */
|
|
1941
|
+
type PresentationReport =
|
|
1942
|
+
/**
|
|
1943
|
+
* A report the settlement facts also state — the resumption warning, the
|
|
1944
|
+
* carried-changes report — which a reply listing them need not repeat.
|
|
1945
|
+
*/
|
|
1946
|
+
| {
|
|
1947
|
+
readonly kind: 'stated';
|
|
1948
|
+
readonly text: string;
|
|
1949
|
+
readonly facts: readonly string[];
|
|
1950
|
+
}
|
|
1951
|
+
/** The unresolved-effect report, whose lines are statements of their own. */
|
|
1952
|
+
| { readonly kind: 'effects'; readonly lines: readonly string[] };
|
|
1953
|
+
|
|
1954
|
+
interface FailureReport {
|
|
1955
|
+
/** The `Failure:` line's sentence. */
|
|
1956
|
+
readonly sentence: string;
|
|
1957
|
+
/** The reason the line states, which weighs it (CAPTAIN-69). */
|
|
1958
|
+
readonly reason: string;
|
|
1959
|
+
readonly controls: readonly string[];
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
/** A fact that stated a frame's failure, and how it reads by the leaf's cause. */
|
|
1963
|
+
interface FailureStatement {
|
|
1964
|
+
readonly frame: EngagementFrame;
|
|
1965
|
+
readonly fact: string;
|
|
1966
|
+
/**
|
|
1967
|
+
* The error message the fact stated, which the leaf's recorded error must
|
|
1968
|
+
* carry for the fact to have stated the parked failure; absent where the
|
|
1969
|
+
* fact is that failure by construction — the leaf's failed run result.
|
|
1970
|
+
*/
|
|
1971
|
+
readonly message?: string;
|
|
1972
|
+
/** The same fact with `failure` as its reason (CAPTAIN-71). */
|
|
1973
|
+
readonly restate: (failure: RecordedFailure) => string;
|
|
1974
|
+
/**
|
|
1975
|
+
* The fact as the Boss reads it, where that differs from the prompt's: a
|
|
1976
|
+
* runtime action named by its label rather than its id (PBRT-52).
|
|
1977
|
+
*/
|
|
1978
|
+
readonly speak?: (fact: string) => string;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
/** A reply the shell composes itself, with the facts it lists. */
|
|
1982
|
+
interface ShellReply {
|
|
1983
|
+
readonly text: string;
|
|
1984
|
+
/** The settlement facts the reply was assembled from (CAPTAIN-69). */
|
|
1985
|
+
readonly stated?: readonly string[];
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
/**
|
|
1989
|
+
* Whether `container` already states `statement`: the same words, case and
|
|
1990
|
+
* terminal mark aside, standing as a phrase of their own rather than inside a
|
|
1991
|
+
* longer word or command.
|
|
1992
|
+
*/
|
|
1993
|
+
function alreadyStates(container: string, statement: string): boolean {
|
|
1994
|
+
const haystack = container.toLowerCase();
|
|
1995
|
+
const needle = statement.toLowerCase().replace(/[.!?]+$/, '');
|
|
1996
|
+
if (needle.length === 0) return true;
|
|
1997
|
+
for (
|
|
1998
|
+
let at = haystack.indexOf(needle);
|
|
1999
|
+
at >= 0;
|
|
2000
|
+
at = haystack.indexOf(needle, at + 1)
|
|
2001
|
+
) {
|
|
2002
|
+
const before = haystack.charAt(at - 1);
|
|
2003
|
+
const after = haystack.charAt(at + needle.length);
|
|
2004
|
+
if (!/[\p{L}\p{N}_/-]/u.test(before) && !/[\p{L}\p{N}_-]/u.test(after)) {
|
|
2005
|
+
return true;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
return false;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
/**
|
|
2012
|
+
* The three kinds of statement a report the shell says holds (CAPTAIN-69):
|
|
2013
|
+
* a listed fact, a numbered unresolved-effect entry, and the `Failure:` line.
|
|
2014
|
+
*/
|
|
2015
|
+
type StatementKind = 'fact' | 'entry' | 'failure';
|
|
2016
|
+
|
|
2017
|
+
/** A statement the report assembly weighs. */
|
|
2018
|
+
interface BossStatement {
|
|
2019
|
+
readonly text: string;
|
|
2020
|
+
/** Its kind, which decides what may drop it; a fact where absent. */
|
|
2021
|
+
readonly kind?: StatementKind;
|
|
2022
|
+
/** The reason a `Failure:` line states (CAPTAIN-67). */
|
|
2023
|
+
readonly reason?: string;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
/** The words a reason CAPTAIN-9 withholds is said as. */
|
|
2027
|
+
const WITHHELD_REASON = 'an internal error';
|
|
2028
|
+
|
|
2029
|
+
/** `text` with each of its code spans read as a space. */
|
|
2030
|
+
function wordsOutsideCodeSpans(text: string): string {
|
|
2031
|
+
let words = '';
|
|
2032
|
+
let at = 0;
|
|
2033
|
+
for (const span of codeSpans(text)) {
|
|
2034
|
+
words += `${text.slice(at, span.start)} `;
|
|
2035
|
+
at = span.end;
|
|
2036
|
+
}
|
|
2037
|
+
return words + text.slice(at);
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
/** The shell's words for something foreign in a statement (CAPTAIN-69). */
|
|
2041
|
+
interface ShellWords {
|
|
2042
|
+
/** Failure subjects, as a statement standing alone begins with one. */
|
|
2043
|
+
readonly subjects: Iterable<string>;
|
|
2044
|
+
/** Runtime actions' labels, quoted as a statement names one. */
|
|
2045
|
+
readonly labels: Iterable<string>;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
const NO_SHELL_WORDS: ShellWords = { subjects: [], labels: [] };
|
|
2049
|
+
|
|
2050
|
+
/**
|
|
2051
|
+
* CAPTAIN-69's guard on one statement the shell says, so no statement is
|
|
2052
|
+
* lost for its own or another's words: a code span whose content repeats an
|
|
2053
|
+
* identifier CAPTAIN-9 withholds reads, with the noun before it, `an internal
|
|
2054
|
+
* error`; a reason the renderer phrased whose own words fail CAPTAIN-9 reads
|
|
2055
|
+
* so too; a failure subject beginning the statement whose own words fail
|
|
2056
|
+
* reads `The step`; and a quoted action label whose own words fail reads `the
|
|
2057
|
+
* selected action`. Whatever is left, the statement is never left out.
|
|
2058
|
+
*/
|
|
2059
|
+
function guardedStatement(
|
|
2060
|
+
text: string,
|
|
2061
|
+
withholds: (content: string) => boolean,
|
|
2062
|
+
refuses: (words: string) => boolean,
|
|
2063
|
+
phrased: Iterable<string> = [],
|
|
2064
|
+
shellWords: ShellWords = NO_SHELL_WORDS,
|
|
1411
2065
|
): string {
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
2066
|
+
let guarded = '';
|
|
2067
|
+
let at = 0;
|
|
2068
|
+
for (const span of codeSpans(text)) {
|
|
2069
|
+
const before = text.slice(at, span.start);
|
|
2070
|
+
guarded += withholds(span.content)
|
|
2071
|
+
? `${before.replace(/the (?:error|reason) $/, '')}${WITHHELD_REASON}`
|
|
2072
|
+
: before + text.slice(span.start, span.end);
|
|
2073
|
+
at = span.end;
|
|
2074
|
+
}
|
|
2075
|
+
guarded += text.slice(at);
|
|
2076
|
+
const fails = (statement: string): boolean => {
|
|
2077
|
+
const words = wordsOutsideCodeSpans(statement);
|
|
2078
|
+
return words.trim().length > 0 && refuses(words);
|
|
2079
|
+
};
|
|
2080
|
+
for (const reason of phrased) {
|
|
2081
|
+
if (!fails(reason)) continue;
|
|
2082
|
+
guarded =
|
|
2083
|
+
guarded === `Failure: ${reason}`
|
|
2084
|
+
? `Failure: the step failed with ${WITHHELD_REASON}.`
|
|
2085
|
+
: guarded.split(`: ${reason}`).join(` with ${WITHHELD_REASON}.`);
|
|
2086
|
+
}
|
|
2087
|
+
for (const subject of shellWords.subjects) {
|
|
2088
|
+
if (guarded.startsWith(`${subject} `) && fails(subject)) {
|
|
2089
|
+
guarded = `The step${guarded.slice(subject.length)}`;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
for (const label of shellWords.labels) {
|
|
2093
|
+
if (fails(label)) {
|
|
2094
|
+
guarded = guarded.split(label).join('the selected action');
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
return guarded;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
/**
|
|
2101
|
+
* CAPTAIN-69: the one way the shell assembles Boss-facing text of its own.
|
|
2102
|
+
* Each statement ends in one terminal mark and passes `guard`. The kinds
|
|
2103
|
+
* decide what is dropped: an unresolved-effect entry records a boundary of
|
|
2104
|
+
* its own and always stays, while a fact or the `Failure:` line is dropped
|
|
2105
|
+
* where an entry, or a fact before it that stays, carries its reason — for
|
|
2106
|
+
* the `Failure:` line the reason it states, for a fact every reason the
|
|
2107
|
+
* renderer gave that turn (`reasons`) that it holds, else the fact itself.
|
|
2108
|
+
* The result is aligned with `statements`, a dropped statement `undefined`.
|
|
2109
|
+
*/
|
|
2110
|
+
function assembleStatements(
|
|
2111
|
+
statements: readonly BossStatement[],
|
|
2112
|
+
reasons: Iterable<string> = [],
|
|
2113
|
+
guard: (text: string) => string = (text) => text,
|
|
2114
|
+
): readonly (string | undefined)[] {
|
|
2115
|
+
const rendered = [...new Set(reasons)];
|
|
2116
|
+
const weighed = statements.map(({ text, kind = 'fact', reason }) => {
|
|
2117
|
+
const statement = oneTerminalMark(text);
|
|
2118
|
+
if (reason !== undefined) return { text: statement, kind, own: [reason] };
|
|
2119
|
+
const held = rendered.filter((candidate) =>
|
|
2120
|
+
alreadyStates(statement, candidate),
|
|
2121
|
+
);
|
|
2122
|
+
return { text: statement, kind, own: held.length > 0 ? held : [statement] };
|
|
2123
|
+
});
|
|
2124
|
+
const entries = weighed
|
|
2125
|
+
.filter(({ kind }) => kind === 'entry')
|
|
2126
|
+
.map(({ text }) => text);
|
|
2127
|
+
const facts: string[] = [];
|
|
2128
|
+
const carried = (own: readonly string[], containers: readonly string[]) =>
|
|
2129
|
+
containers.some((container) =>
|
|
2130
|
+
own.every((reason) => alreadyStates(container, reason)),
|
|
2131
|
+
);
|
|
2132
|
+
const stays = weighed.map(({ text, kind, own }) => {
|
|
2133
|
+
if (kind !== 'fact') return true;
|
|
2134
|
+
if (carried(own, entries) || carried(own, facts)) return false;
|
|
2135
|
+
facts.push(text);
|
|
2136
|
+
return true;
|
|
2137
|
+
});
|
|
2138
|
+
return weighed.map(({ text, kind, own }, index) =>
|
|
2139
|
+
stays[index] &&
|
|
2140
|
+
(kind !== 'failure' || !carried(own, [...entries, ...facts]))
|
|
2141
|
+
? oneTerminalMark(guard(text))
|
|
2142
|
+
: undefined,
|
|
2143
|
+
);
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
/** The statements of `statements` that stay, as CAPTAIN-69 assembles them. */
|
|
2147
|
+
function bossReport(
|
|
2148
|
+
statements: readonly (string | BossStatement)[],
|
|
2149
|
+
reasons: Iterable<string> = [],
|
|
2150
|
+
): string[] {
|
|
2151
|
+
return assembleStatements(
|
|
2152
|
+
statements.map((statement) =>
|
|
2153
|
+
typeof statement === 'string' ? { text: statement } : statement,
|
|
2154
|
+
),
|
|
2155
|
+
reasons,
|
|
2156
|
+
).filter((statement): statement is string => statement !== undefined);
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
/** A turn's shell-said statements, as they stay (CAPTAIN-69). */
|
|
2160
|
+
interface AssembledReport {
|
|
2161
|
+
/** The listed facts that stay. */
|
|
2162
|
+
readonly facts: readonly string[];
|
|
2163
|
+
/** The unresolved-effect entries that stay. */
|
|
2164
|
+
readonly effects: readonly string[];
|
|
2165
|
+
/** The `Failure:` line, where it stays. */
|
|
2166
|
+
readonly failure?: string;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
/**
|
|
2170
|
+
* The reports that accompany a reply, from the turn's assembled statements
|
|
2171
|
+
* (CAPTAIN-69): a report whose facts the reply lists is not said again, the
|
|
2172
|
+
* unresolved-effect report carries every entry as guarded, and the failure
|
|
2173
|
+
* report's `Controls:` list always stays.
|
|
2174
|
+
*/
|
|
2175
|
+
function presentationSuffix(
|
|
2176
|
+
turn: ActiveTurn,
|
|
2177
|
+
assembled: AssembledReport,
|
|
2178
|
+
): string | undefined {
|
|
2179
|
+
const parts = turn.presentationReports.flatMap((report) => {
|
|
2180
|
+
if (report.kind === 'stated') {
|
|
2181
|
+
return report.facts.length > 0 &&
|
|
2182
|
+
report.facts.every((fact) =>
|
|
2183
|
+
assembled.facts.some((listed) => alreadyStates(listed, fact)),
|
|
2184
|
+
)
|
|
2185
|
+
? []
|
|
2186
|
+
: [report.text];
|
|
2187
|
+
}
|
|
2188
|
+
return assembled.effects.length === 0
|
|
2189
|
+
? []
|
|
2190
|
+
: [unresolvedEffectBossReport(assembled.effects)];
|
|
2191
|
+
});
|
|
2192
|
+
const failure = turn.failureReport;
|
|
2193
|
+
if (failure !== undefined) {
|
|
2194
|
+
parts.push(
|
|
2195
|
+
[
|
|
2196
|
+
...(assembled.failure === undefined ? [] : [assembled.failure]),
|
|
2197
|
+
'Controls:',
|
|
2198
|
+
...(failure.controls.length === 0 ? ['- none'] : failure.controls),
|
|
2199
|
+
].join('\n'),
|
|
2200
|
+
);
|
|
2201
|
+
}
|
|
2202
|
+
return parts.length === 0 ? undefined : parts.join('\n\n');
|
|
1417
2203
|
}
|
|
1418
2204
|
|
|
1419
2205
|
function appendMandatoryPresentationSuffix(
|
|
1420
2206
|
turn: ActiveTurn,
|
|
1421
2207
|
suffix: string,
|
|
2208
|
+
facts: readonly string[] = [],
|
|
1422
2209
|
): void {
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
2210
|
+
if (
|
|
2211
|
+
!turn.presentationReports.some(
|
|
2212
|
+
(report) => report.kind === 'stated' && report.text.includes(suffix),
|
|
2213
|
+
)
|
|
2214
|
+
) {
|
|
2215
|
+
turn.presentationReports.push({ kind: 'stated', text: suffix, facts });
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
function appendUnresolvedEffectReport(
|
|
2220
|
+
turn: ActiveTurn,
|
|
2221
|
+
unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[],
|
|
2222
|
+
): void {
|
|
2223
|
+
if (
|
|
2224
|
+
unresolvedEffects.length > 0 &&
|
|
2225
|
+
!turn.presentationReports.some((report) => report.kind === 'effects')
|
|
2226
|
+
) {
|
|
2227
|
+
turn.presentationReports.push({
|
|
2228
|
+
kind: 'effects',
|
|
2229
|
+
lines: unresolvedEffectReportLines(unresolvedEffects),
|
|
2230
|
+
});
|
|
1428
2231
|
}
|
|
1429
2232
|
}
|
|
1430
2233
|
|
|
@@ -3781,23 +4584,60 @@ export function createPlaybookCaptainShell(
|
|
|
3781
4584
|
const report = carriedPreExistingReport(carried);
|
|
3782
4585
|
if (report === undefined) return;
|
|
3783
4586
|
turn.carriedPreExistingReported = true;
|
|
3784
|
-
appendMandatoryPresentationSuffix(turn, report);
|
|
3785
4587
|
const facts = carriedPreExistingFacts(carried);
|
|
3786
|
-
turn
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
// join it here rather than only the turn's running list.
|
|
3790
|
-
if (turn.report !== undefined) {
|
|
3791
|
-
turn.report = {
|
|
3792
|
-
...turn.report,
|
|
3793
|
-
facts: [...turn.report.facts, ...facts],
|
|
3794
|
-
...(turn.report.bossFacts === undefined
|
|
3795
|
-
? {}
|
|
3796
|
-
: { bossFacts: [...turn.report.bossFacts, ...facts] }),
|
|
3797
|
-
};
|
|
4588
|
+
appendMandatoryPresentationSuffix(turn, report, facts);
|
|
4589
|
+
for (const fact of facts) {
|
|
4590
|
+
joinSettlementFact(turn, fact);
|
|
3798
4591
|
}
|
|
3799
4592
|
};
|
|
3800
4593
|
|
|
4594
|
+
/**
|
|
4595
|
+
* CAPTAIN-67: one fact joins the turn once. The action's own report was
|
|
4596
|
+
* assembled from the facts as they stood when it settled, and the
|
|
4597
|
+
* result-phase prompt reads that report, so a later fact joins it as well as
|
|
4598
|
+
* the turn's running list — which that report's `facts` often *is*, so
|
|
4599
|
+
* appending to both stated the fact twice. A fact that restates one already
|
|
4600
|
+
* listed for the same failure takes that fact's place.
|
|
4601
|
+
*/
|
|
4602
|
+
const joinSettlementFact = (
|
|
4603
|
+
turn: ActiveTurn,
|
|
4604
|
+
fact: string,
|
|
4605
|
+
replacing?: string,
|
|
4606
|
+
speak: (fact: string) => string = (text) => text,
|
|
4607
|
+
): void => {
|
|
4608
|
+
const placed = (
|
|
4609
|
+
list: readonly string[],
|
|
4610
|
+
entry: string,
|
|
4611
|
+
replaced: string | undefined,
|
|
4612
|
+
): readonly string[] => {
|
|
4613
|
+
if (list.includes(entry)) return list;
|
|
4614
|
+
const at = replaced === undefined ? -1 : list.indexOf(replaced);
|
|
4615
|
+
return at < 0
|
|
4616
|
+
? [...list, entry]
|
|
4617
|
+
: list.map((listed, index) => (index === at ? entry : listed));
|
|
4618
|
+
};
|
|
4619
|
+
const running = placed(turn.settlementFacts, fact, replacing);
|
|
4620
|
+
if (running !== turn.settlementFacts) {
|
|
4621
|
+
turn.settlementFacts.splice(0, turn.settlementFacts.length, ...running);
|
|
4622
|
+
}
|
|
4623
|
+
const report = turn.report;
|
|
4624
|
+
if (report === undefined) return;
|
|
4625
|
+
// The Boss-facing rendering holds the same fact as the Boss reads it.
|
|
4626
|
+
turn.report = {
|
|
4627
|
+
...report,
|
|
4628
|
+
facts: placed(report.facts, fact, replacing),
|
|
4629
|
+
...(report.bossFacts === undefined
|
|
4630
|
+
? {}
|
|
4631
|
+
: {
|
|
4632
|
+
bossFacts: placed(
|
|
4633
|
+
report.bossFacts,
|
|
4634
|
+
speak(fact),
|
|
4635
|
+
replacing === undefined ? undefined : speak(replacing),
|
|
4636
|
+
),
|
|
4637
|
+
}),
|
|
4638
|
+
};
|
|
4639
|
+
};
|
|
4640
|
+
|
|
3801
4641
|
const normalizeInstalledRetainedGenerations = (
|
|
3802
4642
|
value: Readonly<Record<string, PlaybookCaptainRetainedGeneration>>,
|
|
3803
4643
|
): Map<string, PlaybookCaptainRetainedGeneration> => {
|
|
@@ -4450,6 +5290,19 @@ export function createPlaybookCaptainShell(
|
|
|
4450
5290
|
return { name: 'Error', message: String(value) };
|
|
4451
5291
|
};
|
|
4452
5292
|
|
|
5293
|
+
/**
|
|
5294
|
+
* CAPTAIN-71: what the shell recorded of a thrown value — its message, and
|
|
5295
|
+
* the cause the runtime decided where the value carries one (DR-063 §2).
|
|
5296
|
+
*/
|
|
5297
|
+
const failureOf = (error: unknown): RecordedFailure => {
|
|
5298
|
+
const normalized = normalizeErrorCompact(error) ?? {
|
|
5299
|
+
name: 'Error',
|
|
5300
|
+
message: String(error),
|
|
5301
|
+
};
|
|
5302
|
+
const { cause } = normalizeError(error);
|
|
5303
|
+
return cause === undefined ? normalized : { ...normalized, cause };
|
|
5304
|
+
};
|
|
5305
|
+
|
|
4453
5306
|
const payloadRecord = (
|
|
4454
5307
|
payload: unknown,
|
|
4455
5308
|
): Record<string, unknown> | undefined =>
|
|
@@ -5652,12 +6505,8 @@ export function createPlaybookCaptainShell(
|
|
|
5652
6505
|
visibilityControlError = error;
|
|
5653
6506
|
} else {
|
|
5654
6507
|
if (runFailureFacts) {
|
|
5655
|
-
const normalized = normalizeErrorCompact(error) ?? {
|
|
5656
|
-
name: 'Error',
|
|
5657
|
-
message: String(error),
|
|
5658
|
-
};
|
|
5659
6508
|
runFailureFacts.push(
|
|
5660
|
-
`
|
|
6509
|
+
stateFailure(`cleanup after ${frameLabel(child)}`, failureOf(error)),
|
|
5661
6510
|
);
|
|
5662
6511
|
}
|
|
5663
6512
|
effectiveResult = {
|
|
@@ -5760,12 +6609,14 @@ export function createPlaybookCaptainShell(
|
|
|
5760
6609
|
);
|
|
5761
6610
|
}
|
|
5762
6611
|
if (result.outcome === 'failed' && runFailureFacts) {
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
6612
|
+
const subject = frameLabel(frame);
|
|
6613
|
+
const fact = stateFailure(subject, result.error);
|
|
6614
|
+
runFailureFacts.push(fact);
|
|
6615
|
+
activeTurn?.failureStatements.push({
|
|
6616
|
+
frame,
|
|
6617
|
+
fact,
|
|
6618
|
+
restate: (failure) => stateFailure(subject, failure),
|
|
6619
|
+
});
|
|
5769
6620
|
}
|
|
5770
6621
|
if (leafFrame()) {
|
|
5771
6622
|
await setMode('engaged.parked', `turn:${result.outcome}`);
|
|
@@ -6445,9 +7296,12 @@ export function createPlaybookCaptainShell(
|
|
|
6445
7296
|
* followed by another attempt: the Promise cannot prove whether rendering
|
|
6446
7297
|
* began, so retrying could duplicate a reply the Boss already saw.
|
|
6447
7298
|
*/
|
|
6448
|
-
const surfaceSettlement = async (
|
|
6449
|
-
|
|
6450
|
-
|
|
7299
|
+
const surfaceSettlement = async (settlement: {
|
|
7300
|
+
context: CaptainContext;
|
|
7301
|
+
text: string;
|
|
7302
|
+
/** The facts a shell-composed reply lists, which its reports need not repeat. */
|
|
7303
|
+
stated?: readonly string[];
|
|
7304
|
+
}): Promise<void> => {
|
|
6451
7305
|
const turn = activeTurn;
|
|
6452
7306
|
if (turn?.presentationAttempted) {
|
|
6453
7307
|
const error = new Error(
|
|
@@ -6461,7 +7315,13 @@ export function createPlaybookCaptainShell(
|
|
|
6461
7315
|
// all of this prose. Preserve the exact attempt before crossing the
|
|
6462
7316
|
// boundary; the uncertainty record below keeps recovery from pretending
|
|
6463
7317
|
// delivery was confirmed while still understanding a Boss follow-up.
|
|
6464
|
-
const suffix =
|
|
7318
|
+
const suffix =
|
|
7319
|
+
turn === undefined
|
|
7320
|
+
? undefined
|
|
7321
|
+
: presentationSuffix(
|
|
7322
|
+
turn,
|
|
7323
|
+
assembleTurnReport(turn, settlement.stated ?? []),
|
|
7324
|
+
);
|
|
6465
7325
|
const visibleText =
|
|
6466
7326
|
suffix === undefined || settlement.text.includes(suffix)
|
|
6467
7327
|
? settlement.text
|
|
@@ -6561,6 +7421,85 @@ export function createPlaybookCaptainShell(
|
|
|
6561
7421
|
suppliedIdentifiers(),
|
|
6562
7422
|
);
|
|
6563
7423
|
|
|
7424
|
+
// CAPTAIN-74: whether a recorded reason may be quoted outside a code span —
|
|
7425
|
+
// what the renderer asks before it quotes one as prose.
|
|
7426
|
+
const speakableWords = (words: string): boolean =>
|
|
7427
|
+
replyRejection(words) === undefined;
|
|
7428
|
+
|
|
7429
|
+
// CAPTAIN-69: the guard every statement the shell says passes on its own —
|
|
7430
|
+
// CAPTAIN-9 outside its code spans, and inside one only its identifiers —
|
|
7431
|
+
// with the reasons the renderer gave that turn and the shell words it may
|
|
7432
|
+
// say neutrally.
|
|
7433
|
+
const statementGuard = (text: string, turn: ActiveTurn): string =>
|
|
7434
|
+
guardedStatement(
|
|
7435
|
+
text,
|
|
7436
|
+
(content) =>
|
|
7437
|
+
identifierRejection(
|
|
7438
|
+
content,
|
|
7439
|
+
liveSessionIdentifiers(),
|
|
7440
|
+
liveStateIdentifiers(),
|
|
7441
|
+
suppliedIdentifiers(),
|
|
7442
|
+
) !== undefined,
|
|
7443
|
+
(words) => replyRejection(words) !== undefined,
|
|
7444
|
+
turn.statedReasons,
|
|
7445
|
+
turn.shellWords,
|
|
7446
|
+
);
|
|
7447
|
+
|
|
7448
|
+
/** A reason the renderer gave a statement this turn, noted for the assembly. */
|
|
7449
|
+
const noteReason = (
|
|
7450
|
+
reason: FailureReason | undefined,
|
|
7451
|
+
): FailureReason | undefined => {
|
|
7452
|
+
if (reason !== undefined) {
|
|
7453
|
+
activeTurn?.statedReasons.add(
|
|
7454
|
+
reason.kind === 'phrase' ? reason.text : codeSpan(reason.text),
|
|
7455
|
+
);
|
|
7456
|
+
}
|
|
7457
|
+
return reason;
|
|
7458
|
+
};
|
|
7459
|
+
|
|
7460
|
+
/**
|
|
7461
|
+
* CAPTAIN-69: the turn's shell-said statements — the facts a reply lists,
|
|
7462
|
+
* the unresolved-effect entries, and the `Failure:` line — each said once.
|
|
7463
|
+
*/
|
|
7464
|
+
const assembleTurnReport = (
|
|
7465
|
+
turn: ActiveTurn,
|
|
7466
|
+
facts: readonly string[],
|
|
7467
|
+
): AssembledReport => {
|
|
7468
|
+
const effects = turn.presentationReports.flatMap((report) =>
|
|
7469
|
+
report.kind === 'effects' ? report.lines : [],
|
|
7470
|
+
);
|
|
7471
|
+
const failure = turn.failureReport;
|
|
7472
|
+
const said = assembleStatements(
|
|
7473
|
+
[
|
|
7474
|
+
...facts.map((text) => ({ text, kind: 'fact' as const })),
|
|
7475
|
+
...effects.map((text) => ({ text, kind: 'entry' as const })),
|
|
7476
|
+
...(failure === undefined
|
|
7477
|
+
? []
|
|
7478
|
+
: [
|
|
7479
|
+
{
|
|
7480
|
+
text: `Failure: ${failure.sentence}`,
|
|
7481
|
+
kind: 'failure' as const,
|
|
7482
|
+
reason: failure.reason,
|
|
7483
|
+
},
|
|
7484
|
+
]),
|
|
7485
|
+
],
|
|
7486
|
+
turn.statedReasons,
|
|
7487
|
+
(text) => statementGuard(text, turn),
|
|
7488
|
+
);
|
|
7489
|
+
const kept = (from: number, to: number): string[] =>
|
|
7490
|
+
said
|
|
7491
|
+
.slice(from, to)
|
|
7492
|
+
.filter((statement): statement is string => statement !== undefined);
|
|
7493
|
+
const listed = facts.length + effects.length;
|
|
7494
|
+
return {
|
|
7495
|
+
facts: kept(0, facts.length),
|
|
7496
|
+
effects: kept(facts.length, listed),
|
|
7497
|
+
...(failure === undefined || said[listed] === undefined
|
|
7498
|
+
? {}
|
|
7499
|
+
: { failure: said[listed] }),
|
|
7500
|
+
};
|
|
7501
|
+
};
|
|
7502
|
+
|
|
6564
7503
|
// -------------------------------------------------------------------------
|
|
6565
7504
|
// The durable conversation (CAPTAIN-31, CAPTAIN-35).
|
|
6566
7505
|
// -------------------------------------------------------------------------
|
|
@@ -6864,7 +7803,7 @@ export function createPlaybookCaptainShell(
|
|
|
6864
7803
|
outcome: DurableCallOutcome,
|
|
6865
7804
|
compose: (options: { reseedDigest?: string; proseRejection?: string }) => string,
|
|
6866
7805
|
): Promise<void> => {
|
|
6867
|
-
let text = outcome.finalText;
|
|
7806
|
+
let text = respondEnvelopeText(outcome.finalText);
|
|
6868
7807
|
const rejection = replyRejection(text);
|
|
6869
7808
|
if (rejection !== undefined) {
|
|
6870
7809
|
// DR-028 §26: the reseed already was this call's single corrective, so a
|
|
@@ -6876,7 +7815,7 @@ export function createPlaybookCaptainShell(
|
|
|
6876
7815
|
const reasked = await durableCall(context, (options) =>
|
|
6877
7816
|
compose({ ...options, proseRejection: rejection }),
|
|
6878
7817
|
);
|
|
6879
|
-
text = reasked.finalText;
|
|
7818
|
+
text = respondEnvelopeText(reasked.finalText);
|
|
6880
7819
|
const second = replyRejection(text);
|
|
6881
7820
|
if (second !== undefined) {
|
|
6882
7821
|
throw markControlFailure(new CaptainProseError(second));
|
|
@@ -6973,8 +7912,8 @@ export function createPlaybookCaptainShell(
|
|
|
6973
7912
|
// Through the one presentation seam, exactly where a model-composed
|
|
6974
7913
|
// reply is presented, so this reply is journaled and single-attempt
|
|
6975
7914
|
// like every other. It needs no reply validation or corrective re-ask:
|
|
6976
|
-
// `
|
|
6977
|
-
await surfaceSettlement({ context,
|
|
7915
|
+
// every statement `giveUpReply` lists passed CAPTAIN-69's guard.
|
|
7916
|
+
await surfaceSettlement({ context, ...giveUpReply() });
|
|
6978
7917
|
return { status: 'ok' as const, finalText: 'ok' };
|
|
6979
7918
|
}
|
|
6980
7919
|
const outcome = await durableCall(context, compose);
|
|
@@ -7162,7 +8101,7 @@ export function createPlaybookCaptainShell(
|
|
|
7162
8101
|
const summary = leafStateSummary();
|
|
7163
8102
|
const settlement: ControllerSettlementDraft = {
|
|
7164
8103
|
status: 'rejected',
|
|
7165
|
-
facts: [`Rejected: ${reason}
|
|
8104
|
+
facts: [`Rejected: ${oneTerminalMark(reason)}`],
|
|
7166
8105
|
reason,
|
|
7167
8106
|
...(summary === undefined ? {} : { leafStateSummary: summary }),
|
|
7168
8107
|
};
|
|
@@ -7232,13 +8171,7 @@ export function createPlaybookCaptainShell(
|
|
|
7232
8171
|
return false;
|
|
7233
8172
|
} catch (error) {
|
|
7234
8173
|
// A failing dispose never resurrects the engagement (DR-029).
|
|
7235
|
-
|
|
7236
|
-
name: 'Error',
|
|
7237
|
-
message: String(error),
|
|
7238
|
-
};
|
|
7239
|
-
facts.push(
|
|
7240
|
-
`Dismissed the ${label} engagement; its disposal failed: ${normalized.name}: ${compactEvidence(normalized.message)}.`,
|
|
7241
|
-
);
|
|
8174
|
+
facts.push(stateFailure(`dismissing ${label}`, failureOf(error)));
|
|
7242
8175
|
return true;
|
|
7243
8176
|
}
|
|
7244
8177
|
};
|
|
@@ -7253,12 +8186,11 @@ export function createPlaybookCaptainShell(
|
|
|
7253
8186
|
try {
|
|
7254
8187
|
frame = await runEffect(() => engage(entry));
|
|
7255
8188
|
} catch (error) {
|
|
7256
|
-
const normalized = normalizeErrorCompact(error) ?? {
|
|
7257
|
-
name: 'Error',
|
|
7258
|
-
message: String(error),
|
|
7259
|
-
};
|
|
7260
8189
|
facts.push(
|
|
7261
|
-
|
|
8190
|
+
stateFailure(
|
|
8191
|
+
`starting /${enablementById.get(entry.id)!.command}`,
|
|
8192
|
+
failureOf(error),
|
|
8193
|
+
),
|
|
7262
8194
|
);
|
|
7263
8195
|
return { report: emptyReport(), failed: true };
|
|
7264
8196
|
}
|
|
@@ -7270,13 +8202,18 @@ export function createPlaybookCaptainShell(
|
|
|
7270
8202
|
// CAPTAIN-22/23: a visibility rejection is an internal shell or
|
|
7271
8203
|
// composition error, never a settled Boss outcome.
|
|
7272
8204
|
if (outcome.error instanceof VisibilityControlError) throw outcome.error;
|
|
7273
|
-
const
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
8205
|
+
const failure = failureOf(outcome.error);
|
|
8206
|
+
const subject = `the first turn of ${frameLabel(frame)}`;
|
|
8207
|
+
const restate = (recorded: RecordedFailure): string =>
|
|
8208
|
+
stateFailure(subject, recorded);
|
|
8209
|
+
const fact = restate(failure);
|
|
8210
|
+
facts.push(fact);
|
|
8211
|
+
activeTurn?.failureStatements.push({
|
|
8212
|
+
frame,
|
|
8213
|
+
fact,
|
|
8214
|
+
message: failure.message,
|
|
8215
|
+
restate,
|
|
8216
|
+
});
|
|
7280
8217
|
return { frame, report: outcome.report, failed: true };
|
|
7281
8218
|
}
|
|
7282
8219
|
return { frame, report: outcome.report, failed: false };
|
|
@@ -7380,21 +8317,25 @@ export function createPlaybookCaptainShell(
|
|
|
7380
8317
|
await requireSession().emitStatus(
|
|
7381
8318
|
`◇ /${enablementById.get(rootPlaybookId)!.command} resumed`,
|
|
7382
8319
|
);
|
|
8320
|
+
// The warning is both a settlement fact and a report the reply carries;
|
|
8321
|
+
// a reply that lists the fact need not say it twice (CAPTAIN-69).
|
|
8322
|
+
const warning = requiresEffectReconciliation
|
|
8323
|
+
? 'The retained work remains parked until its repository-effect evidence is reconciled; no ordinary action was resumed.'
|
|
8324
|
+
: RESUMPTION_DUPLICATE_EFFECT_WARNING;
|
|
7383
8325
|
if (activeTurn) {
|
|
7384
8326
|
appendMandatoryPresentationSuffix(
|
|
7385
8327
|
activeTurn,
|
|
7386
8328
|
requiresEffectReconciliation
|
|
7387
8329
|
? 'The retained work remains parked until its repository-effect evidence is reconciled.'
|
|
7388
8330
|
: RESUMPTION_DUPLICATE_EFFECT_WARNING,
|
|
8331
|
+
[warning],
|
|
7389
8332
|
);
|
|
7390
8333
|
}
|
|
7391
8334
|
return [
|
|
7392
8335
|
generation.rootStateDescription === undefined
|
|
7393
8336
|
? `Resumed /${enablementById.get(rootPlaybookId)!.command} from its retained state; no published root-state description was retained.`
|
|
7394
8337
|
: `Resumed /${enablementById.get(rootPlaybookId)!.command} from the retained state described as ${quoteEvidence(compactEvidence(generation.rootStateDescription))}.`,
|
|
7395
|
-
|
|
7396
|
-
? 'The retained work remains parked until its repository-effect evidence is reconciled; no ordinary action was resumed.'
|
|
7397
|
-
: RESUMPTION_DUPLICATE_EFFECT_WARNING,
|
|
8338
|
+
warning,
|
|
7398
8339
|
];
|
|
7399
8340
|
} catch (error) {
|
|
7400
8341
|
if (installed) {
|
|
@@ -7551,6 +8492,25 @@ export function createPlaybookCaptainShell(
|
|
|
7551
8492
|
return enablement === undefined ? playbookId : `/${enablement.command}`;
|
|
7552
8493
|
};
|
|
7553
8494
|
|
|
8495
|
+
/**
|
|
8496
|
+
* CAPTAIN-71: a failure statement naming nested playbooks by command, its
|
|
8497
|
+
* subject said as `the step` where CAPTAIN-9 refuses it (CAPTAIN-69).
|
|
8498
|
+
*/
|
|
8499
|
+
const stateFailure = (
|
|
8500
|
+
subject: string,
|
|
8501
|
+
failure: RecordedFailure | undefined,
|
|
8502
|
+
): string => {
|
|
8503
|
+
activeTurn?.shellWords.subjects.add(sentenceCase(subject));
|
|
8504
|
+
return sentenceCase(
|
|
8505
|
+
failureClause(
|
|
8506
|
+
subject,
|
|
8507
|
+
noteReason(
|
|
8508
|
+
failureReason(failure, describeNestedPlaybook, speakableWords),
|
|
8509
|
+
),
|
|
8510
|
+
),
|
|
8511
|
+
);
|
|
8512
|
+
};
|
|
8513
|
+
|
|
7554
8514
|
// DR-063 §4: appended once, beside the unresolved-effect and pre-existing
|
|
7555
8515
|
// change reports, whenever the turn settles with the leaf parked in its
|
|
7556
8516
|
// failure state or behind the retained-effect fence.
|
|
@@ -7574,24 +8534,34 @@ export function createPlaybookCaptainShell(
|
|
|
7574
8534
|
const cause = leafFailureCause(view, fenced, unresolvedEffects);
|
|
7575
8535
|
if (cause === undefined) return;
|
|
7576
8536
|
turn.failureReported = true;
|
|
7577
|
-
const
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
8537
|
+
const failure: RecordedFailure = { cause };
|
|
8538
|
+
// One failure, one fact: where a fact this turn already stated the leaf's
|
|
8539
|
+
// parked failure — its failed run result, or a runtime action's failed
|
|
8540
|
+
// receipt or a thrown first turn, delivery, or runtime action whose error
|
|
8541
|
+
// is the one the leaf records — the cause restates that fact in its place
|
|
8542
|
+
// rather than joining beside it.
|
|
8543
|
+
const recorded = (view?.lastError ?? lastError)?.message;
|
|
8544
|
+
const statement = turn.failureStatements.find(
|
|
8545
|
+
({ frame, message }) =>
|
|
8546
|
+
frame === leaf &&
|
|
8547
|
+
(message === undefined ||
|
|
8548
|
+
(recorded !== undefined &&
|
|
8549
|
+
compactEvidence(message) === compactEvidence(recorded))),
|
|
7581
8550
|
);
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
}
|
|
8551
|
+
const fact =
|
|
8552
|
+
statement === undefined
|
|
8553
|
+
? stateFailure(frameLabel(leaf), failure)
|
|
8554
|
+
: statement.restate(failure);
|
|
8555
|
+
const speak = statement?.speak ?? ((text: string) => text);
|
|
8556
|
+
turn.failureReport = {
|
|
8557
|
+
...failureLine(
|
|
8558
|
+
noteReason(
|
|
8559
|
+
failureReason(failure, describeNestedPlaybook, speakableWords),
|
|
8560
|
+
),
|
|
8561
|
+
),
|
|
8562
|
+
controls: failureControlLines(view, fenced),
|
|
8563
|
+
};
|
|
8564
|
+
joinSettlementFact(turn, fact, statement?.fact, speak);
|
|
7595
8565
|
};
|
|
7596
8566
|
|
|
7597
8567
|
const settleSelection = async (
|
|
@@ -7606,9 +8576,8 @@ export function createPlaybookCaptainShell(
|
|
|
7606
8576
|
const freezeControllerEvidence =
|
|
7607
8577
|
(): readonly PlaybookCaptainUnresolvedEffect[] => {
|
|
7608
8578
|
frozenUnresolvedEffects ??= freezeTurnUnresolvedEffects();
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
appendMandatoryPresentationSuffix(turn, report);
|
|
8579
|
+
if (turn !== undefined) {
|
|
8580
|
+
appendUnresolvedEffectReport(turn, frozenUnresolvedEffects);
|
|
7612
8581
|
}
|
|
7613
8582
|
if (turn !== undefined) reportCarriedPreExistingChanges(turn);
|
|
7614
8583
|
if (turn !== undefined) {
|
|
@@ -7633,10 +8602,7 @@ export function createPlaybookCaptainShell(
|
|
|
7633
8602
|
} catch (error) {
|
|
7634
8603
|
if (turn?.presentationError === error) throw error;
|
|
7635
8604
|
const aborted = signal.aborted || activeContext?.signal.aborted === true;
|
|
7636
|
-
const
|
|
7637
|
-
name: 'Error',
|
|
7638
|
-
message: String(error),
|
|
7639
|
-
};
|
|
8605
|
+
const failure = failureOf(error);
|
|
7640
8606
|
if (aborted) {
|
|
7641
8607
|
markConversationUnsynchronized();
|
|
7642
8608
|
if (turn?.outcomePending) {
|
|
@@ -7661,11 +8627,25 @@ export function createPlaybookCaptainShell(
|
|
|
7661
8627
|
const mayHaveApplied =
|
|
7662
8628
|
selection.action !== 'respond' &&
|
|
7663
8629
|
containsEffectThrow(turn, error);
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
)
|
|
8630
|
+
const subject = `the ${selection.action} action`;
|
|
8631
|
+
const unconfirmed = mayHaveApplied
|
|
8632
|
+
? 'Its complete outcome could not be confirmed, and it may have changed the session. It was not repeated automatically.'
|
|
8633
|
+
: 'Its complete outcome could not be confirmed. It was not repeated automatically.';
|
|
8634
|
+
const restate = (recorded: RecordedFailure): string =>
|
|
8635
|
+
`${stateFailure(subject, recorded)} ${unconfirmed}`;
|
|
8636
|
+
const fact = restate(failure);
|
|
8637
|
+
turn.settlementFacts.push(fact);
|
|
8638
|
+
// A delivery or runtime action acts on the leaf, so the leaf's parked
|
|
8639
|
+
// failure may be this very error (CAPTAIN-67).
|
|
8640
|
+
const failedLeaf = leafFrame();
|
|
8641
|
+
if (failedLeaf !== undefined) {
|
|
8642
|
+
turn.failureStatements.push({
|
|
8643
|
+
frame: failedLeaf,
|
|
8644
|
+
fact,
|
|
8645
|
+
message: failure.message,
|
|
8646
|
+
restate,
|
|
8647
|
+
});
|
|
8648
|
+
}
|
|
7669
8649
|
if (!turn.outcomePending && !turn.outcomeRecorded) {
|
|
7670
8650
|
journalAction({
|
|
7671
8651
|
action: selection.action,
|
|
@@ -7756,11 +8736,12 @@ export function createPlaybookCaptainShell(
|
|
|
7756
8736
|
journalAction({ action: 'respond' });
|
|
7757
8737
|
const reask = decisionCall;
|
|
7758
8738
|
if (reask === undefined) {
|
|
7759
|
-
const
|
|
8739
|
+
const text = respondEnvelopeText(selection.text);
|
|
8740
|
+
const rejection = replyRejection(text);
|
|
7760
8741
|
if (rejection !== undefined) {
|
|
7761
8742
|
throw markControlFailure(new CaptainProseError(rejection));
|
|
7762
8743
|
}
|
|
7763
|
-
await surfaceSettlement({ context, text:
|
|
8744
|
+
await surfaceSettlement({ context, text: text! });
|
|
7764
8745
|
} else {
|
|
7765
8746
|
await surfaceProse(
|
|
7766
8747
|
context,
|
|
@@ -8066,13 +9047,12 @@ export function createPlaybookCaptainShell(
|
|
|
8066
9047
|
.describe()
|
|
8067
9048
|
.actions.find((action) => action.id === actionId);
|
|
8068
9049
|
} catch (error) {
|
|
8069
|
-
const normalized = normalizeErrorCompact(error) ?? {
|
|
8070
|
-
name: 'Error',
|
|
8071
|
-
message: String(error),
|
|
8072
|
-
};
|
|
8073
9050
|
return rejectSelection(
|
|
8074
9051
|
selection,
|
|
8075
|
-
|
|
9052
|
+
stateFailure(
|
|
9053
|
+
`asking ${frameLabel(leaf)} which actions it offers`,
|
|
9054
|
+
failureOf(error),
|
|
9055
|
+
),
|
|
8076
9056
|
);
|
|
8077
9057
|
}
|
|
8078
9058
|
if (advertised === undefined) {
|
|
@@ -8086,6 +9066,19 @@ export function createPlaybookCaptainShell(
|
|
|
8086
9066
|
);
|
|
8087
9067
|
}
|
|
8088
9068
|
const actionLabel = advertised.label;
|
|
9069
|
+
// The settlement facts are shell-internal strings composed for the hidden
|
|
9070
|
+
// result-phase prompt: they name the action by its id, which is control
|
|
9071
|
+
// data. The Boss-facing rendering of the same settlement names it by the
|
|
9072
|
+
// runtime's own Boss-appropriate label (PBRT-52), for the one place these
|
|
9073
|
+
// facts are spoken rather than prompted — the CAPTAIN-34 fallback.
|
|
9074
|
+
// A code span is the recorded text as recorded (CAPTAIN-74), so only the
|
|
9075
|
+
// words outside one are renamed.
|
|
9076
|
+
const spokenLabel = `"${compactEvidence(actionLabel)}"`;
|
|
9077
|
+
turn.shellWords.labels.add(spokenLabel);
|
|
9078
|
+
const speak = (fact: string): string =>
|
|
9079
|
+
outsideCodeSpans(fact, (words) =>
|
|
9080
|
+
words.split(`"${actionId}"`).join(spokenLabel),
|
|
9081
|
+
);
|
|
8089
9082
|
// The id the digest advertised and the reply selected by is now also the
|
|
8090
9083
|
// id this turn's outcome-report facts carry (CAPTAIN-9): recording it here
|
|
8091
9084
|
// keeps the closing-reply prompt's copy inside the same supplied set the
|
|
@@ -8160,11 +9153,7 @@ export function createPlaybookCaptainShell(
|
|
|
8160
9153
|
turn.report = {
|
|
8161
9154
|
...outcome.report,
|
|
8162
9155
|
facts: [...facts],
|
|
8163
|
-
bossFacts: facts.map(
|
|
8164
|
-
fact
|
|
8165
|
-
.split(`"${actionId}"`)
|
|
8166
|
-
.join(`"${compactEvidence(actionLabel)}"`),
|
|
8167
|
-
),
|
|
9156
|
+
bossFacts: facts.map(speak),
|
|
8168
9157
|
status: 'failed',
|
|
8169
9158
|
receipt: {
|
|
8170
9159
|
disposition: 'failed',
|
|
@@ -8184,11 +9173,7 @@ export function createPlaybookCaptainShell(
|
|
|
8184
9173
|
turn.report = {
|
|
8185
9174
|
...outcome.report,
|
|
8186
9175
|
facts: [...facts],
|
|
8187
|
-
bossFacts: facts.map(
|
|
8188
|
-
fact
|
|
8189
|
-
.split(`"${actionId}"`)
|
|
8190
|
-
.join(`"${compactEvidence(actionLabel)}"`),
|
|
8191
|
-
),
|
|
9176
|
+
bossFacts: facts.map(speak),
|
|
8192
9177
|
status: 'ok',
|
|
8193
9178
|
receipt: receiptEvidence,
|
|
8194
9179
|
...(establishedSummary === undefined
|
|
@@ -8210,23 +9195,30 @@ export function createPlaybookCaptainShell(
|
|
|
8210
9195
|
}
|
|
8211
9196
|
} else if (receipt.disposition === 'rejected') {
|
|
8212
9197
|
facts.push(
|
|
8213
|
-
|
|
9198
|
+
refusalStatement(
|
|
9199
|
+
`"${actionId}"`,
|
|
9200
|
+
noteReason(recordedReason(receipt.reason, speakableWords)),
|
|
9201
|
+
),
|
|
8214
9202
|
);
|
|
8215
9203
|
} else {
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
|
|
9204
|
+
// A failed receipt acts on the leaf, so the leaf's parked failure may
|
|
9205
|
+
// be this very error — a Retry whose re-run failed again (CAPTAIN-67).
|
|
9206
|
+
const subject = `applying "${actionId}"`;
|
|
9207
|
+
const restate = (failure: RecordedFailure): string =>
|
|
9208
|
+
stateFailure(subject, failure);
|
|
9209
|
+
const fact = restate(receipt.error);
|
|
9210
|
+
facts.push(fact);
|
|
9211
|
+
turn.failureStatements.push({
|
|
9212
|
+
frame: leaf,
|
|
9213
|
+
fact,
|
|
9214
|
+
message: receipt.error.message,
|
|
9215
|
+
restate,
|
|
9216
|
+
speak,
|
|
9217
|
+
});
|
|
8219
9218
|
}
|
|
8220
9219
|
const runFailed = drainRunFailureFacts(facts);
|
|
8221
9220
|
if (runFailed) status = 'failed';
|
|
8222
|
-
|
|
8223
|
-
// hidden result-phase prompt: they name the action by its id, which is
|
|
8224
|
-
// control data. The Boss-facing rendering of the same settlement names it
|
|
8225
|
-
// by the runtime's own Boss-appropriate label (PBRT-52), for the one place
|
|
8226
|
-
// these facts are spoken rather than prompted — the CAPTAIN-34 fallback.
|
|
8227
|
-
const bossFacts = facts.map((fact) =>
|
|
8228
|
-
fact.split(`"${actionId}"`).join(`"${compactEvidence(actionLabel)}"`),
|
|
8229
|
-
);
|
|
9221
|
+
const bossFacts = facts.map(speak);
|
|
8230
9222
|
const summary = leafStateSummary();
|
|
8231
9223
|
turn.report = {
|
|
8232
9224
|
...outcome.report,
|
|
@@ -8266,14 +9258,16 @@ export function createPlaybookCaptainShell(
|
|
|
8266
9258
|
// `settled` guard. That guard closes duplicate submissions before an effect
|
|
8267
9259
|
// starts; it is not evidence that anything ran.
|
|
8268
9260
|
// CAPTAIN-63: the give-up's closing reply, composed from the same settlement
|
|
8269
|
-
// facts the result-phase prompt would have carried.
|
|
8270
|
-
//
|
|
8271
|
-
//
|
|
8272
|
-
//
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
const report =
|
|
8276
|
-
const
|
|
9261
|
+
// facts the result-phase prompt would have carried. What it lists is not
|
|
9262
|
+
// host-authored all the way down, so each statement passes CAPTAIN-69's
|
|
9263
|
+
// guard on its own and the reply still states the settlement truthfully;
|
|
9264
|
+
// no statement is ever left out.
|
|
9265
|
+
const giveUpReply = (): ShellReply => {
|
|
9266
|
+
const turn = activeTurn;
|
|
9267
|
+
const report = turn?.report;
|
|
9268
|
+
const stated = report?.bossFacts ?? report?.facts ?? [];
|
|
9269
|
+
const facts =
|
|
9270
|
+
turn === undefined ? [] : assembleTurnReport(turn, stated).facts;
|
|
8277
9271
|
const stopped = report?.status === 'ok';
|
|
8278
9272
|
const opening = stopped
|
|
8279
9273
|
? 'Stopped that workflow. I will not resume it.'
|
|
@@ -8281,30 +9275,31 @@ export function createPlaybookCaptainShell(
|
|
|
8281
9275
|
const closing = stopped
|
|
8282
9276
|
? 'Send the command again to start fresh work.'
|
|
8283
9277
|
: 'The stop did not settle successfully; recover the session before continuing.';
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
9278
|
+
return {
|
|
9279
|
+
text: [
|
|
9280
|
+
opening,
|
|
9281
|
+
...(facts.length === 0
|
|
9282
|
+
? []
|
|
9283
|
+
: ['Here is what happened:', ...facts.map((fact) => `- ${fact}`)]),
|
|
9284
|
+
closing,
|
|
9285
|
+
].join('\n'),
|
|
9286
|
+
stated,
|
|
9287
|
+
};
|
|
8294
9288
|
};
|
|
8295
9289
|
|
|
8296
|
-
const
|
|
9290
|
+
const failureReply = (): ShellReply => {
|
|
8297
9291
|
const commands = [...enablementById.values()]
|
|
8298
9292
|
.map((enablement) => `/${enablement.command} <task>`)
|
|
8299
9293
|
.join(' or ');
|
|
8300
9294
|
const turn = activeTurn;
|
|
8301
9295
|
const report = turn?.report;
|
|
8302
9296
|
if (report === undefined) {
|
|
8303
|
-
return
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
9297
|
+
return {
|
|
9298
|
+
text:
|
|
9299
|
+
'I could not finish deciding that turn. No action was selected or run — please send the request again' +
|
|
9300
|
+
(commands ? `, or start a playbook directly with ${commands}` : '') +
|
|
9301
|
+
'.',
|
|
9302
|
+
};
|
|
8308
9303
|
}
|
|
8309
9304
|
const settledPreamble =
|
|
8310
9305
|
report.status === 'ok'
|
|
@@ -8318,25 +9313,26 @@ export function createPlaybookCaptainShell(
|
|
|
8318
9313
|
'Ask me where things stand and I will report the current state.';
|
|
8319
9314
|
// The Boss-facing rendering of the settlement, never the prompt-side one:
|
|
8320
9315
|
// `facts` name actions by id and quote runtime-authored text nobody
|
|
8321
|
-
// validated.
|
|
8322
|
-
|
|
8323
|
-
const
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
? []
|
|
8327
|
-
: ['Here is what happened:', ...facts.map((fact) => `- ${fact}`)]),
|
|
8328
|
-
closing,
|
|
8329
|
-
].join('\n');
|
|
8330
|
-
// CAPTAIN-34: this reply is host-authored Boss prose and passes the same
|
|
8331
|
-
// validation every model reply passes. What it interpolates is not
|
|
9316
|
+
// validated. Each is said once, in Boss's words, beside the reports the
|
|
9317
|
+
// reply carries (CAPTAIN-69).
|
|
9318
|
+
const stated = report.bossFacts ?? report.facts;
|
|
9319
|
+
const facts = assembleTurnReport(turn!, stated).facts;
|
|
9320
|
+
// CAPTAIN-34: this reply is host-authored Boss prose. What it lists is not
|
|
8332
9321
|
// host-authored all the way down — a refusal reason and a normalized error
|
|
8333
|
-
// message are foreign text — so
|
|
8334
|
-
//
|
|
8335
|
-
//
|
|
8336
|
-
// turn's only remaining settlement.
|
|
8337
|
-
return
|
|
8338
|
-
|
|
8339
|
-
|
|
9322
|
+
// message are foreign text — so each statement passes CAPTAIN-69's guard
|
|
9323
|
+
// on its own, the reply states the settlement truthfully and names the
|
|
9324
|
+
// next step, and no statement is left out for its own or another's words.
|
|
9325
|
+
// It is never withheld: it is the turn's only remaining settlement.
|
|
9326
|
+
return {
|
|
9327
|
+
text: [
|
|
9328
|
+
settledPreamble,
|
|
9329
|
+
...(facts.length === 0
|
|
9330
|
+
? []
|
|
9331
|
+
: ['Here is what happened:', ...facts.map((fact) => `- ${fact}`)]),
|
|
9332
|
+
closing,
|
|
9333
|
+
].join('\n'),
|
|
9334
|
+
stated,
|
|
9335
|
+
};
|
|
8340
9336
|
};
|
|
8341
9337
|
|
|
8342
9338
|
const settleTurnFailure = async (
|
|
@@ -8358,10 +9354,7 @@ export function createPlaybookCaptainShell(
|
|
|
8358
9354
|
// Through the one presentation seam, so this reply is journaled like
|
|
8359
9355
|
// every other Boss-visible Captain reply. A rejected emission propagates
|
|
8360
9356
|
// unchanged: it is never retried and never disguised as an action failure.
|
|
8361
|
-
await surfaceSettlement({
|
|
8362
|
-
context,
|
|
8363
|
-
text: failureReplyText(),
|
|
8364
|
-
});
|
|
9357
|
+
await surfaceSettlement({ context, ...failureReply() });
|
|
8365
9358
|
};
|
|
8366
9359
|
|
|
8367
9360
|
const enabledCatalog = () =>
|
|
@@ -9447,7 +10440,11 @@ export function createPlaybookCaptainShell(
|
|
|
9447
10440
|
...(hostGiveUp === undefined ? {} : { hostGiveUp }),
|
|
9448
10441
|
settled: false,
|
|
9449
10442
|
presentationAttempted: false,
|
|
10443
|
+
presentationReports: [],
|
|
9450
10444
|
settlementFacts: [],
|
|
10445
|
+
failureStatements: [],
|
|
10446
|
+
statedReasons: new Set<string>(),
|
|
10447
|
+
shellWords: { subjects: new Set<string>(), labels: new Set<string>() },
|
|
9451
10448
|
effectThrows: new Set<unknown>(),
|
|
9452
10449
|
controlFailures: new Set<unknown>(),
|
|
9453
10450
|
settingsPreflightFailures: new Set<unknown>(),
|
|
@@ -9606,4 +10603,14 @@ export function createPlaybookCaptainShell(
|
|
|
9606
10603
|
}
|
|
9607
10604
|
}
|
|
9608
10605
|
|
|
10606
|
+
// The Boss-facing failure renderer, report assembly, and minted-reason tables,
|
|
10607
|
+
// for the shell suite's direct cases; the leading underscore keeps them out of
|
|
10608
|
+
// the stable surface.
|
|
10609
|
+
export const _internal = {
|
|
10610
|
+
failureStatement,
|
|
10611
|
+
bossReport,
|
|
10612
|
+
RUNTIME_REASON_PHRASES,
|
|
10613
|
+
LABELLED_RUNTIME_REASONS,
|
|
10614
|
+
};
|
|
10615
|
+
|
|
9609
10616
|
export default createPlaybookCaptainShell;
|