@sublang/playbook 17.8.1 → 17.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/configuration.md +29 -2
- package/package.json +2 -2
- package/reference/sdlc/captain.md +1 -1
- package/reference/sdlc/captain.playbook/captain.fsm.js +1 -1
- package/reference/sdlc/captain.playbook/captain.fsm.ts +1 -1
- package/reference/sdlc/captain.playbook/captain.gears.md +1 -1
- package/reference/sdlc/code.playbook/bin/launch-config.js +55 -13
- package/reference/sdlc/code.playbook/bin/repository-effects.js +26 -2
- package/reference/sdlc/code.playbook/bin/session-store.js +3 -0
- package/reference/sdlc/code.playbook/host-capabilities.d.ts +1 -0
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +2 -1
- package/reference/sdlc/code.playbook/playbook-captain.js +170 -48
- package/reference/sdlc/code.playbook/playbook-captain.ts +227 -63
- package/slc/link.md +25 -3
- package/src/runtime.d.ts +13 -0
- package/src/runtime.ts +14 -0
- package/src/xstate-playbook-runtime.d.ts +4 -3
- package/src/xstate-playbook-runtime.js +163 -19
- package/src/xstate-playbook-runtime.ts +197 -18
- package/src/xstate-runtime.d.ts +7 -5
- package/src/xstate-runtime.js +40 -9
- package/src/xstate-runtime.ts +53 -24
|
@@ -569,6 +569,9 @@ function pendingBossQuestionRecord(pending) {
|
|
|
569
569
|
// Generic strategy defaults.
|
|
570
570
|
// ---------------------------------------------------------------------------
|
|
571
571
|
const CONTINUATION_PREAMBLE = 'Continue the same task using Boss’s reply below.';
|
|
572
|
+
// The labelled continuation a compiled artifact's composer renders, which
|
|
573
|
+
// slc's prompt-contract verifier pins. The runtime no longer sends it: it
|
|
574
|
+
// hands a composer no question and reply, and marks them itself (DR-090 §2).
|
|
572
575
|
function continuationBlocks(input, resuming = false) {
|
|
573
576
|
if (input.pendingBossQuestion === undefined || input.bossReply === undefined) {
|
|
574
577
|
return [];
|
|
@@ -579,6 +582,59 @@ function continuationBlocks(input, resuming = false) {
|
|
|
579
582
|
`Boss reply:\n${input.bossReply}`,
|
|
580
583
|
];
|
|
581
584
|
}
|
|
585
|
+
// DR-090 §2: a Boss exchange reaches a player as structure, not as an
|
|
586
|
+
// English instruction — a marker line before each question and each reply,
|
|
587
|
+
// whatever language the run speaks.
|
|
588
|
+
const BOSS_QUESTION_MARKER = '? ⟶';
|
|
589
|
+
const BOSS_REPLY_MARKER = 'Boss ⟶';
|
|
590
|
+
function bossQuestionBlock(question) {
|
|
591
|
+
return `${BOSS_QUESTION_MARKER}\n${question}`;
|
|
592
|
+
}
|
|
593
|
+
function bossReplyBlock(reply) {
|
|
594
|
+
return `${BOSS_REPLY_MARKER}\n${reply}`;
|
|
595
|
+
}
|
|
596
|
+
/** DR-090 §2: the marked continuation of a call after a Boss reply. */
|
|
597
|
+
function markedContinuationBlocks(input, resuming = false) {
|
|
598
|
+
if (input.pendingBossQuestion === undefined || input.bossReply === undefined) {
|
|
599
|
+
return [];
|
|
600
|
+
}
|
|
601
|
+
return [
|
|
602
|
+
...(resuming ? [] : [bossQuestionBlock(input.pendingBossQuestion.question)]),
|
|
603
|
+
bossReplyBlock(input.bossReply),
|
|
604
|
+
];
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* The input a composer receives: without the question and reply, which the
|
|
608
|
+
* runtime marks itself (DR-090 §2), so a composer adds no labelled blocks.
|
|
609
|
+
*/
|
|
610
|
+
function withoutContinuation(input) {
|
|
611
|
+
if (input.pendingBossQuestion === undefined && input.bossReply === undefined) {
|
|
612
|
+
return input;
|
|
613
|
+
}
|
|
614
|
+
const { pendingBossQuestion: _question, bossReply: _reply, ...rest } = input;
|
|
615
|
+
return rest;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* DR-090 §1: the run's Boss exchanges a player call carries before its body,
|
|
619
|
+
* leaving out the one exchange the call's own continuation blocks carry, so
|
|
620
|
+
* the asking role reads its answered question once.
|
|
621
|
+
*/
|
|
622
|
+
function bossExchangeBlocks(exchanges, input) {
|
|
623
|
+
let carried = -1;
|
|
624
|
+
if (input.pendingBossQuestion !== undefined && input.bossReply !== undefined) {
|
|
625
|
+
const question = input.pendingBossQuestion.question;
|
|
626
|
+
for (let index = exchanges.length - 1; index >= 0; index -= 1) {
|
|
627
|
+
const exchange = exchanges[index];
|
|
628
|
+
if (exchange.question === question && exchange.reply === input.bossReply) {
|
|
629
|
+
carried = index;
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return exchanges.flatMap((exchange, index) => index === carried
|
|
635
|
+
? []
|
|
636
|
+
: [bossQuestionBlock(exchange.question), bossReplyBlock(exchange.reply)]);
|
|
637
|
+
}
|
|
582
638
|
/** Add clarification context without repeating the question in a live conversation. */
|
|
583
639
|
export function composePlayerContinuation(input, body, resuming = false) {
|
|
584
640
|
return [...continuationBlocks(input, resuming), body].join('\n\n');
|
|
@@ -939,14 +995,20 @@ export function renderGovernedOutcomeContract(guard, description, outcome, shape
|
|
|
939
995
|
for (const [field, authority] of Object.entries(outcome?.fields ?? {})) {
|
|
940
996
|
if (authority === 'semantic') {
|
|
941
997
|
const entry = authored.get(field);
|
|
942
|
-
// DR-089: the value has the shape the compiled machine admits
|
|
998
|
+
// DR-089: the value has the shape the compiled machine admits; a
|
|
999
|
+
// shape the probe did not reveal is the machine's to judge, so the
|
|
1000
|
+
// example asserts none (DR-091).
|
|
943
1001
|
const shape = shapes !== undefined && Object.prototype.hasOwnProperty.call(shapes, field)
|
|
944
1002
|
? shapes[field]
|
|
945
|
-
:
|
|
946
|
-
const words = describePlaybookSemanticValueShape(shape);
|
|
1003
|
+
: undefined;
|
|
1004
|
+
const words = shape === undefined ? undefined : describePlaybookSemanticValueShape(shape);
|
|
947
1005
|
replyMembers.push(`${JSON.stringify(field)}: ${entry?.placeholder ??
|
|
948
|
-
(shape ===
|
|
949
|
-
|
|
1006
|
+
(shape === undefined
|
|
1007
|
+
? '<value>'
|
|
1008
|
+
: shape === 'string'
|
|
1009
|
+
? '<string>'
|
|
1010
|
+
: `<${words.replace(/^a /, '')}>`)}`);
|
|
1011
|
+
if (shape !== undefined && shape !== 'string')
|
|
950
1012
|
valueShapes.push(`\`${field}\` is ${words}`);
|
|
951
1013
|
semanticAsAuthored.push(entry?.segment ?? `\`${field}\``);
|
|
952
1014
|
}
|
|
@@ -2005,8 +2067,9 @@ function snapshotOutcomeAuthority(descriptor, label, playerStates, verbatimPaylo
|
|
|
2005
2067
|
// the machine's transition selection — guards only, no action runs — with
|
|
2006
2068
|
// sample values, and the first combination of shapes whose arm carries that
|
|
2007
2069
|
// outcome's accepted-outcome marker is the contract the judge is shown and the
|
|
2008
|
-
// reconciler enforces. A field
|
|
2009
|
-
//
|
|
2070
|
+
// reconciler enforces. A field the machine admits as a string is recorded as
|
|
2071
|
+
// one; a field whose shape it does not reveal is recorded with none, so the
|
|
2072
|
+
// reconciler leaves its value to the machine's own validator (DR-091).
|
|
2010
2073
|
// ---------------------------------------------------------------------------
|
|
2011
2074
|
const SEMANTIC_SHAPE_SAMPLES = [
|
|
2012
2075
|
['string', 'sample'],
|
|
@@ -2045,9 +2108,10 @@ function shapeCombinations(fieldCount) {
|
|
|
2045
2108
|
left.join(',').localeCompare(right.join(',')));
|
|
2046
2109
|
}
|
|
2047
2110
|
/**
|
|
2048
|
-
* DR-089: the
|
|
2049
|
-
*
|
|
2050
|
-
*
|
|
2111
|
+
* DR-089: the JSON value shape the compiled machine admits for each
|
|
2112
|
+
* semantic-owned field of each governed outcome, keyed by state id and then
|
|
2113
|
+
* by outcome. A field, outcome, or state the probe reveals no shape for is
|
|
2114
|
+
* left out (DR-091). Exported so linked machinery of an artifact's own reads
|
|
2051
2115
|
* the same contract the shared factory enforces.
|
|
2052
2116
|
*/
|
|
2053
2117
|
export function inferSemanticValueShapes(machine, authority) {
|
|
@@ -2127,18 +2191,17 @@ export function inferSemanticValueShapes(machine, authority) {
|
|
|
2127
2191
|
});
|
|
2128
2192
|
if (!admits(values))
|
|
2129
2193
|
continue;
|
|
2194
|
+
// DR-091: an admitted string is recorded too, so only a field the
|
|
2195
|
+
// probe revealed nothing for goes unrecorded.
|
|
2130
2196
|
const shapes = {};
|
|
2131
2197
|
semanticFields.forEach((field, index) => {
|
|
2132
2198
|
let shape = SEMANTIC_SHAPE_SAMPLES[combination[index]][0];
|
|
2133
2199
|
if (shape === 'string-array' && admits({ ...values, [field]: [1] })) {
|
|
2134
2200
|
shape = 'string-or-number-array';
|
|
2135
2201
|
}
|
|
2136
|
-
|
|
2137
|
-
shapes[field] = shape;
|
|
2202
|
+
shapes[field] = shape;
|
|
2138
2203
|
});
|
|
2139
|
-
|
|
2140
|
-
stateShapes[guard] = Object.freeze(shapes);
|
|
2141
|
-
}
|
|
2204
|
+
stateShapes[guard] = Object.freeze(shapes);
|
|
2142
2205
|
break;
|
|
2143
2206
|
}
|
|
2144
2207
|
}
|
|
@@ -2715,8 +2778,13 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2715
2778
|
}
|
|
2716
2779
|
const composePlayerPrompt = spec.composePlayerPrompt ??
|
|
2717
2780
|
((input, _identity, resuming = false) => defaultComposePlayerPrompt(input, spec.placeholderFields, resuming));
|
|
2718
|
-
const
|
|
2781
|
+
const composeArtifactCaptainPrompt = spec.composeCaptainPrompt ??
|
|
2719
2782
|
((input) => defaultComposeCaptainPrompt(input, spec.placeholderFields));
|
|
2783
|
+
// DR-090 §2: a direct-Captain continuation is marked by the runtime too.
|
|
2784
|
+
const composeCaptainPrompt = (input) => [
|
|
2785
|
+
...markedContinuationBlocks(input),
|
|
2786
|
+
composeArtifactCaptainPrompt(withoutContinuation(input)),
|
|
2787
|
+
].join('\n\n');
|
|
2720
2788
|
const extractFields = spec.extractRequiredFields ?? defaultExtractRequiredFields;
|
|
2721
2789
|
const verbatimPayloadFields = new Set(spec.verbatimPayloadFields ?? NO_VERBATIM_FIELDS);
|
|
2722
2790
|
const adjudication = {
|
|
@@ -2865,6 +2933,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2865
2933
|
const currentEffectLedger = () => assertPlaybookEffectLedger(effectLedgerCapability.snapshot(), `${label} current host effect ledger`);
|
|
2866
2934
|
let effectLedgerMirror = currentEffectLedger();
|
|
2867
2935
|
let recoveryCheckpoint;
|
|
2936
|
+
// DR-090 §1: the run's Boss exchanges, in order. A run begins at the
|
|
2937
|
+
// artifact's entry event; the list travels in the snapshot.
|
|
2938
|
+
let bossExchanges = [];
|
|
2868
2939
|
let retainedEffectSourceSessionId;
|
|
2869
2940
|
let retainedEffectReconciliation;
|
|
2870
2941
|
let retainedEffectReconciliationRequired = false;
|
|
@@ -3495,7 +3566,17 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3495
3566
|
}
|
|
3496
3567
|
return session?.roleBindings?.[roleId]?.promptIdentity ?? roleId;
|
|
3497
3568
|
}
|
|
3569
|
+
// DR-090 §3: the first block names the role and, where the host bound
|
|
3570
|
+
// it, the model the call runs on as configured, so a player can name
|
|
3571
|
+
// both where its project asks (a commit trailer, say).
|
|
3572
|
+
function playerIdentityBlock(roleId) {
|
|
3573
|
+
const binding = session?.roleBindings?.[roleId];
|
|
3574
|
+
return binding === undefined
|
|
3575
|
+
? `Role: ${roleId}`
|
|
3576
|
+
: `Role: ${roleId} · Model: ${binding.promptIdentity}`;
|
|
3577
|
+
}
|
|
3498
3578
|
function composeBoundPlayerPrompt(input, resuming = false) {
|
|
3579
|
+
const roleId = requireRoleId(input);
|
|
3499
3580
|
let active = true;
|
|
3500
3581
|
const lookup = (roleId) => {
|
|
3501
3582
|
if (!active) {
|
|
@@ -3503,12 +3584,44 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3503
3584
|
}
|
|
3504
3585
|
return promptIdentity(roleId);
|
|
3505
3586
|
};
|
|
3587
|
+
let body;
|
|
3506
3588
|
try {
|
|
3507
|
-
|
|
3589
|
+
body = composePlayerPrompt(withoutContinuation(input), lookup, resuming);
|
|
3508
3590
|
}
|
|
3509
3591
|
finally {
|
|
3510
3592
|
active = false;
|
|
3511
3593
|
}
|
|
3594
|
+
// DR-090 §1, §2: every role hears the Boss — the run's exchanges come
|
|
3595
|
+
// before the body whatever the role, and the asking role's marked
|
|
3596
|
+
// continuation carries the exchange it asked.
|
|
3597
|
+
return [
|
|
3598
|
+
playerIdentityBlock(roleId),
|
|
3599
|
+
...bossExchangeBlocks(bossExchanges, input),
|
|
3600
|
+
...markedContinuationBlocks(input, resuming),
|
|
3601
|
+
body,
|
|
3602
|
+
].join('\n\n');
|
|
3603
|
+
}
|
|
3604
|
+
/** DR-090 §1: record a Boss reply the machine is about to receive. */
|
|
3605
|
+
function recordBossExchange(event, pending) {
|
|
3606
|
+
if (event.type !== 'BOSS_REPLY')
|
|
3607
|
+
return;
|
|
3608
|
+
const { questionId, answer } = event;
|
|
3609
|
+
if (typeof answer !== 'string' || answer.trim().length === 0)
|
|
3610
|
+
return;
|
|
3611
|
+
const question = pending.find((candidate) => candidate.questionId === questionId) ??
|
|
3612
|
+
(questionId === undefined && pending.length === 1 ? pending[0] : undefined);
|
|
3613
|
+
if (question === undefined)
|
|
3614
|
+
return;
|
|
3615
|
+
bossExchanges = Object.freeze([
|
|
3616
|
+
...bossExchanges,
|
|
3617
|
+
Object.freeze({ question: question.question, reply: answer }),
|
|
3618
|
+
]);
|
|
3619
|
+
}
|
|
3620
|
+
/** DR-090 §1: an entry event begins a new run, with no exchanges yet. */
|
|
3621
|
+
function beginRunIfEntry(event) {
|
|
3622
|
+
if (spec.entryEvent !== undefined && event.type === spec.entryEvent.type) {
|
|
3623
|
+
bossExchanges = [];
|
|
3624
|
+
}
|
|
3512
3625
|
}
|
|
3513
3626
|
function continuationKey(roleId, playerId) {
|
|
3514
3627
|
return playerId ?? roleId;
|
|
@@ -4372,7 +4485,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4372
4485
|
try {
|
|
4373
4486
|
await claim.assertOwner();
|
|
4374
4487
|
const restored = snapshotJsonValue(await repositoryCapability.observe());
|
|
4375
|
-
|
|
4488
|
+
// DR-090 §4: the branch an observation names is never compared.
|
|
4489
|
+
const { branch: _restoredBranch, ...restoredState } = restored;
|
|
4490
|
+
const { branch: _savedBranch, ...savedState } = saved.baseline;
|
|
4491
|
+
if (!isDeepStrictEqual(restoredState, savedState)) {
|
|
4376
4492
|
throw new Error(`${label} preparation has not restored the stopped step's repository; it remains paused`);
|
|
4377
4493
|
}
|
|
4378
4494
|
signal.throwIfAborted();
|
|
@@ -4580,6 +4696,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4580
4696
|
...((state.stateId === 'failed' || pendingQuestions.length > 0) && recoveryCheckpoint !== undefined
|
|
4581
4697
|
? { recoveryCheckpoint } : {}),
|
|
4582
4698
|
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
4699
|
+
...(bossExchanges.length === 0
|
|
4700
|
+
? {}
|
|
4701
|
+
: {
|
|
4702
|
+
bossExchanges: bossExchanges.map(({ question, reply }) => ({
|
|
4703
|
+
question,
|
|
4704
|
+
reply,
|
|
4705
|
+
})),
|
|
4706
|
+
}),
|
|
4583
4707
|
};
|
|
4584
4708
|
}
|
|
4585
4709
|
async function runJournalledStep(kind, input, signal, execute) {
|
|
@@ -6354,6 +6478,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6354
6478
|
deferInspectionEmissions = false;
|
|
6355
6479
|
deferredInspectionEmissions = [];
|
|
6356
6480
|
recoveryCheckpoint = undefined;
|
|
6481
|
+
bossExchanges = [];
|
|
6357
6482
|
suppressInspectionEmissions = false;
|
|
6358
6483
|
initialized = false;
|
|
6359
6484
|
traceSequence = 0;
|
|
@@ -6706,6 +6831,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6706
6831
|
}
|
|
6707
6832
|
restoreRoleResumeTokens(boundSnapshot.roleResumeTokens);
|
|
6708
6833
|
}
|
|
6834
|
+
// DR-090 §1: a restored or adopted run keeps the Boss exchanges it
|
|
6835
|
+
// heard; a snapshot without them heard none.
|
|
6836
|
+
bossExchanges = Object.freeze((boundSnapshot.bossExchanges ?? []).map(({ question, reply }) => Object.freeze({ question, reply })));
|
|
6709
6837
|
nestedBridge.prepareRestore(suspendedCall);
|
|
6710
6838
|
if (suspendedCall !== undefined) {
|
|
6711
6839
|
playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
|
|
@@ -7671,7 +7799,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
7671
7799
|
deferredOperation !== undefined) {
|
|
7672
7800
|
if (isExactDeferredBossReply(classifiedSnapshot, event, deferredPending)) {
|
|
7673
7801
|
const statusLine = classificationStatus(event);
|
|
7674
|
-
const
|
|
7802
|
+
const acceptedPending = deferredPending;
|
|
7803
|
+
const acceptedEvent = event;
|
|
7804
|
+
const continuation = await continueBoundDeferredOperation(deferredOperation, event, signal, turnId, statusLine, () => {
|
|
7805
|
+
// DR-090 §1: the host started the continuation, so the
|
|
7806
|
+
// machine receives this reply now.
|
|
7807
|
+
recordBossExchange(acceptedEvent, [acceptedPending]);
|
|
7808
|
+
onAccepted?.();
|
|
7809
|
+
});
|
|
7675
7810
|
if (continuation === 'continued') {
|
|
7676
7811
|
if (controlPlaneError !== undefined) {
|
|
7677
7812
|
throw controlPlaneError;
|
|
@@ -7715,13 +7850,21 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
7715
7850
|
signal.throwIfAborted();
|
|
7716
7851
|
// 3. A final actor cannot accept new events; reconstruct only
|
|
7717
7852
|
// after classification produced a real event.
|
|
7853
|
+
const acceptedPending = classifiedSnapshot === undefined
|
|
7854
|
+
? []
|
|
7855
|
+
: pendingBossQuestionsFor(normalizePlaybookSnapshot(classifiedSnapshot), (classifiedSnapshot.context ??
|
|
7856
|
+
{}));
|
|
7718
7857
|
if (actor.getSnapshot().status === 'done') {
|
|
7719
7858
|
stopActor();
|
|
7720
7859
|
actor = buildActor(runtimePorts);
|
|
7721
7860
|
// The replacement actor's snapshots are real state entries.
|
|
7722
7861
|
suppressInspectionEmissions = false;
|
|
7723
7862
|
actor.start();
|
|
7863
|
+
// DR-090 §1: a finished run's exchanges end with it.
|
|
7864
|
+
bossExchanges = [];
|
|
7724
7865
|
}
|
|
7866
|
+
beginRunIfEntry(event);
|
|
7867
|
+
recordBossExchange(event, acceptedPending);
|
|
7725
7868
|
onAccepted?.();
|
|
7726
7869
|
actor.send(event);
|
|
7727
7870
|
await waitForPlaybookQuiescence(actor, {
|
|
@@ -8002,6 +8145,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
8002
8145
|
failedGovernedAttemptId = undefined;
|
|
8003
8146
|
controlPlaneError = undefined;
|
|
8004
8147
|
emissionFailure = undefined;
|
|
8148
|
+
bossExchanges = [];
|
|
8005
8149
|
retainedEffectSourceSessionId = undefined;
|
|
8006
8150
|
retainedEffectReconciliation = undefined;
|
|
8007
8151
|
retainedEffectReconciliationRequired = false;
|