@sublang/playbook 14.0.0 → 15.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/cli.md +14 -14
- package/docs/configuration.md +15 -9
- package/package.json +1 -1
- package/reference/sdlc/branch.playbook/branch.fsm.d.ts +1 -1
- package/reference/sdlc/code.md +1 -1
- package/reference/sdlc/code.playbook/bin/launch-config.js +55 -16
- package/reference/sdlc/code.playbook/bin/playbook.js +14 -9
- package/reference/sdlc/code.playbook/bin/repository-effects.js +187 -14
- package/reference/sdlc/code.playbook/bin/run.js +12 -8
- package/reference/sdlc/code.playbook/code.fsm.d.ts +1 -1
- package/reference/sdlc/code.playbook/code.fsm.js +1 -1
- package/reference/sdlc/code.playbook/code.fsm.ts +1 -1
- package/reference/sdlc/code.playbook/code.gears.md +2 -2
- 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 +357 -16
- package/reference/sdlc/code.playbook/playbook-captain.ts +459 -34
- package/reference/sdlc/code.playbook/session-host.d.ts +6 -3
- package/reference/sdlc/decide.md +1 -1
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +1 -1
- package/reference/sdlc/decide.playbook/decide.fsm.js +1 -1
- package/reference/sdlc/decide.playbook/decide.fsm.ts +1 -1
- package/reference/sdlc/decide.playbook/decide.gears.md +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.js +1 -1
- package/reference/sdlc/dev.playbook/dev.playbook.ts +1 -2
- package/reference/sdlc/dev.playbook/dev.registry.js +1 -1
- package/reference/sdlc/dev.playbook/dev.registry.ts +1 -1
- package/reference/sdlc/pr.playbook/pr.fsm.d.ts +1 -1
- package/reference/sdlc/review.md +1 -1
- package/reference/sdlc/review.playbook/review.fsm.d.ts +1 -1
- package/reference/sdlc/review.playbook/review.fsm.js +1 -1
- package/reference/sdlc/review.playbook/review.fsm.ts +1 -1
- package/reference/sdlc/review.playbook/review.gears.md +1 -1
- package/slc/link.md +32 -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 +372 -20
- package/src/xstate-playbook-runtime.ts +460 -19
- package/src/xstate-runtime.d.ts +9 -1
- package/src/xstate-runtime.js +299 -19
- package/src/xstate-runtime.ts +362 -21
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Captain, type CaptainSession, type TuningSelection } from '@sublang/cligent/tmux-play';
|
|
2
2
|
import type { Effort, PermissionPolicy } from '@sublang/cligent';
|
|
3
|
-
import type { JsonValue, PlaybookEffectLedger, PlaybookEffectLedgerCommandBatch, PlaybookControlAction, PlaybookRuntime, PlaybookRuntimeSnapshot } from '@sublang/playbook/runtime';
|
|
3
|
+
import type { JsonValue, PlaybookEffectLedger, PlaybookEffectLedgerCommandBatch, PlaybookControlAction, PlaybookFailureCause, PlaybookRuntime, PlaybookRuntimeSnapshot } from '@sublang/playbook/runtime';
|
|
4
4
|
import { type CaptainControllerPort } from '../captain.playbook/captain.playbook.js';
|
|
5
5
|
import type { PlaybookSummaryPolicy } from './code.registry.js';
|
|
6
6
|
interface SessionAgent {
|
|
@@ -176,9 +176,11 @@ type PlaybookCaptainShellSnapshotValue = PlaybookCaptainShellSnapshotFields & ({
|
|
|
176
176
|
readonly frames: readonly PlaybookCaptainFrameSnapshot[];
|
|
177
177
|
readonly retainedEffectReconciliation?: PlaybookCaptainRetainedEffectReconciliation;
|
|
178
178
|
readonly pendingBossQuestions?: JsonValue;
|
|
179
|
+
/** DR-063 §2: the parked failure's cause travels with its error. */
|
|
179
180
|
readonly lastError?: {
|
|
180
181
|
readonly name: string;
|
|
181
182
|
readonly message: string;
|
|
183
|
+
readonly cause?: PlaybookFailureCause;
|
|
182
184
|
};
|
|
183
185
|
});
|
|
184
186
|
export type PlaybookCaptainShellSnapshot = DeepReadonly<PlaybookCaptainShellSnapshotValue>;
|
|
@@ -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);
|
|
@@ -3758,6 +3965,22 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3758
3965
|
...entries.map(([key, value]) => digestLine `- ${key}: ${JSON.stringify(value)}`),
|
|
3759
3966
|
];
|
|
3760
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
|
+
};
|
|
3761
3984
|
const retainedResumptionDigest = () => {
|
|
3762
3985
|
if (rootFrame() !== undefined) {
|
|
3763
3986
|
return 'Retained resumptions: unavailable while a playbook is engaged.';
|
|
@@ -3802,12 +4025,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3802
4025
|
}
|
|
3803
4026
|
lines.push(`Leaf ${frameLabel(leaf)} is parked for repository-effect reconciliation.`);
|
|
3804
4027
|
lines.push('Pending Boss questions: withheld until reconciliation.');
|
|
3805
|
-
lines.push(reconciliationActions
|
|
3806
|
-
? 'Advertised actions: none.'
|
|
3807
|
-
: [
|
|
3808
|
-
'Advertised actions:',
|
|
3809
|
-
...reconciliationActions.map((action) => digestLine `- ${action.id}: ${action.label}`),
|
|
3810
|
-
].join('\n'));
|
|
4028
|
+
lines.push(advertisedActionDigest(reconciliationActions));
|
|
3811
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.');
|
|
3812
4030
|
lines.push(retainedResumptionDigest());
|
|
3813
4031
|
return lines.join('\n');
|
|
@@ -3891,12 +4109,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3891
4109
|
message: view.lastError.message,
|
|
3892
4110
|
})}`);
|
|
3893
4111
|
}
|
|
3894
|
-
lines.push(view.actions
|
|
3895
|
-
? 'Advertised actions: none.'
|
|
3896
|
-
: [
|
|
3897
|
-
'Advertised actions:',
|
|
3898
|
-
...view.actions.map((action) => digestLine `- ${action.id}: ${action.label}`),
|
|
3899
|
-
].join('\n'));
|
|
4112
|
+
lines.push(advertisedActionDigest(view.actions));
|
|
3900
4113
|
lines.push(retainedResumptionDigest());
|
|
3901
4114
|
return lines.join('\n');
|
|
3902
4115
|
};
|
|
@@ -3936,7 +4149,15 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3936
4149
|
.filter((action) => !fenced ||
|
|
3937
4150
|
action.id === UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID ||
|
|
3938
4151
|
action.id === UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID)
|
|
3939
|
-
|
|
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
|
+
})));
|
|
3940
4161
|
};
|
|
3941
4162
|
// CAPTAIN-62: the controls the shell effects itself, published beside the
|
|
3942
4163
|
// leaf's advertised actions and never mixed with them — these are the
|
|
@@ -3963,6 +4184,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3963
4184
|
// way the Boss started it rather than the frame that happens to be
|
|
3964
4185
|
// parked inside it.
|
|
3965
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',
|
|
3966
4190
|
}),
|
|
3967
4191
|
]);
|
|
3968
4192
|
};
|
|
@@ -4919,6 +5143,106 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4919
5143
|
return (error instanceof AggregateError &&
|
|
4920
5144
|
error.errors.some((nested) => containsEffectThrow(turn, nested)));
|
|
4921
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
|
+
};
|
|
4922
5246
|
const settleSelection = async (selection, signal) => {
|
|
4923
5247
|
const turn = activeTurn;
|
|
4924
5248
|
runFailureFacts = [];
|
|
@@ -4929,6 +5253,11 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4929
5253
|
if (turn !== undefined && report !== undefined) {
|
|
4930
5254
|
appendMandatoryPresentationSuffix(turn, report);
|
|
4931
5255
|
}
|
|
5256
|
+
if (turn !== undefined)
|
|
5257
|
+
reportCarriedPreExistingChanges(turn);
|
|
5258
|
+
if (turn !== undefined) {
|
|
5259
|
+
reportLeafFailure(turn, frozenUnresolvedEffects);
|
|
5260
|
+
}
|
|
4932
5261
|
return frozenUnresolvedEffects;
|
|
4933
5262
|
};
|
|
4934
5263
|
const finalizeSettlement = (settlement) => Object.freeze({
|
|
@@ -6407,12 +6736,24 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
6407
6736
|
const hostRuntimeActionId = decided?.kind === 'runtime' ? decided.actionId : undefined;
|
|
6408
6737
|
const hostGiveUp = decided?.kind === 'give-up' ? true : undefined;
|
|
6409
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
|
+
}
|
|
6410
6748
|
activeTurn = {
|
|
6411
6749
|
id: ++turnSequence,
|
|
6412
6750
|
captainSyncedJournalSeq: journalSeq,
|
|
6413
6751
|
bossText: turn.prompt,
|
|
6414
6752
|
authoritativeText: parsed?.authoritativeText ?? turn.prompt,
|
|
6415
6753
|
...(parsed ? { resolution: parsed.resolution } : {}),
|
|
6754
|
+
...(effectBoundaryPrefixCount === undefined
|
|
6755
|
+
? {}
|
|
6756
|
+
: { effectBoundaryPrefixCount }),
|
|
6416
6757
|
...(hostRuntimeActionId === undefined ? {} : { hostRuntimeActionId }),
|
|
6417
6758
|
...(hostGiveUp === undefined ? {} : { hostGiveUp }),
|
|
6418
6759
|
settled: false,
|