@sublang/playbook 15.0.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/README.md +1 -1
- package/docs/embedding.md +9 -7
- package/package.json +3 -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/bin/repository-effects.js +54 -13
- 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 -67
- package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3570
- package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5061
- 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 +191 -0
- package/slc/prefix.md +203 -0
- package/slc/slc.pin-inputs.json +11 -1
- package/slc/text2gears.md +71 -11
- package/src/xstate-playbook-runtime.d.ts +82 -9
- package/src/xstate-playbook-runtime.js +1224 -283
- package/src/xstate-playbook-runtime.ts +1671 -338
- package/src/xstate-runtime.d.ts +3 -1
- package/src/xstate-runtime.js +9 -2
- package/src/xstate-runtime.ts +12 -2
|
@@ -5,7 +5,7 @@ import { isDeepStrictEqual } from 'node:util';
|
|
|
5
5
|
import PQueue from 'p-queue';
|
|
6
6
|
import { isAgentCallSettingsError, } from '@sublang/cligent/tmux-play';
|
|
7
7
|
import { assertPlaybookFailureCause } from '@sublang/playbook/runtime';
|
|
8
|
-
import { assertPlaybookRuntimeSnapshot, assertPlaybookEffectLedger, emptyPlaybookEffectLedger, hiddenControlEnvelope, isPlaybookEffectLedgerMonotonicExtension, registerPlaybookAbortCleanup, snapshotJsonValue, validatePlayerResult, } from '../../../src/xstate-runtime.js';
|
|
8
|
+
import { assertPlaybookRuntimeSnapshot, assertPlaybookEffectLedger, emptyPlaybookEffectLedger, hiddenControlEnvelope, isPlaybookEffectLedgerMonotonicExtension, normalizeError, registerPlaybookAbortCleanup, snapshotJsonValue, validatePlayerResult, } from '../../../src/xstate-runtime.js';
|
|
9
9
|
import createDefaultCaptainRuntime from '../captain.playbook/captain.playbook.js';
|
|
10
10
|
function retainedEffectLedgerCanRebase(checkpoint, current) {
|
|
11
11
|
if (checkpoint.boundaries.some(({ physicalReceipt }) => physicalReceipt === undefined)) {
|
|
@@ -390,6 +390,14 @@ function proseRejection(prose, liveSessionIds = [], liveStateIds = [], suppliedI
|
|
|
390
390
|
return 'the reply leaked hidden control syntax or internal control vocabulary';
|
|
391
391
|
}
|
|
392
392
|
}
|
|
393
|
+
return identifierRejection(prose, liveSessionIds, liveStateIds, suppliedIds);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* CAPTAIN-9's identifier duty alone: whether `prose` repeats a live session
|
|
397
|
+
* identifier, a live internal state identifier, or an identifier the shell
|
|
398
|
+
* supplied for selection only.
|
|
399
|
+
*/
|
|
400
|
+
function identifierRejection(prose, liveSessionIds, liveStateIds, suppliedIds) {
|
|
393
401
|
const caseFoldedProse = prose.toLowerCase();
|
|
394
402
|
for (const sessionId of liveSessionIds) {
|
|
395
403
|
// UUID hexadecimal is case-insensitive. A model uppercasing A-F has not
|
|
@@ -410,6 +418,36 @@ function proseRejection(prose, liveSessionIds = [], liveStateIds = [], suppliedI
|
|
|
410
418
|
}
|
|
411
419
|
return undefined;
|
|
412
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* CAPTAIN-9: a prose call answered with the routing envelope of a `respond`
|
|
423
|
+
* selection — exactly `{"action": "respond", "text": <string>}`, with nothing
|
|
424
|
+
* but whitespace around it — said its prose in the decision shape, so the
|
|
425
|
+
* prose is that `text`, which then passes the same validation as any reply.
|
|
426
|
+
* Refusing the envelope instead spends the one corrective re-ask on a shape
|
|
427
|
+
* whose meaning is unambiguous, and a model that answers the same way twice
|
|
428
|
+
* costs the Boss the whole reply. Any other JSON stays control JSON.
|
|
429
|
+
*/
|
|
430
|
+
function respondEnvelopeText(reply) {
|
|
431
|
+
const trimmed = reply?.trim();
|
|
432
|
+
if (trimmed === undefined || !trimmed.startsWith('{'))
|
|
433
|
+
return reply;
|
|
434
|
+
let parsed;
|
|
435
|
+
try {
|
|
436
|
+
parsed = JSON.parse(trimmed);
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
return reply;
|
|
440
|
+
}
|
|
441
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
442
|
+
return reply;
|
|
443
|
+
}
|
|
444
|
+
const members = parsed;
|
|
445
|
+
return Object.keys(members).length === 2 &&
|
|
446
|
+
members.action === 'respond' &&
|
|
447
|
+
typeof members.text === 'string'
|
|
448
|
+
? members.text
|
|
449
|
+
: reply;
|
|
450
|
+
}
|
|
413
451
|
// DR-013 A1: adapters with no provider-enforced tool-restriction surface.
|
|
414
452
|
// Cligent's Codex, Kimi, and OpenCode adapters reject any `allowedTools`
|
|
415
453
|
// value — including the empty list that expresses tool-free — because their
|
|
@@ -452,28 +490,36 @@ function forwardedToolOptions(requested, captainAdapter) {
|
|
|
452
490
|
return { allowedTools: requested };
|
|
453
491
|
}
|
|
454
492
|
const hiddenJudgeEnvelope = hiddenControlEnvelope;
|
|
493
|
+
/**
|
|
494
|
+
* Each unresolved-effect classification as what the step did (CAPTAIN-58), in
|
|
495
|
+
* the words the matching failure cause uses, so the report assembly reads a
|
|
496
|
+
* cause an entry already states as said (CAPTAIN-69).
|
|
497
|
+
*/
|
|
498
|
+
const UNRESOLVED_EFFECT_CLAUSES = {
|
|
499
|
+
'one-descendant-commit': 'the step made one new commit',
|
|
500
|
+
'multiple-commits': 'the step made more than one commit',
|
|
501
|
+
'rewritten-or-non-descendant': 'the repository history was rewritten',
|
|
502
|
+
'worktree-only-change': 'the step left changes uncommitted',
|
|
503
|
+
'concurrent-or-foreign-change': 'the repository changed outside this step',
|
|
504
|
+
'observation-ambiguous': 'the repository could not be observed after the step',
|
|
505
|
+
incomplete: "the step's effect on the repository was not fully recorded",
|
|
506
|
+
};
|
|
455
507
|
function unresolvedEffectReportLines(unresolvedEffects) {
|
|
456
508
|
return unresolvedEffects.map((effect, index) => {
|
|
457
509
|
const merelyPossible = effect.classification === 'observation-ambiguous' ||
|
|
458
510
|
effect.classification === 'incomplete';
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
? '
|
|
464
|
-
|
|
465
|
-
...(effect.commitOid === undefined
|
|
466
|
-
? []
|
|
467
|
-
: [`proven commit OID ${effect.commitOid}`]),
|
|
468
|
-
].join('; ') + '.';
|
|
511
|
+
const heads = effect.afterHead === undefined
|
|
512
|
+
? `the repository stood at ${effect.baselineHead} before it; no HEAD was available after it`
|
|
513
|
+
: effect.afterHead === effect.baselineHead
|
|
514
|
+
? `HEAD stayed at ${effect.baselineHead}`
|
|
515
|
+
: `HEAD moved from ${effect.baselineHead} to ${effect.commitOid === undefined ? '' : 'the proven commit '}${effect.afterHead}`;
|
|
516
|
+
return `${index + 1}. ${merelyPossible ? 'Possible repository effect, which could not be excluded' : 'Observed repository change'}: ${UNRESOLVED_EFFECT_CLAUSES[effect.classification]}; ${heads}.`;
|
|
469
517
|
});
|
|
470
518
|
}
|
|
471
|
-
function unresolvedEffectBossReport(
|
|
472
|
-
if (unresolvedEffects.length === 0)
|
|
473
|
-
return undefined;
|
|
519
|
+
function unresolvedEffectBossReport(lines) {
|
|
474
520
|
return [
|
|
475
521
|
'Repository-effect evidence:',
|
|
476
|
-
...
|
|
522
|
+
...lines.map((line) => `- ${line}`),
|
|
477
523
|
'This evidence does not establish workflow completion or attribute any repository change or commit to this workflow.',
|
|
478
524
|
].join('\n');
|
|
479
525
|
}
|
|
@@ -535,10 +581,21 @@ function carriedPreExistingReport(carried) {
|
|
|
535
581
|
].join('\n'))
|
|
536
582
|
.join('\n\n');
|
|
537
583
|
}
|
|
584
|
+
/**
|
|
585
|
+
* The carried-changes report as settlement facts, one per commit, saying all
|
|
586
|
+
* the report says so a reply that lists them need not repeat it.
|
|
587
|
+
*/
|
|
538
588
|
function carriedPreExistingFacts(carried) {
|
|
539
589
|
return carried.map(({ commitOid, absorbed, altered }) => {
|
|
540
|
-
const
|
|
541
|
-
|
|
590
|
+
const lists = [
|
|
591
|
+
...(absorbed.length === 0
|
|
592
|
+
? []
|
|
593
|
+
: [`absorbed as found ${boundedPathList(absorbed)}`]),
|
|
594
|
+
...(altered.length === 0
|
|
595
|
+
? []
|
|
596
|
+
: [`altered before committing ${boundedPathList(altered)}`]),
|
|
597
|
+
];
|
|
598
|
+
return `The commit ${commitOid} carries changes that were uncommitted before the step: ${lists.join('; ')}.`;
|
|
542
599
|
});
|
|
543
600
|
}
|
|
544
601
|
// DR-063 §4: the Boss-visible failure report. Every host sees the same
|
|
@@ -563,59 +620,359 @@ function causePathList(paths, truncated) {
|
|
|
563
620
|
? `${printed.join(', ')}, … and ${omitted} more`
|
|
564
621
|
: printed.join(', ');
|
|
565
622
|
}
|
|
566
|
-
|
|
567
|
-
|
|
623
|
+
// CAPTAIN-71: one renderer puts every failure into words, for the Boss and for
|
|
624
|
+
// the turn's settlement facts alike — a failure statement names what failed
|
|
625
|
+
// and why — so no site composes a failure's words or quotes its message
|
|
626
|
+
// itself. A cause says why in the Boss phrase of its code, and a reason the
|
|
627
|
+
// runtime mints from a closed set in its own phrase (CAPTAIN-76); any other
|
|
628
|
+
// recorded reason or message is quoted where it reads as prose and is
|
|
629
|
+
// otherwise shown once, whole, as recorded, in a code span, never replaced by
|
|
630
|
+
// a generic sentence that hides it (CAPTAIN-74).
|
|
631
|
+
const REPOSITORY_EVIDENCE_SENTENCE = "the repository evidence for the step is missing or inconsistent, so the step's outcome was not accepted.";
|
|
632
|
+
const RESULT_EVIDENCE_SENTENCE = "the step's reported result is incomplete or inconsistent, so its outcome was not accepted.";
|
|
633
|
+
const UNDECIDED_OUTCOME_SENTENCE = "the step's outcome could not be decided.";
|
|
634
|
+
const CAPTAIN_INCOMPLETE_SENTENCE = 'the Captain call did not complete.';
|
|
635
|
+
const PLAYER_ERROR_SENTENCE = 'the player reported an error without a message.';
|
|
636
|
+
const PLAYER_ABORTED_SENTENCE = 'the player call was aborted.';
|
|
637
|
+
/**
|
|
638
|
+
* CAPTAIN-76: every reason the runtime records from its own literal text, as
|
|
639
|
+
* the Boss reads it. These are the runtime's own words for its own states — the
|
|
640
|
+
* semantic-reconciliation reasons (PBRT-77), the governed settlement's
|
|
641
|
+
* reasons, the judge's reasons, the player and Captain calls' results that
|
|
642
|
+
* carried no message or no text, its own invariant reasons behind its
|
|
643
|
+
* diagnostic label, and the stand-ins for a player result or an error that
|
|
644
|
+
* carried no message — so no recorded text of one is ever shown as a code the
|
|
645
|
+
* Boss has to decode. Anything not listed here is foreign text (CAPTAIN-74).
|
|
646
|
+
*/
|
|
647
|
+
const SEMANTIC_RECONCILIATION_PHRASES = {
|
|
648
|
+
'missing-repository-receipt': REPOSITORY_EVIDENCE_SENTENCE,
|
|
649
|
+
'invalid-repository-receipt': REPOSITORY_EVIDENCE_SENTENCE,
|
|
650
|
+
'repository-disposition-mismatch': REPOSITORY_EVIDENCE_SENTENCE,
|
|
651
|
+
'missing-effect-evidence': REPOSITORY_EVIDENCE_SENTENCE,
|
|
652
|
+
'missing-presentation-evidence': RESULT_EVIDENCE_SENTENCE,
|
|
653
|
+
'missing-runtime-evidence': RESULT_EVIDENCE_SENTENCE,
|
|
654
|
+
'inconsistent-runtime-evidence': RESULT_EVIDENCE_SENTENCE,
|
|
655
|
+
};
|
|
656
|
+
const RUNTIME_REASON_PHRASES = {
|
|
657
|
+
...SEMANTIC_RECONCILIATION_PHRASES,
|
|
658
|
+
'deferred player result has no semantic evidence': RESULT_EVIDENCE_SENTENCE,
|
|
659
|
+
'host omitted governed semantic settlement': "the host did not settle the step's outcome.",
|
|
660
|
+
'deferred question did not receive an eligible durable binding': "the step's question could not be kept for a later reply.",
|
|
661
|
+
'judge transport failed': UNDECIDED_OUTCOME_SENTENCE,
|
|
662
|
+
'corrective judge failed': UNDECIDED_OUTCOME_SENTENCE,
|
|
663
|
+
'corrective semantic candidate is invalid': UNDECIDED_OUTCOME_SENTENCE,
|
|
664
|
+
'semantic correction budget is unavailable': UNDECIDED_OUTCOME_SENTENCE,
|
|
665
|
+
'callPlayer status "error"': PLAYER_ERROR_SENTENCE,
|
|
666
|
+
'callPlayer status "aborted"': PLAYER_ABORTED_SENTENCE,
|
|
667
|
+
'captainBridge: callPlayer status "error"': PLAYER_ERROR_SENTENCE,
|
|
668
|
+
'captainBridge: callPlayer status "aborted"': PLAYER_ABORTED_SENTENCE,
|
|
669
|
+
'captainBridge: callPlayer returned status=ok with no finalText': 'the player call returned nothing.',
|
|
670
|
+
'captainActor: callCaptain status "error"': CAPTAIN_INCOMPLETE_SENTENCE,
|
|
671
|
+
'captainActor: callCaptain status "aborted"': CAPTAIN_INCOMPLETE_SENTENCE,
|
|
672
|
+
'captainActor: callCaptain returned status=ok with no finalText': 'the Captain call returned nothing.',
|
|
673
|
+
'apply settled with outcome failed': 'the action did not complete.',
|
|
674
|
+
'apply settled with outcome aborted': 'the action was stopped.',
|
|
675
|
+
'unknown runtime defect': 'the runtime failed without saying why.',
|
|
676
|
+
'Unknown error': 'an unknown error.',
|
|
677
|
+
};
|
|
678
|
+
/**
|
|
679
|
+
* CAPTAIN-76: the reasons the runtime mints behind its diagnostic label —
|
|
680
|
+
* `playbook` unless a playbook names its own — as `<label> <suffix>`, the
|
|
681
|
+
* parallel cohort's naming its state id as well and the unresolved governed
|
|
682
|
+
* outcome its own reason; each matches only the whole reason.
|
|
683
|
+
*/
|
|
684
|
+
const LABELLED_RUNTIME_REASONS = [
|
|
685
|
+
[
|
|
686
|
+
/^.+ retained governed semantic envelope is no longer exact$/,
|
|
687
|
+
"the step's retained result no longer matches.",
|
|
688
|
+
],
|
|
689
|
+
[/^.+ parallel state .+ cohort failed$/, 'a parallel step failed.'],
|
|
690
|
+
[
|
|
691
|
+
/^.+ governed semantic reconciliation remains unresolved$/,
|
|
692
|
+
"the step's outcome could not be reconciled.",
|
|
693
|
+
],
|
|
694
|
+
[
|
|
695
|
+
/^.+ reconstructed governed output changed before FSM acceptance$/,
|
|
696
|
+
"the step's result changed before it was accepted.",
|
|
697
|
+
],
|
|
698
|
+
[
|
|
699
|
+
/^.+ deferred continuation remains unresolved$/,
|
|
700
|
+
'the deferred step is still unresolved.',
|
|
701
|
+
],
|
|
702
|
+
[
|
|
703
|
+
/^.+ deferred player returned no result$/,
|
|
704
|
+
'the deferred player returned nothing.',
|
|
705
|
+
],
|
|
706
|
+
[
|
|
707
|
+
/^.+ governed outcome remains unresolved: .+$/,
|
|
708
|
+
"the step's outcome could not be settled.",
|
|
709
|
+
],
|
|
710
|
+
[/^.+ actor entered error status$/, "the step's actor failed."],
|
|
711
|
+
];
|
|
712
|
+
/**
|
|
713
|
+
* The runtime's refusal of an action it does not advertise, which repeats the
|
|
714
|
+
* selected id as JSON (PBRT-52) and so says nothing the Boss can use but that
|
|
715
|
+
* the action is no longer offered (CAPTAIN-76).
|
|
716
|
+
*/
|
|
717
|
+
const NOT_ADVERTISED_REFUSAL = /^action "(?:[^"\\]|\\.)*" is not currently advertised$/;
|
|
718
|
+
/** The phrase of a reason the runtime mints from a closed set (CAPTAIN-76). */
|
|
719
|
+
function runtimeReasonPhrase(reason) {
|
|
720
|
+
if (Object.hasOwn(RUNTIME_REASON_PHRASES, reason)) {
|
|
721
|
+
return RUNTIME_REASON_PHRASES[reason];
|
|
722
|
+
}
|
|
723
|
+
if (NOT_ADVERTISED_REFUSAL.test(reason)) {
|
|
724
|
+
return 'that action is no longer offered.';
|
|
725
|
+
}
|
|
726
|
+
return LABELLED_RUNTIME_REASONS.find(([minted]) => minted.test(reason))?.[1];
|
|
727
|
+
}
|
|
728
|
+
/** A declared repository disposition, as what the step was to leave. */
|
|
729
|
+
const REQUIRED_DISPOSITION_PHRASES = {
|
|
730
|
+
unchanged: 'no change',
|
|
731
|
+
'one-descendant-commit': 'one new commit',
|
|
732
|
+
deferred: 'a change deferred to a later step',
|
|
733
|
+
};
|
|
734
|
+
/** A receipt classification, as what the step left. */
|
|
735
|
+
const OBSERVED_CLASSIFICATION_PHRASES = {
|
|
736
|
+
unchanged: 'no change',
|
|
737
|
+
'one-descendant-commit': 'one new commit',
|
|
738
|
+
'multiple-commits': 'more than one new commit',
|
|
739
|
+
'rewritten-or-non-descendant': 'rewritten history',
|
|
740
|
+
'worktree-only-change': 'uncommitted changes only',
|
|
741
|
+
'concurrent-or-foreign-change': 'a change made outside the step',
|
|
742
|
+
'observation-ambiguous': 'a state that could not be read',
|
|
743
|
+
};
|
|
744
|
+
/**
|
|
745
|
+
* An error-class name leading a message (CAPTAIN-74): `Error`, `Exception`,
|
|
746
|
+
* `Failure`, or `Defect`, alone or ending a CamelCase identifier, as in
|
|
747
|
+
* `TypeError: …`, followed by `: `.
|
|
748
|
+
*/
|
|
749
|
+
const LEADING_ERROR_CLASS = /^(?:[A-Z][A-Za-z0-9]*)?(?:Error|Exception|Failure|Defect): /;
|
|
750
|
+
/**
|
|
751
|
+
* CAPTAIN-74: prose begins with no error-class name — a message behind one is
|
|
752
|
+
* machine text by origin, however what follows it reads — holds a space,
|
|
753
|
+
* begins with a capital letter, ends in a terminal mark, and holds no word
|
|
754
|
+
* CAPTAIN-9's identifier grammar tells apart from English — the same tokens
|
|
755
|
+
* and the same test the reply validation applies, so no second notion of
|
|
756
|
+
* "code-like" exists to disagree with it.
|
|
757
|
+
*/
|
|
758
|
+
function readsAsProse(text) {
|
|
759
|
+
return (!LEADING_ERROR_CLASS.test(text) &&
|
|
760
|
+
text.includes(' ') &&
|
|
761
|
+
/^\p{Lu}/u.test(text) &&
|
|
762
|
+
/[.!?]$/.test(text) &&
|
|
763
|
+
!(text.match(IDENTIFIER_TOKENS) ?? []).some(machineShapedIdentifier));
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* A statement's own end (CAPTAIN-69): a run of terminal marks at its very end
|
|
767
|
+
* collapses to its first, and a statement with none gains a period. A run
|
|
768
|
+
* inside it — `main..HEAD`, `../config.yaml` — is evidence and stays.
|
|
769
|
+
*/
|
|
770
|
+
function oneTerminalMark(text) {
|
|
771
|
+
const statement = text.trim().replace(/([.!?])[.!?]+$/, '$1');
|
|
772
|
+
return /[.!?]$/.test(statement) ? statement : `${statement}.`;
|
|
773
|
+
}
|
|
774
|
+
/** `text` as one Markdown code span, unchanged whatever backticks it holds. */
|
|
775
|
+
function codeSpan(text) {
|
|
776
|
+
const longest = Math.max(0, ...[...text.matchAll(/`+/g)].map(([run]) => run.length));
|
|
777
|
+
const fence = '`'.repeat(longest + 1);
|
|
778
|
+
const pad = text.startsWith('`') || text.endsWith('`') ? ' ' : '';
|
|
779
|
+
return `${fence}${pad}${text}${pad}${fence}`;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* The code spans of `text` as CommonMark reads them: a backtick run opens one
|
|
783
|
+
* that the next run of the same length closes, and one space padding both of
|
|
784
|
+
* its ends is no part of what it shows.
|
|
785
|
+
*/
|
|
786
|
+
function codeSpans(text) {
|
|
787
|
+
const runs = [...text.matchAll(/`+/g)].map((match) => ({
|
|
788
|
+
at: match.index ?? 0,
|
|
789
|
+
length: match[0].length,
|
|
790
|
+
}));
|
|
791
|
+
const spans = [];
|
|
792
|
+
for (let index = 0; index < runs.length; index += 1) {
|
|
793
|
+
const open = runs[index];
|
|
794
|
+
const offset = runs
|
|
795
|
+
.slice(index + 1)
|
|
796
|
+
.findIndex(({ length }) => length === open.length);
|
|
797
|
+
if (offset < 0)
|
|
798
|
+
continue;
|
|
799
|
+
const close = runs[index + 1 + offset];
|
|
800
|
+
const inner = text.slice(open.at + open.length, close.at);
|
|
801
|
+
spans.push({
|
|
802
|
+
start: open.at,
|
|
803
|
+
end: close.at + close.length,
|
|
804
|
+
content: inner.length > 2 &&
|
|
805
|
+
inner.startsWith(' ') &&
|
|
806
|
+
inner.endsWith(' ') &&
|
|
807
|
+
inner.trim().length > 0
|
|
808
|
+
? inner.slice(1, -1)
|
|
809
|
+
: inner,
|
|
810
|
+
});
|
|
811
|
+
index += 1 + offset;
|
|
812
|
+
}
|
|
813
|
+
return spans;
|
|
814
|
+
}
|
|
815
|
+
/** `text` with `map` applied to what stands outside its code spans. */
|
|
816
|
+
function outsideCodeSpans(text, map) {
|
|
817
|
+
let mapped = '';
|
|
818
|
+
let at = 0;
|
|
819
|
+
for (const span of codeSpans(text)) {
|
|
820
|
+
mapped += map(text.slice(at, span.start)) + text.slice(span.start, span.end);
|
|
821
|
+
at = span.end;
|
|
822
|
+
}
|
|
823
|
+
return mapped + map(text.slice(at));
|
|
824
|
+
}
|
|
825
|
+
function sentenceCase(text) {
|
|
826
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
827
|
+
}
|
|
828
|
+
const SPEAKABLE_ANYWHERE = () => true;
|
|
829
|
+
/**
|
|
830
|
+
* CAPTAIN-76/74: a recorded reason or message, its control characters spaced,
|
|
831
|
+
* its ends trimmed, and its length bounded, is said by its phrase where the
|
|
832
|
+
* runtime minted it from a closed set; otherwise it is quoted where it reads
|
|
833
|
+
* as prose — and only where the shell may say it outside a code span — and is
|
|
834
|
+
* otherwise kept whole as recorded; `undefined` where nothing is left of it.
|
|
835
|
+
*/
|
|
836
|
+
function recordedReason(text, speakable = SPEAKABLE_ANYWHERE) {
|
|
837
|
+
const reason = compactEvidence(text);
|
|
838
|
+
if (reason.length === 0)
|
|
839
|
+
return undefined;
|
|
840
|
+
const minted = runtimeReasonPhrase(reason);
|
|
841
|
+
if (minted !== undefined)
|
|
842
|
+
return { kind: 'phrase', text: minted };
|
|
843
|
+
return readsAsProse(reason) && speakable(reason)
|
|
844
|
+
? { kind: 'phrase', text: oneTerminalMark(reason) }
|
|
845
|
+
: { kind: 'code', text: reason };
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* `<subject> <outcome>: <reason>` for words the Boss reads as they are,
|
|
849
|
+
* `<subject> <outcome> with the <noun> `<reason>`.` for a reason kept as
|
|
850
|
+
* recorded, and `<subject> <outcome>.` where there is none.
|
|
851
|
+
*/
|
|
852
|
+
function reasonedClause(subject, outcome, noun, reason) {
|
|
853
|
+
if (reason === undefined)
|
|
854
|
+
return `${subject} ${outcome}.`;
|
|
855
|
+
return reason.kind === 'phrase'
|
|
856
|
+
? `${subject} ${outcome}: ${reason.text}`
|
|
857
|
+
: `${subject} ${outcome} with the ${noun} ${codeSpan(reason.text)}.`;
|
|
858
|
+
}
|
|
859
|
+
function failureClause(subject, reason) {
|
|
860
|
+
return reasonedClause(subject, 'failed', 'error', reason);
|
|
861
|
+
}
|
|
862
|
+
/** The Boss phrase of each cause code, with its bounded evidence (DR-063 §4). */
|
|
863
|
+
function causeReason(cause, describePlaybook, speakable = SPEAKABLE_ANYWHERE) {
|
|
864
|
+
const phrase = (text) => ({ kind: 'phrase', text });
|
|
568
865
|
const evidence = cause.evidence;
|
|
569
866
|
const paths = evidence.paths;
|
|
570
867
|
const code = cause.code;
|
|
571
868
|
switch (code) {
|
|
572
869
|
case 'commit-missing':
|
|
573
|
-
return (paths?.uncommitted ?? []).length === 0
|
|
870
|
+
return phrase((paths?.uncommitted ?? []).length === 0
|
|
574
871
|
? 'the step had to commit its work and committed nothing.'
|
|
575
|
-
: `the step had to commit its work and committed nothing; these changes are uncommitted: ${causePathList(paths?.uncommitted, paths?.truncated)}
|
|
872
|
+
: `the step had to commit its work and committed nothing; these changes are uncommitted: ${causePathList(paths?.uncommitted, paths?.truncated)}.`);
|
|
576
873
|
case 'commit-residual': {
|
|
577
874
|
const subject = evidence.commitOid === undefined
|
|
578
875
|
? "the step's commit"
|
|
579
876
|
: `the commit ${evidence.commitOid}`;
|
|
580
877
|
if ((paths?.uncommitted ?? []).length > 0) {
|
|
581
|
-
return `${subject} left changes uncommitted: ${causePathList(paths?.uncommitted, paths?.truncated)}
|
|
878
|
+
return phrase(`${subject} left changes uncommitted: ${causePathList(paths?.uncommitted, paths?.truncated)}.`);
|
|
582
879
|
}
|
|
583
880
|
if ((paths?.altered ?? []).length > 0) {
|
|
584
|
-
return `${subject} left altered changes uncommitted: ${causePathList(paths?.altered, paths?.truncated)}
|
|
881
|
+
return phrase(`${subject} left altered changes uncommitted: ${causePathList(paths?.altered, paths?.truncated)}.`);
|
|
585
882
|
}
|
|
586
|
-
return `${subject} left changes uncommitted beside it
|
|
883
|
+
return phrase(`${subject} left changes uncommitted beside it.`);
|
|
587
884
|
}
|
|
588
885
|
case 'pre-existing-lost':
|
|
589
|
-
return `uncommitted changes you had were lost: ${causePathList(paths?.lost, paths?.truncated)}
|
|
886
|
+
return phrase(`uncommitted changes you had were lost: ${causePathList(paths?.lost, paths?.truncated)}.`);
|
|
590
887
|
case 'commits-more-than-one':
|
|
591
|
-
return `the step made more than one commit; HEAD moved from ${evidence.baselineHead} to ${evidence.afterHead}
|
|
888
|
+
return phrase(`the step made more than one commit; HEAD moved from ${evidence.baselineHead} to ${evidence.afterHead}.`);
|
|
592
889
|
case 'history-rewritten':
|
|
593
|
-
return `the repository history was rewritten; HEAD moved from ${evidence.baselineHead} to ${evidence.afterHead}
|
|
890
|
+
return phrase(`the repository history was rewritten; HEAD moved from ${evidence.baselineHead} to ${evidence.afterHead}.`);
|
|
594
891
|
case 'foreign-change':
|
|
595
|
-
return `the repository changed outside this step: ${causePathList(paths?.changed, paths?.truncated)}
|
|
892
|
+
return phrase(`the repository changed outside this step: ${causePathList(paths?.changed, paths?.truncated)}.`);
|
|
596
893
|
case 'observation-unstable':
|
|
597
|
-
return `the repository could not be observed after the step;
|
|
598
|
-
case 'attribution-ambiguous':
|
|
599
|
-
|
|
894
|
+
return phrase(`the repository could not be observed after the step; the repository stood at ${evidence.baselineHead} before it.`);
|
|
895
|
+
case 'attribution-ambiguous': {
|
|
896
|
+
const required = evidence.required === undefined
|
|
897
|
+
? undefined
|
|
898
|
+
: REQUIRED_DISPOSITION_PHRASES[evidence.required];
|
|
899
|
+
const observed = evidence.observed === undefined
|
|
900
|
+
? undefined
|
|
901
|
+
: OBSERVED_CLASSIFICATION_PHRASES[evidence.observed];
|
|
902
|
+
return phrase(required === undefined || observed === undefined
|
|
903
|
+
? 'the repository change could not be attributed to this step.'
|
|
904
|
+
: `the repository change could not be attributed to this step: it should have left ${required} but left ${observed}.`);
|
|
905
|
+
}
|
|
600
906
|
case 'receipt-missing':
|
|
601
|
-
return `
|
|
907
|
+
return phrase(`the step's effect on the repository was not fully recorded; the repository stood at ${evidence.baselineHead} before it.`);
|
|
602
908
|
case 'judge-failed':
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
: `the hidden adjudication failed: ${evidence.reason} (${evidence.error.name}: ${compactEvidence(evidence.error.message)}).`;
|
|
909
|
+
// The runtime's own closed reasons stay in the cause's data.
|
|
910
|
+
return phrase(UNDECIDED_OUTCOME_SENTENCE);
|
|
606
911
|
case 'player-failed':
|
|
607
|
-
|
|
912
|
+
// The player's message says why, as any recorded message does: by the
|
|
913
|
+
// runtime's phrase where it minted the message, else as prose or code.
|
|
914
|
+
return phrase(failureClause(`the ${evidence.roleId} call`, recordedReason(evidence.error?.message ?? '', speakable)));
|
|
608
915
|
case 'aborted':
|
|
609
|
-
return 'the turn was aborted before the work settled.';
|
|
610
|
-
case 'child-failed':
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
: `the nested ${nested} run failed: ${failureCauseSentence(evidence.cause, describePlaybook)}`;
|
|
615
|
-
}
|
|
916
|
+
return phrase('the turn was aborted before the work settled.');
|
|
917
|
+
case 'child-failed':
|
|
918
|
+
return phrase(failureClause(`the nested ${describePlaybook(evidence.playbookId ?? '')} run`, evidence.cause === undefined
|
|
919
|
+
? undefined
|
|
920
|
+
: causeReason(evidence.cause, describePlaybook, speakable)));
|
|
616
921
|
case 'runtime-defect':
|
|
617
|
-
|
|
922
|
+
// No more specific code: a reason the runtime minted is said by its
|
|
923
|
+
// phrase, and any other reason as it was recorded.
|
|
924
|
+
return recordedReason(evidence.reason ?? '', speakable);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Why a recorded failure failed: the phrase of the cause it carries where the
|
|
929
|
+
* closed validator accepts one, and otherwise its recorded message.
|
|
930
|
+
*/
|
|
931
|
+
function failureReason(failure, describePlaybook, speakable = SPEAKABLE_ANYWHERE) {
|
|
932
|
+
if (failure === undefined)
|
|
933
|
+
return undefined;
|
|
934
|
+
if (failure.cause !== undefined) {
|
|
935
|
+
let cause;
|
|
936
|
+
try {
|
|
937
|
+
cause = assertPlaybookFailureCause(failure.cause);
|
|
938
|
+
}
|
|
939
|
+
catch {
|
|
940
|
+
// A cause the closed validator refuses is not a cause.
|
|
941
|
+
}
|
|
942
|
+
if (cause !== undefined) {
|
|
943
|
+
return causeReason(cause, describePlaybook, speakable);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return failure.message === undefined
|
|
947
|
+
? undefined
|
|
948
|
+
: recordedReason(failure.message, speakable);
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* CAPTAIN-71: the one way a failure is put into words — `<Subject> failed:
|
|
952
|
+
* <reason>.` — `subject` naming what failed in the Boss's words and `failure`
|
|
953
|
+
* the error or cause the shell recorded.
|
|
954
|
+
*/
|
|
955
|
+
function failureStatement(subject, failure, describePlaybook = (playbookId) => playbookId, speakable = SPEAKABLE_ANYWHERE) {
|
|
956
|
+
return sentenceCase(failureClause(subject, failureReason(failure, describePlaybook, speakable)));
|
|
957
|
+
}
|
|
958
|
+
/** The runtime's refusal of `action`, its reason said as a failure's is. */
|
|
959
|
+
function refusalStatement(action, reason) {
|
|
960
|
+
return sentenceCase(reasonedClause('the runtime', `refused ${action}`, 'reason', reason));
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* The `Failure:` line of a parked failure (CAPTAIN-67): the reason's phrase,
|
|
964
|
+
* or, for a reason kept as recorded, the step's failure statement; and the
|
|
965
|
+
* reason it states, which a statement carrying the same failure holds.
|
|
966
|
+
*/
|
|
967
|
+
function failureLine(reason) {
|
|
968
|
+
if (reason?.kind === 'phrase') {
|
|
969
|
+
return { sentence: reason.text, reason: reason.text };
|
|
618
970
|
}
|
|
971
|
+
const sentence = failureClause('the step', reason);
|
|
972
|
+
return {
|
|
973
|
+
sentence,
|
|
974
|
+
reason: reason === undefined ? sentence : codeSpan(reason.text),
|
|
975
|
+
};
|
|
619
976
|
}
|
|
620
977
|
function failureControlLine(action) {
|
|
621
978
|
// DR-063 §3: an action that publishes no standing is `ready`.
|
|
@@ -624,20 +981,163 @@ function failureControlLine(action) {
|
|
|
624
981
|
? `- ${action.label} (ready)`
|
|
625
982
|
: `- ${action.label} (${standing}: ${controlReasonPhrase(action.reason)})`;
|
|
626
983
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
984
|
+
/**
|
|
985
|
+
* Whether `container` already states `statement`: the same words, case and
|
|
986
|
+
* terminal mark aside, standing as a phrase of their own rather than inside a
|
|
987
|
+
* longer word or command.
|
|
988
|
+
*/
|
|
989
|
+
function alreadyStates(container, statement) {
|
|
990
|
+
const haystack = container.toLowerCase();
|
|
991
|
+
const needle = statement.toLowerCase().replace(/[.!?]+$/, '');
|
|
992
|
+
if (needle.length === 0)
|
|
993
|
+
return true;
|
|
994
|
+
for (let at = haystack.indexOf(needle); at >= 0; at = haystack.indexOf(needle, at + 1)) {
|
|
995
|
+
const before = haystack.charAt(at - 1);
|
|
996
|
+
const after = haystack.charAt(at + needle.length);
|
|
997
|
+
if (!/[\p{L}\p{N}_/-]/u.test(before) && !/[\p{L}\p{N}_-]/u.test(after)) {
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return false;
|
|
633
1002
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
1003
|
+
/** The words a reason CAPTAIN-9 withholds is said as. */
|
|
1004
|
+
const WITHHELD_REASON = 'an internal error';
|
|
1005
|
+
/** `text` with each of its code spans read as a space. */
|
|
1006
|
+
function wordsOutsideCodeSpans(text) {
|
|
1007
|
+
let words = '';
|
|
1008
|
+
let at = 0;
|
|
1009
|
+
for (const span of codeSpans(text)) {
|
|
1010
|
+
words += `${text.slice(at, span.start)} `;
|
|
1011
|
+
at = span.end;
|
|
1012
|
+
}
|
|
1013
|
+
return words + text.slice(at);
|
|
1014
|
+
}
|
|
1015
|
+
const NO_SHELL_WORDS = { subjects: [], labels: [] };
|
|
1016
|
+
/**
|
|
1017
|
+
* CAPTAIN-69's guard on one statement the shell says, so no statement is
|
|
1018
|
+
* lost for its own or another's words: a code span whose content repeats an
|
|
1019
|
+
* identifier CAPTAIN-9 withholds reads, with the noun before it, `an internal
|
|
1020
|
+
* error`; a reason the renderer phrased whose own words fail CAPTAIN-9 reads
|
|
1021
|
+
* so too; a failure subject beginning the statement whose own words fail
|
|
1022
|
+
* reads `The step`; and a quoted action label whose own words fail reads `the
|
|
1023
|
+
* selected action`. Whatever is left, the statement is never left out.
|
|
1024
|
+
*/
|
|
1025
|
+
function guardedStatement(text, withholds, refuses, phrased = [], shellWords = NO_SHELL_WORDS) {
|
|
1026
|
+
let guarded = '';
|
|
1027
|
+
let at = 0;
|
|
1028
|
+
for (const span of codeSpans(text)) {
|
|
1029
|
+
const before = text.slice(at, span.start);
|
|
1030
|
+
guarded += withholds(span.content)
|
|
1031
|
+
? `${before.replace(/the (?:error|reason) $/, '')}${WITHHELD_REASON}`
|
|
1032
|
+
: before + text.slice(span.start, span.end);
|
|
1033
|
+
at = span.end;
|
|
638
1034
|
}
|
|
639
|
-
|
|
640
|
-
|
|
1035
|
+
guarded += text.slice(at);
|
|
1036
|
+
const fails = (statement) => {
|
|
1037
|
+
const words = wordsOutsideCodeSpans(statement);
|
|
1038
|
+
return words.trim().length > 0 && refuses(words);
|
|
1039
|
+
};
|
|
1040
|
+
for (const reason of phrased) {
|
|
1041
|
+
if (!fails(reason))
|
|
1042
|
+
continue;
|
|
1043
|
+
guarded =
|
|
1044
|
+
guarded === `Failure: ${reason}`
|
|
1045
|
+
? `Failure: the step failed with ${WITHHELD_REASON}.`
|
|
1046
|
+
: guarded.split(`: ${reason}`).join(` with ${WITHHELD_REASON}.`);
|
|
1047
|
+
}
|
|
1048
|
+
for (const subject of shellWords.subjects) {
|
|
1049
|
+
if (guarded.startsWith(`${subject} `) && fails(subject)) {
|
|
1050
|
+
guarded = `The step${guarded.slice(subject.length)}`;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
for (const label of shellWords.labels) {
|
|
1054
|
+
if (fails(label)) {
|
|
1055
|
+
guarded = guarded.split(label).join('the selected action');
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return guarded;
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* CAPTAIN-69: the one way the shell assembles Boss-facing text of its own.
|
|
1062
|
+
* Each statement ends in one terminal mark and passes `guard`. The kinds
|
|
1063
|
+
* decide what is dropped: an unresolved-effect entry records a boundary of
|
|
1064
|
+
* its own and always stays, while a fact or the `Failure:` line is dropped
|
|
1065
|
+
* where an entry, or a fact before it that stays, carries its reason — for
|
|
1066
|
+
* the `Failure:` line the reason it states, for a fact every reason the
|
|
1067
|
+
* renderer gave that turn (`reasons`) that it holds, else the fact itself.
|
|
1068
|
+
* The result is aligned with `statements`, a dropped statement `undefined`.
|
|
1069
|
+
*/
|
|
1070
|
+
function assembleStatements(statements, reasons = [], guard = (text) => text) {
|
|
1071
|
+
const rendered = [...new Set(reasons)];
|
|
1072
|
+
const weighed = statements.map(({ text, kind = 'fact', reason }) => {
|
|
1073
|
+
const statement = oneTerminalMark(text);
|
|
1074
|
+
if (reason !== undefined)
|
|
1075
|
+
return { text: statement, kind, own: [reason] };
|
|
1076
|
+
const held = rendered.filter((candidate) => alreadyStates(statement, candidate));
|
|
1077
|
+
return { text: statement, kind, own: held.length > 0 ? held : [statement] };
|
|
1078
|
+
});
|
|
1079
|
+
const entries = weighed
|
|
1080
|
+
.filter(({ kind }) => kind === 'entry')
|
|
1081
|
+
.map(({ text }) => text);
|
|
1082
|
+
const facts = [];
|
|
1083
|
+
const carried = (own, containers) => containers.some((container) => own.every((reason) => alreadyStates(container, reason)));
|
|
1084
|
+
const stays = weighed.map(({ text, kind, own }) => {
|
|
1085
|
+
if (kind !== 'fact')
|
|
1086
|
+
return true;
|
|
1087
|
+
if (carried(own, entries) || carried(own, facts))
|
|
1088
|
+
return false;
|
|
1089
|
+
facts.push(text);
|
|
1090
|
+
return true;
|
|
1091
|
+
});
|
|
1092
|
+
return weighed.map(({ text, kind, own }, index) => stays[index] &&
|
|
1093
|
+
(kind !== 'failure' || !carried(own, [...entries, ...facts]))
|
|
1094
|
+
? oneTerminalMark(guard(text))
|
|
1095
|
+
: undefined);
|
|
1096
|
+
}
|
|
1097
|
+
/** The statements of `statements` that stay, as CAPTAIN-69 assembles them. */
|
|
1098
|
+
function bossReport(statements, reasons = []) {
|
|
1099
|
+
return assembleStatements(statements.map((statement) => typeof statement === 'string' ? { text: statement } : statement), reasons).filter((statement) => statement !== undefined);
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* The reports that accompany a reply, from the turn's assembled statements
|
|
1103
|
+
* (CAPTAIN-69): a report whose facts the reply lists is not said again, the
|
|
1104
|
+
* unresolved-effect report carries every entry as guarded, and the failure
|
|
1105
|
+
* report's `Controls:` list always stays.
|
|
1106
|
+
*/
|
|
1107
|
+
function presentationSuffix(turn, assembled) {
|
|
1108
|
+
const parts = turn.presentationReports.flatMap((report) => {
|
|
1109
|
+
if (report.kind === 'stated') {
|
|
1110
|
+
return report.facts.length > 0 &&
|
|
1111
|
+
report.facts.every((fact) => assembled.facts.some((listed) => alreadyStates(listed, fact)))
|
|
1112
|
+
? []
|
|
1113
|
+
: [report.text];
|
|
1114
|
+
}
|
|
1115
|
+
return assembled.effects.length === 0
|
|
1116
|
+
? []
|
|
1117
|
+
: [unresolvedEffectBossReport(assembled.effects)];
|
|
1118
|
+
});
|
|
1119
|
+
const failure = turn.failureReport;
|
|
1120
|
+
if (failure !== undefined) {
|
|
1121
|
+
parts.push([
|
|
1122
|
+
...(assembled.failure === undefined ? [] : [assembled.failure]),
|
|
1123
|
+
'Controls:',
|
|
1124
|
+
...(failure.controls.length === 0 ? ['- none'] : failure.controls),
|
|
1125
|
+
].join('\n'));
|
|
1126
|
+
}
|
|
1127
|
+
return parts.length === 0 ? undefined : parts.join('\n\n');
|
|
1128
|
+
}
|
|
1129
|
+
function appendMandatoryPresentationSuffix(turn, suffix, facts = []) {
|
|
1130
|
+
if (!turn.presentationReports.some((report) => report.kind === 'stated' && report.text.includes(suffix))) {
|
|
1131
|
+
turn.presentationReports.push({ kind: 'stated', text: suffix, facts });
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
function appendUnresolvedEffectReport(turn, unresolvedEffects) {
|
|
1135
|
+
if (unresolvedEffects.length > 0 &&
|
|
1136
|
+
!turn.presentationReports.some((report) => report.kind === 'effects')) {
|
|
1137
|
+
turn.presentationReports.push({
|
|
1138
|
+
kind: 'effects',
|
|
1139
|
+
lines: unresolvedEffectReportLines(unresolvedEffects),
|
|
1140
|
+
});
|
|
641
1141
|
}
|
|
642
1142
|
}
|
|
643
1143
|
// CAPTAIN-20: the result-phase block the shell supplies inside the closing
|
|
@@ -2180,21 +2680,46 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2180
2680
|
if (report === undefined)
|
|
2181
2681
|
return;
|
|
2182
2682
|
turn.carriedPreExistingReported = true;
|
|
2183
|
-
appendMandatoryPresentationSuffix(turn, report);
|
|
2184
2683
|
const facts = carriedPreExistingFacts(carried);
|
|
2185
|
-
turn
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2684
|
+
appendMandatoryPresentationSuffix(turn, report, facts);
|
|
2685
|
+
for (const fact of facts) {
|
|
2686
|
+
joinSettlementFact(turn, fact);
|
|
2687
|
+
}
|
|
2688
|
+
};
|
|
2689
|
+
/**
|
|
2690
|
+
* CAPTAIN-67: one fact joins the turn once. The action's own report was
|
|
2691
|
+
* assembled from the facts as they stood when it settled, and the
|
|
2692
|
+
* result-phase prompt reads that report, so a later fact joins it as well as
|
|
2693
|
+
* the turn's running list — which that report's `facts` often *is*, so
|
|
2694
|
+
* appending to both stated the fact twice. A fact that restates one already
|
|
2695
|
+
* listed for the same failure takes that fact's place.
|
|
2696
|
+
*/
|
|
2697
|
+
const joinSettlementFact = (turn, fact, replacing, speak = (text) => text) => {
|
|
2698
|
+
const placed = (list, entry, replaced) => {
|
|
2699
|
+
if (list.includes(entry))
|
|
2700
|
+
return list;
|
|
2701
|
+
const at = replaced === undefined ? -1 : list.indexOf(replaced);
|
|
2702
|
+
return at < 0
|
|
2703
|
+
? [...list, entry]
|
|
2704
|
+
: list.map((listed, index) => (index === at ? entry : listed));
|
|
2705
|
+
};
|
|
2706
|
+
const running = placed(turn.settlementFacts, fact, replacing);
|
|
2707
|
+
if (running !== turn.settlementFacts) {
|
|
2708
|
+
turn.settlementFacts.splice(0, turn.settlementFacts.length, ...running);
|
|
2197
2709
|
}
|
|
2710
|
+
const report = turn.report;
|
|
2711
|
+
if (report === undefined)
|
|
2712
|
+
return;
|
|
2713
|
+
// The Boss-facing rendering holds the same fact as the Boss reads it.
|
|
2714
|
+
turn.report = {
|
|
2715
|
+
...report,
|
|
2716
|
+
facts: placed(report.facts, fact, replacing),
|
|
2717
|
+
...(report.bossFacts === undefined
|
|
2718
|
+
? {}
|
|
2719
|
+
: {
|
|
2720
|
+
bossFacts: placed(report.bossFacts, speak(fact), replacing === undefined ? undefined : speak(replacing)),
|
|
2721
|
+
}),
|
|
2722
|
+
};
|
|
2198
2723
|
};
|
|
2199
2724
|
const normalizeInstalledRetainedGenerations = (value) => {
|
|
2200
2725
|
const path = 'Captain retained generations';
|
|
@@ -2631,6 +3156,18 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2631
3156
|
}
|
|
2632
3157
|
return { name: 'Error', message: String(value) };
|
|
2633
3158
|
};
|
|
3159
|
+
/**
|
|
3160
|
+
* CAPTAIN-71: what the shell recorded of a thrown value — its message, and
|
|
3161
|
+
* the cause the runtime decided where the value carries one (DR-063 §2).
|
|
3162
|
+
*/
|
|
3163
|
+
const failureOf = (error) => {
|
|
3164
|
+
const normalized = normalizeErrorCompact(error) ?? {
|
|
3165
|
+
name: 'Error',
|
|
3166
|
+
message: String(error),
|
|
3167
|
+
};
|
|
3168
|
+
const { cause } = normalizeError(error);
|
|
3169
|
+
return cause === undefined ? normalized : { ...normalized, cause };
|
|
3170
|
+
};
|
|
2634
3171
|
const payloadRecord = (payload) => typeof payload === 'object' && payload !== null && !Array.isArray(payload)
|
|
2635
3172
|
? payload
|
|
2636
3173
|
: undefined;
|
|
@@ -3638,11 +4175,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3638
4175
|
}
|
|
3639
4176
|
else {
|
|
3640
4177
|
if (runFailureFacts) {
|
|
3641
|
-
|
|
3642
|
-
name: 'Error',
|
|
3643
|
-
message: String(error),
|
|
3644
|
-
};
|
|
3645
|
-
runFailureFacts.push(`Cleanup while removing ${frameLabel(child)} failed: ${normalized.name}: ${compactEvidence(normalized.message)}.`);
|
|
4178
|
+
runFailureFacts.push(stateFailure(`cleanup after ${frameLabel(child)}`, failureOf(error)));
|
|
3646
4179
|
}
|
|
3647
4180
|
effectiveResult = {
|
|
3648
4181
|
status: context.signal.aborted ? 'aborted' : 'error',
|
|
@@ -3727,10 +4260,14 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3727
4260
|
runFailureFacts.push(`${frameLabel(frame)} was aborted before its outcome could be confirmed; it was not repeated automatically.`);
|
|
3728
4261
|
}
|
|
3729
4262
|
if (result.outcome === 'failed' && runFailureFacts) {
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
4263
|
+
const subject = frameLabel(frame);
|
|
4264
|
+
const fact = stateFailure(subject, result.error);
|
|
4265
|
+
runFailureFacts.push(fact);
|
|
4266
|
+
activeTurn?.failureStatements.push({
|
|
4267
|
+
frame,
|
|
4268
|
+
fact,
|
|
4269
|
+
restate: (failure) => stateFailure(subject, failure),
|
|
4270
|
+
});
|
|
3734
4271
|
}
|
|
3735
4272
|
if (leafFrame()) {
|
|
3736
4273
|
await setMode('engaged.parked', `turn:${result.outcome}`);
|
|
@@ -4309,7 +4846,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4309
4846
|
// all of this prose. Preserve the exact attempt before crossing the
|
|
4310
4847
|
// boundary; the uncertainty record below keeps recovery from pretending
|
|
4311
4848
|
// delivery was confirmed while still understanding a Boss follow-up.
|
|
4312
|
-
const suffix = turn
|
|
4849
|
+
const suffix = turn === undefined
|
|
4850
|
+
? undefined
|
|
4851
|
+
: presentationSuffix(turn, assembleTurnReport(turn, settlement.stated ?? []));
|
|
4313
4852
|
const visibleText = suffix === undefined || settlement.text.includes(suffix)
|
|
4314
4853
|
? settlement.text
|
|
4315
4854
|
: `${settlement.text.trimEnd()}\n\n${suffix}`;
|
|
@@ -4400,6 +4939,53 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4400
4939
|
// The one predicate every Boss-visible Captain reply passes, whether the
|
|
4401
4940
|
// words came from the model or the shell's conservative failure fallback.
|
|
4402
4941
|
const replyRejection = (prose) => proseRejection(prose, liveSessionIdentifiers(), liveStateIdentifiers(), suppliedIdentifiers());
|
|
4942
|
+
// CAPTAIN-74: whether a recorded reason may be quoted outside a code span —
|
|
4943
|
+
// what the renderer asks before it quotes one as prose.
|
|
4944
|
+
const speakableWords = (words) => replyRejection(words) === undefined;
|
|
4945
|
+
// CAPTAIN-69: the guard every statement the shell says passes on its own —
|
|
4946
|
+
// CAPTAIN-9 outside its code spans, and inside one only its identifiers —
|
|
4947
|
+
// with the reasons the renderer gave that turn and the shell words it may
|
|
4948
|
+
// say neutrally.
|
|
4949
|
+
const statementGuard = (text, turn) => guardedStatement(text, (content) => identifierRejection(content, liveSessionIdentifiers(), liveStateIdentifiers(), suppliedIdentifiers()) !== undefined, (words) => replyRejection(words) !== undefined, turn.statedReasons, turn.shellWords);
|
|
4950
|
+
/** A reason the renderer gave a statement this turn, noted for the assembly. */
|
|
4951
|
+
const noteReason = (reason) => {
|
|
4952
|
+
if (reason !== undefined) {
|
|
4953
|
+
activeTurn?.statedReasons.add(reason.kind === 'phrase' ? reason.text : codeSpan(reason.text));
|
|
4954
|
+
}
|
|
4955
|
+
return reason;
|
|
4956
|
+
};
|
|
4957
|
+
/**
|
|
4958
|
+
* CAPTAIN-69: the turn's shell-said statements — the facts a reply lists,
|
|
4959
|
+
* the unresolved-effect entries, and the `Failure:` line — each said once.
|
|
4960
|
+
*/
|
|
4961
|
+
const assembleTurnReport = (turn, facts) => {
|
|
4962
|
+
const effects = turn.presentationReports.flatMap((report) => report.kind === 'effects' ? report.lines : []);
|
|
4963
|
+
const failure = turn.failureReport;
|
|
4964
|
+
const said = assembleStatements([
|
|
4965
|
+
...facts.map((text) => ({ text, kind: 'fact' })),
|
|
4966
|
+
...effects.map((text) => ({ text, kind: 'entry' })),
|
|
4967
|
+
...(failure === undefined
|
|
4968
|
+
? []
|
|
4969
|
+
: [
|
|
4970
|
+
{
|
|
4971
|
+
text: `Failure: ${failure.sentence}`,
|
|
4972
|
+
kind: 'failure',
|
|
4973
|
+
reason: failure.reason,
|
|
4974
|
+
},
|
|
4975
|
+
]),
|
|
4976
|
+
], turn.statedReasons, (text) => statementGuard(text, turn));
|
|
4977
|
+
const kept = (from, to) => said
|
|
4978
|
+
.slice(from, to)
|
|
4979
|
+
.filter((statement) => statement !== undefined);
|
|
4980
|
+
const listed = facts.length + effects.length;
|
|
4981
|
+
return {
|
|
4982
|
+
facts: kept(0, facts.length),
|
|
4983
|
+
effects: kept(facts.length, listed),
|
|
4984
|
+
...(failure === undefined || said[listed] === undefined
|
|
4985
|
+
? {}
|
|
4986
|
+
: { failure: said[listed] }),
|
|
4987
|
+
};
|
|
4988
|
+
};
|
|
4403
4989
|
// -------------------------------------------------------------------------
|
|
4404
4990
|
// The durable conversation (CAPTAIN-31, CAPTAIN-35).
|
|
4405
4991
|
// -------------------------------------------------------------------------
|
|
@@ -4627,7 +5213,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4627
5213
|
// Captain speech (DR-029): all durable calls are hidden; the shell
|
|
4628
5214
|
// validates the returned prose and surfaces it through `emitReply`.
|
|
4629
5215
|
const surfaceProse = async (context, outcome, compose) => {
|
|
4630
|
-
let text = outcome.finalText;
|
|
5216
|
+
let text = respondEnvelopeText(outcome.finalText);
|
|
4631
5217
|
const rejection = replyRejection(text);
|
|
4632
5218
|
if (rejection !== undefined) {
|
|
4633
5219
|
// DR-028 §26: the reseed already was this call's single corrective, so a
|
|
@@ -4637,7 +5223,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4637
5223
|
}
|
|
4638
5224
|
// DR-028's single corrective re-ask on the same durable conversation.
|
|
4639
5225
|
const reasked = await durableCall(context, (options) => compose({ ...options, proseRejection: rejection }));
|
|
4640
|
-
text = reasked.finalText;
|
|
5226
|
+
text = respondEnvelopeText(reasked.finalText);
|
|
4641
5227
|
const second = replyRejection(text);
|
|
4642
5228
|
if (second !== undefined) {
|
|
4643
5229
|
throw markControlFailure(new CaptainProseError(second));
|
|
@@ -4720,8 +5306,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4720
5306
|
// Through the one presentation seam, exactly where a model-composed
|
|
4721
5307
|
// reply is presented, so this reply is journaled and single-attempt
|
|
4722
5308
|
// like every other. It needs no reply validation or corrective re-ask:
|
|
4723
|
-
// `
|
|
4724
|
-
await surfaceSettlement({ context,
|
|
5309
|
+
// every statement `giveUpReply` lists passed CAPTAIN-69's guard.
|
|
5310
|
+
await surfaceSettlement({ context, ...giveUpReply() });
|
|
4725
5311
|
return { status: 'ok', finalText: 'ok' };
|
|
4726
5312
|
}
|
|
4727
5313
|
const outcome = await durableCall(context, compose);
|
|
@@ -4881,7 +5467,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4881
5467
|
const summary = leafStateSummary();
|
|
4882
5468
|
const settlement = {
|
|
4883
5469
|
status: 'rejected',
|
|
4884
|
-
facts: [`Rejected: ${reason}
|
|
5470
|
+
facts: [`Rejected: ${oneTerminalMark(reason)}`],
|
|
4885
5471
|
reason,
|
|
4886
5472
|
...(summary === undefined ? {} : { leafStateSummary: summary }),
|
|
4887
5473
|
};
|
|
@@ -4945,11 +5531,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4945
5531
|
}
|
|
4946
5532
|
catch (error) {
|
|
4947
5533
|
// A failing dispose never resurrects the engagement (DR-029).
|
|
4948
|
-
|
|
4949
|
-
name: 'Error',
|
|
4950
|
-
message: String(error),
|
|
4951
|
-
};
|
|
4952
|
-
facts.push(`Dismissed the ${label} engagement; its disposal failed: ${normalized.name}: ${compactEvidence(normalized.message)}.`);
|
|
5534
|
+
facts.push(stateFailure(`dismissing ${label}`, failureOf(error)));
|
|
4953
5535
|
return true;
|
|
4954
5536
|
}
|
|
4955
5537
|
};
|
|
@@ -4959,11 +5541,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4959
5541
|
frame = await runEffect(() => engage(entry));
|
|
4960
5542
|
}
|
|
4961
5543
|
catch (error) {
|
|
4962
|
-
|
|
4963
|
-
name: 'Error',
|
|
4964
|
-
message: String(error),
|
|
4965
|
-
};
|
|
4966
|
-
facts.push(`Starting /${enablementById.get(entry.id).command} failed: ${normalized.name}: ${compactEvidence(normalized.message)}.`);
|
|
5544
|
+
facts.push(stateFailure(`starting /${enablementById.get(entry.id).command}`, failureOf(error)));
|
|
4967
5545
|
return { report: emptyReport(), failed: true };
|
|
4968
5546
|
}
|
|
4969
5547
|
facts.push(`Started ${frameLabel(frame)} with the selected request.`);
|
|
@@ -4975,11 +5553,17 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4975
5553
|
// composition error, never a settled Boss outcome.
|
|
4976
5554
|
if (outcome.error instanceof VisibilityControlError)
|
|
4977
5555
|
throw outcome.error;
|
|
4978
|
-
const
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
facts.push(
|
|
5556
|
+
const failure = failureOf(outcome.error);
|
|
5557
|
+
const subject = `the first turn of ${frameLabel(frame)}`;
|
|
5558
|
+
const restate = (recorded) => stateFailure(subject, recorded);
|
|
5559
|
+
const fact = restate(failure);
|
|
5560
|
+
facts.push(fact);
|
|
5561
|
+
activeTurn?.failureStatements.push({
|
|
5562
|
+
frame,
|
|
5563
|
+
fact,
|
|
5564
|
+
message: failure.message,
|
|
5565
|
+
restate,
|
|
5566
|
+
});
|
|
4983
5567
|
return { frame, report: outcome.report, failed: true };
|
|
4984
5568
|
}
|
|
4985
5569
|
return { frame, report: outcome.report, failed: false };
|
|
@@ -5055,18 +5639,21 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5055
5639
|
await requestVisibility(adoptedFrames.at(-1));
|
|
5056
5640
|
}
|
|
5057
5641
|
await requireSession().emitStatus(`◇ /${enablementById.get(rootPlaybookId).command} resumed`);
|
|
5642
|
+
// The warning is both a settlement fact and a report the reply carries;
|
|
5643
|
+
// a reply that lists the fact need not say it twice (CAPTAIN-69).
|
|
5644
|
+
const warning = requiresEffectReconciliation
|
|
5645
|
+
? 'The retained work remains parked until its repository-effect evidence is reconciled; no ordinary action was resumed.'
|
|
5646
|
+
: RESUMPTION_DUPLICATE_EFFECT_WARNING;
|
|
5058
5647
|
if (activeTurn) {
|
|
5059
5648
|
appendMandatoryPresentationSuffix(activeTurn, requiresEffectReconciliation
|
|
5060
5649
|
? 'The retained work remains parked until its repository-effect evidence is reconciled.'
|
|
5061
|
-
: RESUMPTION_DUPLICATE_EFFECT_WARNING);
|
|
5650
|
+
: RESUMPTION_DUPLICATE_EFFECT_WARNING, [warning]);
|
|
5062
5651
|
}
|
|
5063
5652
|
return [
|
|
5064
5653
|
generation.rootStateDescription === undefined
|
|
5065
5654
|
? `Resumed /${enablementById.get(rootPlaybookId).command} from its retained state; no published root-state description was retained.`
|
|
5066
5655
|
: `Resumed /${enablementById.get(rootPlaybookId).command} from the retained state described as ${quoteEvidence(compactEvidence(generation.rootStateDescription))}.`,
|
|
5067
|
-
|
|
5068
|
-
? 'The retained work remains parked until its repository-effect evidence is reconciled; no ordinary action was resumed.'
|
|
5069
|
-
: RESUMPTION_DUPLICATE_EFFECT_WARNING,
|
|
5656
|
+
warning,
|
|
5070
5657
|
];
|
|
5071
5658
|
}
|
|
5072
5659
|
catch (error) {
|
|
@@ -5202,6 +5789,14 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5202
5789
|
const enablement = enablementById.get(playbookId);
|
|
5203
5790
|
return enablement === undefined ? playbookId : `/${enablement.command}`;
|
|
5204
5791
|
};
|
|
5792
|
+
/**
|
|
5793
|
+
* CAPTAIN-71: a failure statement naming nested playbooks by command, its
|
|
5794
|
+
* subject said as `the step` where CAPTAIN-9 refuses it (CAPTAIN-69).
|
|
5795
|
+
*/
|
|
5796
|
+
const stateFailure = (subject, failure) => {
|
|
5797
|
+
activeTurn?.shellWords.subjects.add(sentenceCase(subject));
|
|
5798
|
+
return sentenceCase(failureClause(subject, noteReason(failureReason(failure, describeNestedPlaybook, speakableWords))));
|
|
5799
|
+
};
|
|
5205
5800
|
// DR-063 §4: appended once, beside the unresolved-effect and pre-existing
|
|
5206
5801
|
// change reports, whenever the turn settles with the leaf parked in its
|
|
5207
5802
|
// failure state or behind the retained-effect fence.
|
|
@@ -5227,21 +5822,26 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5227
5822
|
if (cause === undefined)
|
|
5228
5823
|
return;
|
|
5229
5824
|
turn.failureReported = true;
|
|
5230
|
-
const
|
|
5231
|
-
|
|
5232
|
-
//
|
|
5233
|
-
//
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5825
|
+
const failure = { cause };
|
|
5826
|
+
// One failure, one fact: where a fact this turn already stated the leaf's
|
|
5827
|
+
// parked failure — its failed run result, or a runtime action's failed
|
|
5828
|
+
// receipt or a thrown first turn, delivery, or runtime action whose error
|
|
5829
|
+
// is the one the leaf records — the cause restates that fact in its place
|
|
5830
|
+
// rather than joining beside it.
|
|
5831
|
+
const recorded = (view?.lastError ?? lastError)?.message;
|
|
5832
|
+
const statement = turn.failureStatements.find(({ frame, message }) => frame === leaf &&
|
|
5833
|
+
(message === undefined ||
|
|
5834
|
+
(recorded !== undefined &&
|
|
5835
|
+
compactEvidence(message) === compactEvidence(recorded))));
|
|
5836
|
+
const fact = statement === undefined
|
|
5837
|
+
? stateFailure(frameLabel(leaf), failure)
|
|
5838
|
+
: statement.restate(failure);
|
|
5839
|
+
const speak = statement?.speak ?? ((text) => text);
|
|
5840
|
+
turn.failureReport = {
|
|
5841
|
+
...failureLine(noteReason(failureReason(failure, describeNestedPlaybook, speakableWords))),
|
|
5842
|
+
controls: failureControlLines(view, fenced),
|
|
5843
|
+
};
|
|
5844
|
+
joinSettlementFact(turn, fact, statement?.fact, speak);
|
|
5245
5845
|
};
|
|
5246
5846
|
const settleSelection = async (selection, signal) => {
|
|
5247
5847
|
const turn = activeTurn;
|
|
@@ -5249,9 +5849,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5249
5849
|
let frozenUnresolvedEffects;
|
|
5250
5850
|
const freezeControllerEvidence = () => {
|
|
5251
5851
|
frozenUnresolvedEffects ??= freezeTurnUnresolvedEffects();
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
appendMandatoryPresentationSuffix(turn, report);
|
|
5852
|
+
if (turn !== undefined) {
|
|
5853
|
+
appendUnresolvedEffectReport(turn, frozenUnresolvedEffects);
|
|
5255
5854
|
}
|
|
5256
5855
|
if (turn !== undefined)
|
|
5257
5856
|
reportCarriedPreExistingChanges(turn);
|
|
@@ -5277,10 +5876,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5277
5876
|
if (turn?.presentationError === error)
|
|
5278
5877
|
throw error;
|
|
5279
5878
|
const aborted = signal.aborted || activeContext?.signal.aborted === true;
|
|
5280
|
-
const
|
|
5281
|
-
name: 'Error',
|
|
5282
|
-
message: String(error),
|
|
5283
|
-
};
|
|
5879
|
+
const failure = failureOf(error);
|
|
5284
5880
|
if (aborted) {
|
|
5285
5881
|
markConversationUnsynchronized();
|
|
5286
5882
|
if (turn?.outcomePending) {
|
|
@@ -5296,9 +5892,24 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5296
5892
|
}
|
|
5297
5893
|
const mayHaveApplied = selection.action !== 'respond' &&
|
|
5298
5894
|
containsEffectThrow(turn, error);
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5895
|
+
const subject = `the ${selection.action} action`;
|
|
5896
|
+
const unconfirmed = mayHaveApplied
|
|
5897
|
+
? 'Its complete outcome could not be confirmed, and it may have changed the session. It was not repeated automatically.'
|
|
5898
|
+
: 'Its complete outcome could not be confirmed. It was not repeated automatically.';
|
|
5899
|
+
const restate = (recorded) => `${stateFailure(subject, recorded)} ${unconfirmed}`;
|
|
5900
|
+
const fact = restate(failure);
|
|
5901
|
+
turn.settlementFacts.push(fact);
|
|
5902
|
+
// A delivery or runtime action acts on the leaf, so the leaf's parked
|
|
5903
|
+
// failure may be this very error (CAPTAIN-67).
|
|
5904
|
+
const failedLeaf = leafFrame();
|
|
5905
|
+
if (failedLeaf !== undefined) {
|
|
5906
|
+
turn.failureStatements.push({
|
|
5907
|
+
frame: failedLeaf,
|
|
5908
|
+
fact,
|
|
5909
|
+
message: failure.message,
|
|
5910
|
+
restate,
|
|
5911
|
+
});
|
|
5912
|
+
}
|
|
5302
5913
|
if (!turn.outcomePending && !turn.outcomeRecorded) {
|
|
5303
5914
|
journalAction({
|
|
5304
5915
|
action: selection.action,
|
|
@@ -5375,11 +5986,12 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5375
5986
|
journalAction({ action: 'respond' });
|
|
5376
5987
|
const reask = decisionCall;
|
|
5377
5988
|
if (reask === undefined) {
|
|
5378
|
-
const
|
|
5989
|
+
const text = respondEnvelopeText(selection.text);
|
|
5990
|
+
const rejection = replyRejection(text);
|
|
5379
5991
|
if (rejection !== undefined) {
|
|
5380
5992
|
throw markControlFailure(new CaptainProseError(rejection));
|
|
5381
5993
|
}
|
|
5382
|
-
await surfaceSettlement({ context, text:
|
|
5994
|
+
await surfaceSettlement({ context, text: text });
|
|
5383
5995
|
}
|
|
5384
5996
|
else {
|
|
5385
5997
|
await surfaceProse(context, { ...reask.outcome, finalText: selection.text }, reask.compose);
|
|
@@ -5630,11 +6242,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5630
6242
|
.actions.find((action) => action.id === actionId);
|
|
5631
6243
|
}
|
|
5632
6244
|
catch (error) {
|
|
5633
|
-
|
|
5634
|
-
name: 'Error',
|
|
5635
|
-
message: String(error),
|
|
5636
|
-
};
|
|
5637
|
-
return rejectSelection(selection, `${frameLabel(leaf)} could not be asked which actions it offers: ${normalized.name}: ${compactEvidence(normalized.message)}`);
|
|
6245
|
+
return rejectSelection(selection, stateFailure(`asking ${frameLabel(leaf)} which actions it offers`, failureOf(error)));
|
|
5638
6246
|
}
|
|
5639
6247
|
if (advertised === undefined) {
|
|
5640
6248
|
// CAPPLAY-5: the chosen id is control data whether or not the leaf
|
|
@@ -5644,6 +6252,16 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5644
6252
|
return rejectSelection(selection, `${frameLabel(leaf)} does not advertise that action`);
|
|
5645
6253
|
}
|
|
5646
6254
|
const actionLabel = advertised.label;
|
|
6255
|
+
// The settlement facts are shell-internal strings composed for the hidden
|
|
6256
|
+
// result-phase prompt: they name the action by its id, which is control
|
|
6257
|
+
// data. The Boss-facing rendering of the same settlement names it by the
|
|
6258
|
+
// runtime's own Boss-appropriate label (PBRT-52), for the one place these
|
|
6259
|
+
// facts are spoken rather than prompted — the CAPTAIN-34 fallback.
|
|
6260
|
+
// A code span is the recorded text as recorded (CAPTAIN-74), so only the
|
|
6261
|
+
// words outside one are renamed.
|
|
6262
|
+
const spokenLabel = `"${compactEvidence(actionLabel)}"`;
|
|
6263
|
+
turn.shellWords.labels.add(spokenLabel);
|
|
6264
|
+
const speak = (fact) => outsideCodeSpans(fact, (words) => words.split(`"${actionId}"`).join(spokenLabel));
|
|
5647
6265
|
// The id the digest advertised and the reply selected by is now also the
|
|
5648
6266
|
// id this turn's outcome-report facts carry (CAPTAIN-9): recording it here
|
|
5649
6267
|
// keeps the closing-reply prompt's copy inside the same supplied set the
|
|
@@ -5708,9 +6326,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5708
6326
|
turn.report = {
|
|
5709
6327
|
...outcome.report,
|
|
5710
6328
|
facts: [...facts],
|
|
5711
|
-
bossFacts: facts.map(
|
|
5712
|
-
.split(`"${actionId}"`)
|
|
5713
|
-
.join(`"${compactEvidence(actionLabel)}"`)),
|
|
6329
|
+
bossFacts: facts.map(speak),
|
|
5714
6330
|
status: 'failed',
|
|
5715
6331
|
receipt: {
|
|
5716
6332
|
disposition: 'failed',
|
|
@@ -5730,9 +6346,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5730
6346
|
turn.report = {
|
|
5731
6347
|
...outcome.report,
|
|
5732
6348
|
facts: [...facts],
|
|
5733
|
-
bossFacts: facts.map(
|
|
5734
|
-
.split(`"${actionId}"`)
|
|
5735
|
-
.join(`"${compactEvidence(actionLabel)}"`)),
|
|
6349
|
+
bossFacts: facts.map(speak),
|
|
5736
6350
|
status: 'ok',
|
|
5737
6351
|
receipt: receiptEvidence,
|
|
5738
6352
|
...(establishedSummary === undefined
|
|
@@ -5752,20 +6366,27 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5752
6366
|
}
|
|
5753
6367
|
}
|
|
5754
6368
|
else if (receipt.disposition === 'rejected') {
|
|
5755
|
-
facts.push(`
|
|
6369
|
+
facts.push(refusalStatement(`"${actionId}"`, noteReason(recordedReason(receipt.reason, speakableWords))));
|
|
5756
6370
|
}
|
|
5757
6371
|
else {
|
|
5758
|
-
|
|
6372
|
+
// A failed receipt acts on the leaf, so the leaf's parked failure may
|
|
6373
|
+
// be this very error — a Retry whose re-run failed again (CAPTAIN-67).
|
|
6374
|
+
const subject = `applying "${actionId}"`;
|
|
6375
|
+
const restate = (failure) => stateFailure(subject, failure);
|
|
6376
|
+
const fact = restate(receipt.error);
|
|
6377
|
+
facts.push(fact);
|
|
6378
|
+
turn.failureStatements.push({
|
|
6379
|
+
frame: leaf,
|
|
6380
|
+
fact,
|
|
6381
|
+
message: receipt.error.message,
|
|
6382
|
+
restate,
|
|
6383
|
+
speak,
|
|
6384
|
+
});
|
|
5759
6385
|
}
|
|
5760
6386
|
const runFailed = drainRunFailureFacts(facts);
|
|
5761
6387
|
if (runFailed)
|
|
5762
6388
|
status = 'failed';
|
|
5763
|
-
|
|
5764
|
-
// hidden result-phase prompt: they name the action by its id, which is
|
|
5765
|
-
// control data. The Boss-facing rendering of the same settlement names it
|
|
5766
|
-
// by the runtime's own Boss-appropriate label (PBRT-52), for the one place
|
|
5767
|
-
// these facts are spoken rather than prompted — the CAPTAIN-34 fallback.
|
|
5768
|
-
const bossFacts = facts.map((fact) => fact.split(`"${actionId}"`).join(`"${compactEvidence(actionLabel)}"`));
|
|
6389
|
+
const bossFacts = facts.map(speak);
|
|
5769
6390
|
const summary = leafStateSummary();
|
|
5770
6391
|
turn.report = {
|
|
5771
6392
|
...outcome.report,
|
|
@@ -5802,14 +6423,15 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5802
6423
|
// `settled` guard. That guard closes duplicate submissions before an effect
|
|
5803
6424
|
// starts; it is not evidence that anything ran.
|
|
5804
6425
|
// CAPTAIN-63: the give-up's closing reply, composed from the same settlement
|
|
5805
|
-
// facts the result-phase prompt would have carried.
|
|
5806
|
-
//
|
|
5807
|
-
//
|
|
5808
|
-
//
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
const report =
|
|
5812
|
-
const
|
|
6426
|
+
// facts the result-phase prompt would have carried. What it lists is not
|
|
6427
|
+
// host-authored all the way down, so each statement passes CAPTAIN-69's
|
|
6428
|
+
// guard on its own and the reply still states the settlement truthfully;
|
|
6429
|
+
// no statement is ever left out.
|
|
6430
|
+
const giveUpReply = () => {
|
|
6431
|
+
const turn = activeTurn;
|
|
6432
|
+
const report = turn?.report;
|
|
6433
|
+
const stated = report?.bossFacts ?? report?.facts ?? [];
|
|
6434
|
+
const facts = turn === undefined ? [] : assembleTurnReport(turn, stated).facts;
|
|
5813
6435
|
const stopped = report?.status === 'ok';
|
|
5814
6436
|
const opening = stopped
|
|
5815
6437
|
? 'Stopped that workflow. I will not resume it.'
|
|
@@ -5817,27 +6439,29 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5817
6439
|
const closing = stopped
|
|
5818
6440
|
? 'Send the command again to start fresh work.'
|
|
5819
6441
|
: 'The stop did not settle successfully; recover the session before continuing.';
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
};
|
|
5831
|
-
const
|
|
6442
|
+
return {
|
|
6443
|
+
text: [
|
|
6444
|
+
opening,
|
|
6445
|
+
...(facts.length === 0
|
|
6446
|
+
? []
|
|
6447
|
+
: ['Here is what happened:', ...facts.map((fact) => `- ${fact}`)]),
|
|
6448
|
+
closing,
|
|
6449
|
+
].join('\n'),
|
|
6450
|
+
stated,
|
|
6451
|
+
};
|
|
6452
|
+
};
|
|
6453
|
+
const failureReply = () => {
|
|
5832
6454
|
const commands = [...enablementById.values()]
|
|
5833
6455
|
.map((enablement) => `/${enablement.command} <task>`)
|
|
5834
6456
|
.join(' or ');
|
|
5835
6457
|
const turn = activeTurn;
|
|
5836
6458
|
const report = turn?.report;
|
|
5837
6459
|
if (report === undefined) {
|
|
5838
|
-
return
|
|
5839
|
-
|
|
5840
|
-
|
|
6460
|
+
return {
|
|
6461
|
+
text: 'I could not finish deciding that turn. No action was selected or run — please send the request again' +
|
|
6462
|
+
(commands ? `, or start a playbook directly with ${commands}` : '') +
|
|
6463
|
+
'.',
|
|
6464
|
+
};
|
|
5841
6465
|
}
|
|
5842
6466
|
const settledPreamble = report.status === 'ok'
|
|
5843
6467
|
? 'I could not finish reporting that turn, but the reported action completed — please do not send it again.'
|
|
@@ -5849,25 +6473,26 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5849
6473
|
const closing = 'Ask me where things stand and I will report the current state.';
|
|
5850
6474
|
// The Boss-facing rendering of the settlement, never the prompt-side one:
|
|
5851
6475
|
// `facts` name actions by id and quote runtime-authored text nobody
|
|
5852
|
-
// validated.
|
|
5853
|
-
|
|
5854
|
-
const
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
? []
|
|
5858
|
-
: ['Here is what happened:', ...facts.map((fact) => `- ${fact}`)]),
|
|
5859
|
-
closing,
|
|
5860
|
-
].join('\n');
|
|
5861
|
-
// CAPTAIN-34: this reply is host-authored Boss prose and passes the same
|
|
5862
|
-
// validation every model reply passes. What it interpolates is not
|
|
6476
|
+
// validated. Each is said once, in Boss's words, beside the reports the
|
|
6477
|
+
// reply carries (CAPTAIN-69).
|
|
6478
|
+
const stated = report.bossFacts ?? report.facts;
|
|
6479
|
+
const facts = assembleTurnReport(turn, stated).facts;
|
|
6480
|
+
// CAPTAIN-34: this reply is host-authored Boss prose. What it lists is not
|
|
5863
6481
|
// host-authored all the way down — a refusal reason and a normalized error
|
|
5864
|
-
// message are foreign text — so
|
|
5865
|
-
//
|
|
5866
|
-
//
|
|
5867
|
-
// turn's only remaining settlement.
|
|
5868
|
-
return
|
|
5869
|
-
|
|
5870
|
-
|
|
6482
|
+
// message are foreign text — so each statement passes CAPTAIN-69's guard
|
|
6483
|
+
// on its own, the reply states the settlement truthfully and names the
|
|
6484
|
+
// next step, and no statement is left out for its own or another's words.
|
|
6485
|
+
// It is never withheld: it is the turn's only remaining settlement.
|
|
6486
|
+
return {
|
|
6487
|
+
text: [
|
|
6488
|
+
settledPreamble,
|
|
6489
|
+
...(facts.length === 0
|
|
6490
|
+
? []
|
|
6491
|
+
: ['Here is what happened:', ...facts.map((fact) => `- ${fact}`)]),
|
|
6492
|
+
closing,
|
|
6493
|
+
].join('\n'),
|
|
6494
|
+
stated,
|
|
6495
|
+
};
|
|
5871
6496
|
};
|
|
5872
6497
|
const settleTurnFailure = async (context, error) => {
|
|
5873
6498
|
if (context.signal.aborted)
|
|
@@ -5888,10 +6513,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5888
6513
|
// Through the one presentation seam, so this reply is journaled like
|
|
5889
6514
|
// every other Boss-visible Captain reply. A rejected emission propagates
|
|
5890
6515
|
// unchanged: it is never retried and never disguised as an action failure.
|
|
5891
|
-
await surfaceSettlement({
|
|
5892
|
-
context,
|
|
5893
|
-
text: failureReplyText(),
|
|
5894
|
-
});
|
|
6516
|
+
await surfaceSettlement({ context, ...failureReply() });
|
|
5895
6517
|
};
|
|
5896
6518
|
const enabledCatalog = () => Object.freeze(entries.map((entry) => Object.freeze({
|
|
5897
6519
|
id: entry.id,
|
|
@@ -6758,7 +7380,11 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
6758
7380
|
...(hostGiveUp === undefined ? {} : { hostGiveUp }),
|
|
6759
7381
|
settled: false,
|
|
6760
7382
|
presentationAttempted: false,
|
|
7383
|
+
presentationReports: [],
|
|
6761
7384
|
settlementFacts: [],
|
|
7385
|
+
failureStatements: [],
|
|
7386
|
+
statedReasons: new Set(),
|
|
7387
|
+
shellWords: { subjects: new Set(), labels: new Set() },
|
|
6762
7388
|
effectThrows: new Set(),
|
|
6763
7389
|
controlFailures: new Set(),
|
|
6764
7390
|
settingsPreflightFailures: new Set(),
|
|
@@ -6901,4 +7527,13 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
6901
7527
|
throw failure;
|
|
6902
7528
|
}
|
|
6903
7529
|
}
|
|
7530
|
+
// The Boss-facing failure renderer, report assembly, and minted-reason tables,
|
|
7531
|
+
// for the shell suite's direct cases; the leading underscore keeps them out of
|
|
7532
|
+
// the stable surface.
|
|
7533
|
+
export const _internal = {
|
|
7534
|
+
failureStatement,
|
|
7535
|
+
bossReport,
|
|
7536
|
+
RUNTIME_REASON_PHRASES,
|
|
7537
|
+
LABELLED_RUNTIME_REASONS,
|
|
7538
|
+
};
|
|
6904
7539
|
export default createPlaybookCaptainShell;
|