@sublang/playbook 13.3.0 → 14.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/docs/cli.md +14 -14
- package/docs/configuration.md +10 -2
- package/docs/embedding.md +20 -1
- package/package.json +6 -1
- package/reference/sdlc/branch.playbook/branch.fsm.d.ts +1 -1
- package/reference/sdlc/code.playbook/bin/launch-config.js +88 -5
- package/reference/sdlc/code.playbook/bin/playbook.js +4 -0
- package/reference/sdlc/code.playbook/bin/repository-effects.js +198 -17
- package/reference/sdlc/code.playbook/bin/run.js +4 -0
- package/reference/sdlc/code.playbook/bin/session-store.js +2 -2
- package/reference/sdlc/code.playbook/code.fsm.d.ts +1 -1
- package/reference/sdlc/code.playbook/host-capabilities.d.ts +6 -0
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +3 -1
- package/reference/sdlc/code.playbook/playbook-captain.js +374 -20
- package/reference/sdlc/code.playbook/playbook-captain.ts +478 -38
- package/reference/sdlc/code.playbook/session-host.d.ts +6 -3
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +1 -1
- package/reference/sdlc/decide.playbook/decide.playbook.js +25 -1
- package/reference/sdlc/decide.playbook/decide.playbook.ts +35 -2
- package/reference/sdlc/dev.md +15 -8
- package/reference/sdlc/dev.playbook/dev.fsm.d.ts +1 -1
- package/reference/sdlc/dev.playbook/dev.fsm.js +18 -12
- package/reference/sdlc/dev.playbook/dev.fsm.ts +18 -13
- package/reference/sdlc/dev.playbook/dev.gears.md +19 -12
- package/reference/sdlc/dev.playbook/dev.playbook.d.ts +3 -10
- package/reference/sdlc/dev.playbook/dev.playbook.js +6 -6
- package/reference/sdlc/dev.playbook/dev.playbook.ts +8 -9
- package/reference/sdlc/dev.playbook/dev.registry.js +1 -1
- package/reference/sdlc/dev.playbook/dev.registry.ts +1 -1
- package/reference/sdlc/pr.md +17 -4
- package/reference/sdlc/pr.playbook/pr.fsm.d.ts +18 -4
- package/reference/sdlc/pr.playbook/pr.fsm.js +49 -18
- package/reference/sdlc/pr.playbook/pr.fsm.ts +63 -20
- package/reference/sdlc/pr.playbook/pr.gears.md +20 -7
- package/reference/sdlc/pr.playbook/pr.playbook.js +2 -1
- package/reference/sdlc/pr.playbook/pr.playbook.ts +2 -1
- package/reference/sdlc/review.playbook/review.fsm.d.ts +1 -1
- package/slc/experiments/fsm-scaffold-guidance.md +31 -0
- package/slc/gears2fsm.md +145 -58
- package/slc/link.md +190 -7
- package/slc/materialize-link.mjs +460 -0
- package/slc/optimize.md +46 -6
- package/slc/scaffold-fsm.mjs +225 -0
- package/slc/slc.pin-inputs.json +13 -0
- package/slc/text2gears.md +38 -3
- package/slc/workflow-contracts.json +371 -0
- package/src/runtime.d.ts +70 -0
- package/src/runtime.js +232 -1
- package/src/runtime.ts +358 -1
- package/src/xstate-playbook-runtime.d.ts +9 -10
- package/src/xstate-playbook-runtime.js +407 -21
- package/src/xstate-playbook-runtime.ts +514 -20
- package/src/xstate-runtime.d.ts +9 -1
- package/src/xstate-runtime.js +299 -19
- package/src/xstate-runtime.ts +362 -21
|
@@ -4,6 +4,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { isDeepStrictEqual } from 'node:util';
|
|
5
5
|
import PQueue from 'p-queue';
|
|
6
6
|
import { isAgentCallSettingsError, } from '@sublang/cligent/tmux-play';
|
|
7
|
+
import { assertPlaybookFailureCause } from '@sublang/playbook/runtime';
|
|
7
8
|
import { assertPlaybookRuntimeSnapshot, assertPlaybookEffectLedger, emptyPlaybookEffectLedger, hiddenControlEnvelope, isPlaybookEffectLedgerMonotonicExtension, registerPlaybookAbortCleanup, snapshotJsonValue, validatePlayerResult, } from '../../../src/xstate-runtime.js';
|
|
8
9
|
import createDefaultCaptainRuntime from '../captain.playbook/captain.playbook.js';
|
|
9
10
|
function retainedEffectLedgerCanRebase(checkpoint, current) {
|
|
@@ -476,6 +477,160 @@ function unresolvedEffectBossReport(unresolvedEffects) {
|
|
|
476
477
|
'This evidence does not establish workflow completion or attribute any repository change or commit to this workflow.',
|
|
477
478
|
].join('\n');
|
|
478
479
|
}
|
|
480
|
+
// DR-062 §5: a settled turn whose accepted commit absorbed or altered the
|
|
481
|
+
// Boss's own uncommitted changes says so, bounded to paths.
|
|
482
|
+
const CARRIED_CHANGE_PATH_LIMIT = 24;
|
|
483
|
+
function boundedPathList(paths) {
|
|
484
|
+
const printed = paths.slice(0, CARRIED_CHANGE_PATH_LIMIT);
|
|
485
|
+
const omitted = paths.length - printed.length;
|
|
486
|
+
return omitted > 0
|
|
487
|
+
? `${printed.join(', ')}, … and ${omitted} more`
|
|
488
|
+
: printed.join(', ');
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* The carried pre-existing changes of the boundaries completed after
|
|
492
|
+
* `boundaryPrefixCount`, one entry per commit OID in boundary sequence order.
|
|
493
|
+
*/
|
|
494
|
+
function carriedPreExistingChanges(ledger, boundaryPrefixCount) {
|
|
495
|
+
const byCommit = new Map();
|
|
496
|
+
for (const boundary of ledger.boundaries) {
|
|
497
|
+
if (boundary.sequence <= boundaryPrefixCount)
|
|
498
|
+
continue;
|
|
499
|
+
const receipt = boundary.physicalReceipt;
|
|
500
|
+
if (receipt?.classification !== 'one-descendant-commit' ||
|
|
501
|
+
receipt.commitOid === undefined ||
|
|
502
|
+
receipt.preExisting === undefined) {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
const { absorbed, altered } = receipt.preExisting;
|
|
506
|
+
if (absorbed.length === 0 && altered.length === 0)
|
|
507
|
+
continue;
|
|
508
|
+
const carried = byCommit.get(receipt.commitOid) ?? {
|
|
509
|
+
absorbed: [],
|
|
510
|
+
altered: [],
|
|
511
|
+
};
|
|
512
|
+
carried.absorbed.push(...absorbed);
|
|
513
|
+
carried.altered.push(...altered);
|
|
514
|
+
byCommit.set(receipt.commitOid, carried);
|
|
515
|
+
}
|
|
516
|
+
return [...byCommit].map(([commitOid, { absorbed, altered }]) => ({
|
|
517
|
+
commitOid,
|
|
518
|
+
absorbed: [...new Set(absorbed)].sort(),
|
|
519
|
+
altered: [...new Set(altered)].sort(),
|
|
520
|
+
}));
|
|
521
|
+
}
|
|
522
|
+
function carriedPreExistingReport(carried) {
|
|
523
|
+
if (carried.length === 0)
|
|
524
|
+
return undefined;
|
|
525
|
+
return carried
|
|
526
|
+
.map(({ commitOid, absorbed, altered }) => [
|
|
527
|
+
`Pre-existing changes carried by commit ${commitOid}:`,
|
|
528
|
+
...(absorbed.length === 0
|
|
529
|
+
? []
|
|
530
|
+
: [`- absorbed as found: ${boundedPathList(absorbed)}`]),
|
|
531
|
+
...(altered.length === 0
|
|
532
|
+
? []
|
|
533
|
+
: [`- altered before committing: ${boundedPathList(altered)}`]),
|
|
534
|
+
'These changes were uncommitted before the step.',
|
|
535
|
+
].join('\n'))
|
|
536
|
+
.join('\n\n');
|
|
537
|
+
}
|
|
538
|
+
function carriedPreExistingFacts(carried) {
|
|
539
|
+
return carried.map(({ commitOid, absorbed, altered }) => {
|
|
540
|
+
const paths = [...new Set([...absorbed, ...altered])].sort();
|
|
541
|
+
return `The commit ${commitOid} carries ${paths.length} changes that were uncommitted before the step: ${boundedPathList(paths)}.`;
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
// DR-063 §4: the Boss-visible failure report. Every host sees the same
|
|
545
|
+
// sentences because the shell composes them from the structured cause instead
|
|
546
|
+
// of asking a model to describe facts it may drop. The report exposes no file
|
|
547
|
+
// content, player prose, or internal identity: paths, revisions, dispositions,
|
|
548
|
+
// and the reported error message are what a cause carries.
|
|
549
|
+
/** The Boss-facing phrase for each closed control-action reason (DR-063 §3). */
|
|
550
|
+
const CONTROL_REASON_PHRASES = {
|
|
551
|
+
'receipt-complete': 'nothing has changed since it failed',
|
|
552
|
+
};
|
|
553
|
+
function controlReasonPhrase(reason) {
|
|
554
|
+
if (reason === undefined)
|
|
555
|
+
return 'no reason was published';
|
|
556
|
+
return CONTROL_REASON_PHRASES[reason] ?? reason;
|
|
557
|
+
}
|
|
558
|
+
function causePathList(paths, truncated) {
|
|
559
|
+
const list = paths ?? [];
|
|
560
|
+
const printed = list.slice(0, CARRIED_CHANGE_PATH_LIMIT);
|
|
561
|
+
const omitted = list.length - printed.length + (truncated ?? 0);
|
|
562
|
+
return omitted > 0
|
|
563
|
+
? `${printed.join(', ')}, … and ${omitted} more`
|
|
564
|
+
: printed.join(', ');
|
|
565
|
+
}
|
|
566
|
+
/** One sentence per code, with that code's bounded evidence (DR-063 §4). */
|
|
567
|
+
function failureCauseSentence(cause, describePlaybook) {
|
|
568
|
+
const evidence = cause.evidence;
|
|
569
|
+
const paths = evidence.paths;
|
|
570
|
+
const code = cause.code;
|
|
571
|
+
switch (code) {
|
|
572
|
+
case 'commit-missing':
|
|
573
|
+
return (paths?.uncommitted ?? []).length === 0
|
|
574
|
+
? '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)}.`;
|
|
576
|
+
case 'commit-residual': {
|
|
577
|
+
const subject = evidence.commitOid === undefined
|
|
578
|
+
? "the step's commit"
|
|
579
|
+
: `the commit ${evidence.commitOid}`;
|
|
580
|
+
if ((paths?.uncommitted ?? []).length > 0) {
|
|
581
|
+
return `${subject} left changes uncommitted: ${causePathList(paths?.uncommitted, paths?.truncated)}.`;
|
|
582
|
+
}
|
|
583
|
+
if ((paths?.altered ?? []).length > 0) {
|
|
584
|
+
return `${subject} left altered changes uncommitted: ${causePathList(paths?.altered, paths?.truncated)}.`;
|
|
585
|
+
}
|
|
586
|
+
return `${subject} left changes uncommitted beside it.`;
|
|
587
|
+
}
|
|
588
|
+
case 'pre-existing-lost':
|
|
589
|
+
return `uncommitted changes you had were lost: ${causePathList(paths?.lost, paths?.truncated)}.`;
|
|
590
|
+
case 'commits-more-than-one':
|
|
591
|
+
return `the step made more than one commit; HEAD moved from ${evidence.baselineHead} to ${evidence.afterHead}.`;
|
|
592
|
+
case 'history-rewritten':
|
|
593
|
+
return `the repository history was rewritten; HEAD moved from ${evidence.baselineHead} to ${evidence.afterHead}.`;
|
|
594
|
+
case 'foreign-change':
|
|
595
|
+
return `the repository changed outside this step: ${causePathList(paths?.changed, paths?.truncated)}.`;
|
|
596
|
+
case 'observation-unstable':
|
|
597
|
+
return `the repository could not be observed after the step; it stood at ${evidence.baselineHead} before it.`;
|
|
598
|
+
case 'attribution-ambiguous':
|
|
599
|
+
return `the repository change could not be attributed to this step; it required ${evidence.required} and observed ${evidence.observed}.`;
|
|
600
|
+
case 'receipt-missing':
|
|
601
|
+
return `a step left no complete repository receipt; the repository stood at ${evidence.baselineHead} before it.`;
|
|
602
|
+
case 'judge-failed':
|
|
603
|
+
return evidence.error === undefined
|
|
604
|
+
? `the hidden adjudication failed: ${evidence.reason}.`
|
|
605
|
+
: `the hidden adjudication failed: ${evidence.reason} (${evidence.error.name}: ${compactEvidence(evidence.error.message)}).`;
|
|
606
|
+
case 'player-failed':
|
|
607
|
+
return `the ${evidence.roleId} call failed: ${compactEvidence(evidence.error?.message ?? 'no detail was reported')}.`;
|
|
608
|
+
case 'aborted':
|
|
609
|
+
return 'the turn was aborted before the work settled.';
|
|
610
|
+
case 'child-failed': {
|
|
611
|
+
const nested = describePlaybook(evidence.playbookId ?? 'nested playbook');
|
|
612
|
+
return evidence.cause === undefined
|
|
613
|
+
? `the nested ${nested} run failed.`
|
|
614
|
+
: `the nested ${nested} run failed: ${failureCauseSentence(evidence.cause, describePlaybook)}`;
|
|
615
|
+
}
|
|
616
|
+
case 'runtime-defect':
|
|
617
|
+
return `the runtime could not settle the step: ${compactEvidence(evidence.reason ?? 'no reason was recorded')}.`;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function failureControlLine(action) {
|
|
621
|
+
// DR-063 §3: an action that publishes no standing is `ready`.
|
|
622
|
+
const standing = action.standing ?? 'ready';
|
|
623
|
+
return standing === 'ready'
|
|
624
|
+
? `- ${action.label} (ready)`
|
|
625
|
+
: `- ${action.label} (${standing}: ${controlReasonPhrase(action.reason)})`;
|
|
626
|
+
}
|
|
627
|
+
function failureReportText(sentence, controls) {
|
|
628
|
+
return [
|
|
629
|
+
`Failure: ${sentence}`,
|
|
630
|
+
'Controls:',
|
|
631
|
+
...(controls.length === 0 ? ['- none'] : controls),
|
|
632
|
+
].join('\n');
|
|
633
|
+
}
|
|
479
634
|
function appendMandatoryPresentationSuffix(turn, suffix) {
|
|
480
635
|
const current = turn.mandatoryPresentationSuffix;
|
|
481
636
|
if (current === undefined) {
|
|
@@ -1247,10 +1402,16 @@ export function assertPlaybookCaptainShellSnapshot(value) {
|
|
|
1247
1402
|
let normalizedLastError;
|
|
1248
1403
|
if (snapshot.lastError !== undefined) {
|
|
1249
1404
|
const error = snapshotRecord(snapshot.lastError, 'Captain shell snapshot.lastError');
|
|
1250
|
-
|
|
1405
|
+
// DR-063 §2: the cause enters the stored record inside `lastError`, so a
|
|
1406
|
+
// parked failure explains itself after a restart. An unrecognized shape is
|
|
1407
|
+
// not a cause and the closed validator refuses the snapshot.
|
|
1408
|
+
rejectSnapshotKeys(error, ['name', 'message', 'cause'], 'Captain shell snapshot.lastError');
|
|
1251
1409
|
normalizedLastError = {
|
|
1252
1410
|
name: snapshotString(error.name, 'Captain shell snapshot.lastError.name', true),
|
|
1253
1411
|
message: snapshotString(error.message, 'Captain shell snapshot.lastError.message', true),
|
|
1412
|
+
...(error.cause === undefined
|
|
1413
|
+
? {}
|
|
1414
|
+
: { cause: assertPlaybookFailureCause(error.cause) }),
|
|
1254
1415
|
};
|
|
1255
1416
|
}
|
|
1256
1417
|
const activePlaybooks = new Set();
|
|
@@ -1854,6 +2015,12 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1854
2015
|
return unresolvedEffectFromReceipt(receipt, original.head);
|
|
1855
2016
|
}
|
|
1856
2017
|
const sameProjection = isDeepStrictEqual(original.projection, after.projection);
|
|
2018
|
+
// DR-062 §3: a pure projection never downgrades a classification the
|
|
2019
|
+
// physical receipt proved. The latest receipt already accounted for every
|
|
2020
|
+
// pre-existing entry of its own baseline, so when that baseline is the
|
|
2021
|
+
// original one its accounting is the operation's accounting, and the
|
|
2022
|
+
// projection may not demand a byte-equal after projection on top of it.
|
|
2023
|
+
const receiptAccountsFromOriginal = isDeepStrictEqual(receipt.baseline.projection, original.projection);
|
|
1857
2024
|
if (after.head === original.head) {
|
|
1858
2025
|
if (receipt.classification !== 'unchanged' &&
|
|
1859
2026
|
receipt.classification !== 'worktree-only-change') {
|
|
@@ -1868,7 +2035,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1868
2035
|
const preservesOriginal = Object.entries(original.projection).every(([path, entry]) => Object.hasOwn(after.projection, path) &&
|
|
1869
2036
|
isDeepStrictEqual(entry, after.projection[path]));
|
|
1870
2037
|
return {
|
|
1871
|
-
classification: preservesOriginal
|
|
2038
|
+
classification: preservesOriginal ||
|
|
2039
|
+
(receipt.classification === 'worktree-only-change' &&
|
|
2040
|
+
receiptAccountsFromOriginal)
|
|
1872
2041
|
? 'worktree-only-change'
|
|
1873
2042
|
: 'observation-ambiguous',
|
|
1874
2043
|
baselineHead: original.head,
|
|
@@ -1876,7 +2045,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1876
2045
|
};
|
|
1877
2046
|
}
|
|
1878
2047
|
if (receipt.classification === 'one-descendant-commit' &&
|
|
1879
|
-
sameProjection) {
|
|
2048
|
+
(sameProjection || receiptAccountsFromOriginal)) {
|
|
1880
2049
|
return {
|
|
1881
2050
|
classification: 'one-descendant-commit',
|
|
1882
2051
|
baselineHead: original.head,
|
|
@@ -1989,6 +2158,44 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1989
2158
|
settledTurnUnresolvedEffects = frozen;
|
|
1990
2159
|
return frozen;
|
|
1991
2160
|
};
|
|
2161
|
+
/**
|
|
2162
|
+
* DR-062 §5: one deterministic Boss-visible report, once per turn, for the
|
|
2163
|
+
* pre-existing changes the commits completed during this turn carried, and
|
|
2164
|
+
* the same information as settlement facts the closing-reply prompt reads.
|
|
2165
|
+
* It supplements the unresolved-effect report and enters no run result.
|
|
2166
|
+
*/
|
|
2167
|
+
const reportCarriedPreExistingChanges = (turn) => {
|
|
2168
|
+
if (turn.carriedPreExistingReported)
|
|
2169
|
+
return;
|
|
2170
|
+
if (turn.effectBoundaryPrefixCount === undefined)
|
|
2171
|
+
return;
|
|
2172
|
+
let carried;
|
|
2173
|
+
try {
|
|
2174
|
+
carried = carriedPreExistingChanges(assertPlaybookEffectLedger(currentEffectLedger()), turn.effectBoundaryPrefixCount);
|
|
2175
|
+
}
|
|
2176
|
+
catch {
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
const report = carriedPreExistingReport(carried);
|
|
2180
|
+
if (report === undefined)
|
|
2181
|
+
return;
|
|
2182
|
+
turn.carriedPreExistingReported = true;
|
|
2183
|
+
appendMandatoryPresentationSuffix(turn, report);
|
|
2184
|
+
const facts = carriedPreExistingFacts(carried);
|
|
2185
|
+
turn.settlementFacts.push(...facts);
|
|
2186
|
+
// The action's own report was assembled from the facts as they stood when
|
|
2187
|
+
// it settled; the result-phase prompt reads this report, so the same facts
|
|
2188
|
+
// join it here rather than only the turn's running list.
|
|
2189
|
+
if (turn.report !== undefined) {
|
|
2190
|
+
turn.report = {
|
|
2191
|
+
...turn.report,
|
|
2192
|
+
facts: [...turn.report.facts, ...facts],
|
|
2193
|
+
...(turn.report.bossFacts === undefined
|
|
2194
|
+
? {}
|
|
2195
|
+
: { bossFacts: [...turn.report.bossFacts, ...facts] }),
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
};
|
|
1992
2199
|
const normalizeInstalledRetainedGenerations = (value) => {
|
|
1993
2200
|
const path = 'Captain retained generations';
|
|
1994
2201
|
const detached = snapshotJsonValue(value, path);
|
|
@@ -3321,6 +3528,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3321
3528
|
* may return an executed receipt to its result phase.
|
|
3322
3529
|
*/
|
|
3323
3530
|
const settleUnresolvedEffectAbandonment = async (unresolvedLeaf) => {
|
|
3531
|
+
abandonmentSettlementUnsafe = true;
|
|
3324
3532
|
if (leafFrame() !== unresolvedLeaf) {
|
|
3325
3533
|
throw new Error('unresolved-effect abandonment requires the active leaf');
|
|
3326
3534
|
}
|
|
@@ -3347,6 +3555,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3347
3555
|
rootPlaybookId,
|
|
3348
3556
|
});
|
|
3349
3557
|
await runEffect(() => unresolvedEffectSettlement.complete(settlement));
|
|
3558
|
+
abandonmentSettlementUnsafe = false;
|
|
3350
3559
|
};
|
|
3351
3560
|
const callResultFor = (frame, result) => {
|
|
3352
3561
|
if (result.outcome === 'unresolved-effect') {
|
|
@@ -3756,6 +3965,22 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3756
3965
|
...entries.map(([key, value]) => digestLine `- ${key}: ${JSON.stringify(value)}`),
|
|
3757
3966
|
];
|
|
3758
3967
|
};
|
|
3968
|
+
// DR-063 §3: every advertised action names its standing in the digest, so a
|
|
3969
|
+
// model is never invited to select an action that changes nothing.
|
|
3970
|
+
const advertisedActionDigest = (actions) => {
|
|
3971
|
+
if (actions.length === 0)
|
|
3972
|
+
return 'Advertised actions: none.';
|
|
3973
|
+
const lines = [
|
|
3974
|
+
'Advertised actions:',
|
|
3975
|
+
...actions.map((action) => (action.standing ?? 'ready') === 'ready'
|
|
3976
|
+
? digestLine `- ${action.id}: ${action.label}`
|
|
3977
|
+
: digestLine `- ${action.id}: ${action.label} (${action.standing}: ${action.reason ?? 'no reason was published'})`),
|
|
3978
|
+
];
|
|
3979
|
+
if (actions.some(({ standing }) => standing !== undefined && standing !== 'ready')) {
|
|
3980
|
+
lines.push('An action marked no-op runs and changes nothing; a blocked action cannot run at all.');
|
|
3981
|
+
}
|
|
3982
|
+
return lines.join('\n');
|
|
3983
|
+
};
|
|
3759
3984
|
const retainedResumptionDigest = () => {
|
|
3760
3985
|
if (rootFrame() !== undefined) {
|
|
3761
3986
|
return 'Retained resumptions: unavailable while a playbook is engaged.';
|
|
@@ -3800,12 +4025,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3800
4025
|
}
|
|
3801
4026
|
lines.push(`Leaf ${frameLabel(leaf)} is parked for repository-effect reconciliation.`);
|
|
3802
4027
|
lines.push('Pending Boss questions: withheld until reconciliation.');
|
|
3803
|
-
lines.push(reconciliationActions
|
|
3804
|
-
? 'Advertised actions: none.'
|
|
3805
|
-
: [
|
|
3806
|
-
'Advertised actions:',
|
|
3807
|
-
...reconciliationActions.map((action) => digestLine `- ${action.id}: ${action.label}`),
|
|
3808
|
-
].join('\n'));
|
|
4028
|
+
lines.push(advertisedActionDigest(reconciliationActions));
|
|
3809
4029
|
lines.push('Ordinary delivery, switching, dismissal, and runtime actions are unavailable while retained effect evidence is unresolved. Only the advertised unresolved-effect controls may run. Conversation is unaffected: `respond` stays valid for any turn.');
|
|
3810
4030
|
lines.push(retainedResumptionDigest());
|
|
3811
4031
|
return lines.join('\n');
|
|
@@ -3889,12 +4109,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3889
4109
|
message: view.lastError.message,
|
|
3890
4110
|
})}`);
|
|
3891
4111
|
}
|
|
3892
|
-
lines.push(view.actions
|
|
3893
|
-
? 'Advertised actions: none.'
|
|
3894
|
-
: [
|
|
3895
|
-
'Advertised actions:',
|
|
3896
|
-
...view.actions.map((action) => digestLine `- ${action.id}: ${action.label}`),
|
|
3897
|
-
].join('\n'));
|
|
4112
|
+
lines.push(advertisedActionDigest(view.actions));
|
|
3898
4113
|
lines.push(retainedResumptionDigest());
|
|
3899
4114
|
return lines.join('\n');
|
|
3900
4115
|
};
|
|
@@ -3934,7 +4149,15 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3934
4149
|
.filter((action) => !fenced ||
|
|
3935
4150
|
action.id === UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID ||
|
|
3936
4151
|
action.id === UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID)
|
|
3937
|
-
|
|
4152
|
+
// DR-063 §3: the leaf's own standing is published with the pair, so a
|
|
4153
|
+
// host drawing the control says what running it would do. A leaf that
|
|
4154
|
+
// publishes none advertises `ready`, which is what it always meant.
|
|
4155
|
+
.map((action) => Object.freeze({
|
|
4156
|
+
id: action.id,
|
|
4157
|
+
label: action.label,
|
|
4158
|
+
standing: action.standing ?? 'ready',
|
|
4159
|
+
...(action.reason === undefined ? {} : { reason: action.reason }),
|
|
4160
|
+
})));
|
|
3938
4161
|
};
|
|
3939
4162
|
// CAPTAIN-62: the controls the shell effects itself, published beside the
|
|
3940
4163
|
// leaf's advertised actions and never mixed with them — these are the
|
|
@@ -3961,6 +4184,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3961
4184
|
// way the Boss started it rather than the frame that happens to be
|
|
3962
4185
|
// parked inside it.
|
|
3963
4186
|
label: `Stop ${frameLabel(root)}`,
|
|
4187
|
+
// DR-063 §3: giving up always ends the engagement, whatever the leaf
|
|
4188
|
+
// is parked on.
|
|
4189
|
+
standing: 'ready',
|
|
3964
4190
|
}),
|
|
3965
4191
|
]);
|
|
3966
4192
|
};
|
|
@@ -4693,6 +4919,12 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4693
4919
|
if (!root)
|
|
4694
4920
|
return false;
|
|
4695
4921
|
const label = frameLabel(root);
|
|
4922
|
+
if (activeTurn?.hostGiveUp === true &&
|
|
4923
|
+
freezeTurnUnresolvedEffects().length > 0) {
|
|
4924
|
+
await settleUnresolvedEffectAbandonment(leafFrame());
|
|
4925
|
+
facts.push(`Dismissed the ${label} engagement with unresolved repository effects reported.`);
|
|
4926
|
+
return false;
|
|
4927
|
+
}
|
|
4696
4928
|
// Dismissal leaves the procedure unfinished. Persist the latest safe
|
|
4697
4929
|
// generation captured for this turn before disposal erases the frames —
|
|
4698
4930
|
// unless the Boss gave up on it (CAPTAIN-44), in which case retaining it
|
|
@@ -4911,6 +5143,106 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4911
5143
|
return (error instanceof AggregateError &&
|
|
4912
5144
|
error.errors.some((nested) => containsEffectThrow(turn, nested)));
|
|
4913
5145
|
};
|
|
5146
|
+
// DR-063 §4: the leaf's failure as the shell reads it — the cause the runtime
|
|
5147
|
+
// decided, or, behind the retained-effect fence where the leaf itself holds
|
|
5148
|
+
// no error, the incomplete receipt the frozen evidence names.
|
|
5149
|
+
const leafFailureCause = (view, fenced, unresolvedEffects) => {
|
|
5150
|
+
const recorded = view?.lastError ?? lastError;
|
|
5151
|
+
if (recorded?.cause !== undefined) {
|
|
5152
|
+
try {
|
|
5153
|
+
return assertPlaybookFailureCause(recorded.cause);
|
|
5154
|
+
}
|
|
5155
|
+
catch {
|
|
5156
|
+
// A cause the closed validator refuses is not a cause.
|
|
5157
|
+
}
|
|
5158
|
+
}
|
|
5159
|
+
if (fenced) {
|
|
5160
|
+
const incomplete = unresolvedEffects.find(({ classification }) => classification === 'incomplete');
|
|
5161
|
+
if (incomplete !== undefined) {
|
|
5162
|
+
return assertPlaybookFailureCause({
|
|
5163
|
+
code: 'receipt-missing',
|
|
5164
|
+
evidence: { baselineHead: incomplete.baselineHead },
|
|
5165
|
+
});
|
|
5166
|
+
}
|
|
5167
|
+
}
|
|
5168
|
+
// There is no failure without a cause: a parked failure whose error
|
|
5169
|
+
// carries none is a runtime defect named by that error.
|
|
5170
|
+
if (view?.state.stateId === 'failed' && recorded !== undefined) {
|
|
5171
|
+
return assertPlaybookFailureCause({
|
|
5172
|
+
code: 'runtime-defect',
|
|
5173
|
+
evidence: {
|
|
5174
|
+
reason: recorded.message.trim().length > 0
|
|
5175
|
+
? recorded.message
|
|
5176
|
+
: recorded.name,
|
|
5177
|
+
},
|
|
5178
|
+
});
|
|
5179
|
+
}
|
|
5180
|
+
return undefined;
|
|
5181
|
+
};
|
|
5182
|
+
const failureControlLines = (view, fenced) => {
|
|
5183
|
+
const runtimeActions = (view?.actions ?? []).filter((action) => !fenced ||
|
|
5184
|
+
action.id === UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID ||
|
|
5185
|
+
action.id === UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID);
|
|
5186
|
+
const root = rootFrame();
|
|
5187
|
+
return [
|
|
5188
|
+
...runtimeActions.map((action) => failureControlLine(action)),
|
|
5189
|
+
// The shell's own control is published beside the leaf's, so the report
|
|
5190
|
+
// names every door out of this failure (CAPTAIN-62).
|
|
5191
|
+
...(root === undefined
|
|
5192
|
+
? []
|
|
5193
|
+
: [
|
|
5194
|
+
failureControlLine({
|
|
5195
|
+
label: `Stop ${frameLabel(root)}`,
|
|
5196
|
+
standing: 'ready',
|
|
5197
|
+
}),
|
|
5198
|
+
]),
|
|
5199
|
+
];
|
|
5200
|
+
};
|
|
5201
|
+
const describeNestedPlaybook = (playbookId) => {
|
|
5202
|
+
const enablement = enablementById.get(playbookId);
|
|
5203
|
+
return enablement === undefined ? playbookId : `/${enablement.command}`;
|
|
5204
|
+
};
|
|
5205
|
+
// DR-063 §4: appended once, beside the unresolved-effect and pre-existing
|
|
5206
|
+
// change reports, whenever the turn settles with the leaf parked in its
|
|
5207
|
+
// failure state or behind the retained-effect fence.
|
|
5208
|
+
const reportLeafFailure = (turn, unresolvedEffects) => {
|
|
5209
|
+
if (turn.failureReported)
|
|
5210
|
+
return;
|
|
5211
|
+
const leaf = leafFrame();
|
|
5212
|
+
if (!leaf)
|
|
5213
|
+
return;
|
|
5214
|
+
const fenced = retainedEffectReconciliation !== undefined;
|
|
5215
|
+
let view;
|
|
5216
|
+
if (typeof leaf.runtime.describe === 'function') {
|
|
5217
|
+
try {
|
|
5218
|
+
view = leaf.runtime.describe();
|
|
5219
|
+
}
|
|
5220
|
+
catch {
|
|
5221
|
+
// An unreadable control view leaves the mirrored error as evidence.
|
|
5222
|
+
}
|
|
5223
|
+
}
|
|
5224
|
+
if (!fenced && view?.state.stateId !== 'failed')
|
|
5225
|
+
return;
|
|
5226
|
+
const cause = leafFailureCause(view, fenced, unresolvedEffects);
|
|
5227
|
+
if (cause === undefined)
|
|
5228
|
+
return;
|
|
5229
|
+
turn.failureReported = true;
|
|
5230
|
+
const sentence = failureCauseSentence(cause, describeNestedPlaybook);
|
|
5231
|
+
appendMandatoryPresentationSuffix(turn, failureReportText(sentence, failureControlLines(view, fenced)));
|
|
5232
|
+
// The result-phase prompt reads the turn's report, so the same fact joins
|
|
5233
|
+
// it here rather than only the turn's running list.
|
|
5234
|
+
const fact = `The workflow failed: ${sentence}`;
|
|
5235
|
+
turn.settlementFacts.push(fact);
|
|
5236
|
+
if (turn.report !== undefined) {
|
|
5237
|
+
turn.report = {
|
|
5238
|
+
...turn.report,
|
|
5239
|
+
facts: [...turn.report.facts, fact],
|
|
5240
|
+
...(turn.report.bossFacts === undefined
|
|
5241
|
+
? {}
|
|
5242
|
+
: { bossFacts: [...turn.report.bossFacts, fact] }),
|
|
5243
|
+
};
|
|
5244
|
+
}
|
|
5245
|
+
};
|
|
4914
5246
|
const settleSelection = async (selection, signal) => {
|
|
4915
5247
|
const turn = activeTurn;
|
|
4916
5248
|
runFailureFacts = [];
|
|
@@ -4921,6 +5253,11 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4921
5253
|
if (turn !== undefined && report !== undefined) {
|
|
4922
5254
|
appendMandatoryPresentationSuffix(turn, report);
|
|
4923
5255
|
}
|
|
5256
|
+
if (turn !== undefined)
|
|
5257
|
+
reportCarriedPreExistingChanges(turn);
|
|
5258
|
+
if (turn !== undefined) {
|
|
5259
|
+
reportLeafFailure(turn, frozenUnresolvedEffects);
|
|
5260
|
+
}
|
|
4924
5261
|
return frozenUnresolvedEffects;
|
|
4925
5262
|
};
|
|
4926
5263
|
const finalizeSettlement = (settlement) => Object.freeze({
|
|
@@ -5066,7 +5403,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5066
5403
|
selection.actionId === UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID) &&
|
|
5067
5404
|
fencedLeaf !== undefined;
|
|
5068
5405
|
if (retainedEffectReconciliation !== undefined &&
|
|
5069
|
-
!routesRetainedReconciliation
|
|
5406
|
+
!routesRetainedReconciliation &&
|
|
5407
|
+
!(selection.action === 'dismiss' && turn.hostGiveUp === true)) {
|
|
5070
5408
|
return rejectSelection(selection, 'retained work must reconcile its repository-effect evidence before an ordinary action can run');
|
|
5071
5409
|
}
|
|
5072
5410
|
if (selection.action === 'resume') {
|
|
@@ -5359,7 +5697,6 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5359
5697
|
await settleUnresolvedEffectAbandonment(leaf);
|
|
5360
5698
|
}
|
|
5361
5699
|
catch (error) {
|
|
5362
|
-
abandonmentSettlementUnsafe = true;
|
|
5363
5700
|
const normalized = normalizeErrorCompact(error) ?? {
|
|
5364
5701
|
name: 'Error',
|
|
5365
5702
|
message: String(error),
|
|
@@ -5473,8 +5810,13 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5473
5810
|
const giveUpReplyText = () => {
|
|
5474
5811
|
const report = activeTurn?.report;
|
|
5475
5812
|
const facts = report?.bossFacts ?? report?.facts ?? [];
|
|
5476
|
-
const
|
|
5477
|
-
const
|
|
5813
|
+
const stopped = report?.status === 'ok';
|
|
5814
|
+
const opening = stopped
|
|
5815
|
+
? 'Stopped that workflow. I will not resume it.'
|
|
5816
|
+
: 'I could not confirm that the workflow was stopped.';
|
|
5817
|
+
const closing = stopped
|
|
5818
|
+
? 'Send the command again to start fresh work.'
|
|
5819
|
+
: 'The stop did not settle successfully; recover the session before continuing.';
|
|
5478
5820
|
const composed = [
|
|
5479
5821
|
opening,
|
|
5480
5822
|
...(facts.length === 0
|
|
@@ -6394,12 +6736,24 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
6394
6736
|
const hostRuntimeActionId = decided?.kind === 'runtime' ? decided.actionId : undefined;
|
|
6395
6737
|
const hostGiveUp = decided?.kind === 'give-up' ? true : undefined;
|
|
6396
6738
|
const parsed = decided === undefined ? resolveCommandTurn(turn.prompt) : undefined;
|
|
6739
|
+
// DR-062 §5: the boundary count this turn starts from. Every boundary
|
|
6740
|
+
// beyond it completed during this turn.
|
|
6741
|
+
let effectBoundaryPrefixCount;
|
|
6742
|
+
try {
|
|
6743
|
+
effectBoundaryPrefixCount = assertPlaybookEffectLedger(currentEffectLedger()).boundaries.length;
|
|
6744
|
+
}
|
|
6745
|
+
catch {
|
|
6746
|
+
effectBoundaryPrefixCount = undefined;
|
|
6747
|
+
}
|
|
6397
6748
|
activeTurn = {
|
|
6398
6749
|
id: ++turnSequence,
|
|
6399
6750
|
captainSyncedJournalSeq: journalSeq,
|
|
6400
6751
|
bossText: turn.prompt,
|
|
6401
6752
|
authoritativeText: parsed?.authoritativeText ?? turn.prompt,
|
|
6402
6753
|
...(parsed ? { resolution: parsed.resolution } : {}),
|
|
6754
|
+
...(effectBoundaryPrefixCount === undefined
|
|
6755
|
+
? {}
|
|
6756
|
+
: { effectBoundaryPrefixCount }),
|
|
6403
6757
|
...(hostRuntimeActionId === undefined ? {} : { hostRuntimeActionId }),
|
|
6404
6758
|
...(hostGiveUp === undefined ? {} : { hostGiveUp }),
|
|
6405
6759
|
settled: false,
|