@sublang/playbook 17.4.0 → 17.5.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/cli.md +30 -3
- package/docs/configuration.md +32 -2
- package/docs/embedding.md +29 -0
- package/package.json +2 -2
- package/reference/sdlc/code.md +24 -4
- package/reference/sdlc/code.playbook/bin/launch-config.js +249 -39
- package/reference/sdlc/code.playbook/bin/playbook.js +43 -6
- package/reference/sdlc/code.playbook/bin/run.js +37 -3
- package/reference/sdlc/code.playbook/bin/session-store.js +11 -5
- package/reference/sdlc/code.playbook/code.fsm.d.ts +12 -0
- package/reference/sdlc/code.playbook/code.fsm.js +40 -6
- package/reference/sdlc/code.playbook/code.fsm.ts +53 -6
- package/reference/sdlc/code.playbook/code.gears.md +32 -4
- package/reference/sdlc/code.playbook/playbook-captain.js +45 -4
- package/reference/sdlc/code.playbook/playbook-captain.ts +50 -3
- package/reference/sdlc/code.playbook/session-assets.js +7 -6
- package/reference/sdlc/code.playbook/session-host.d.ts +9 -2
- package/slc/gears2fsm.md +17 -3
- package/slc/text2gears.md +7 -0
- package/src/xstate-playbook-runtime.js +5 -0
- package/src/xstate-playbook-runtime.ts +7 -0
|
@@ -446,12 +446,27 @@ export interface PlaybookCaptainShell extends Captain {
|
|
|
446
446
|
// normalized `captain.options.playbooks.<id>` role map.
|
|
447
447
|
interface Enablement {
|
|
448
448
|
entry: PlaybookCaptainRegistryEntry;
|
|
449
|
+
readonly configuredFrom: string;
|
|
449
450
|
artifactSchema: 3;
|
|
450
451
|
command: string;
|
|
451
452
|
options: JsonValue;
|
|
452
453
|
roleBindings: ReadonlyMap<string, EffectivePlayerBinding>;
|
|
453
454
|
}
|
|
454
455
|
|
|
456
|
+
function missingPackagedReview(
|
|
457
|
+
enablementById: ReadonlyMap<string, Enablement>,
|
|
458
|
+
playbookId: string,
|
|
459
|
+
): string | undefined {
|
|
460
|
+
const selected = enablementById.get(playbookId);
|
|
461
|
+
if (
|
|
462
|
+
selected === undefined ||
|
|
463
|
+
enablementById.has('review') ||
|
|
464
|
+
!['code', 'decide'].includes(playbookId) ||
|
|
465
|
+
selected.configuredFrom !== `@sublang/playbook/${playbookId}/registry`
|
|
466
|
+
) return undefined;
|
|
467
|
+
return `/${selected.command} requires enabled playbook "review"; enable REVIEW before starting this packaged workflow`;
|
|
468
|
+
}
|
|
469
|
+
|
|
455
470
|
function createRuntimeForEnablement(
|
|
456
471
|
enablement: Enablement,
|
|
457
472
|
hostCapabilitiesById: ReadonlyMap<
|
|
@@ -4181,6 +4196,7 @@ async function buildEnablements(
|
|
|
4181
4196
|
hostCapabilitiesById.set(entry.id, hostCapability);
|
|
4182
4197
|
enablementById.set(entry.id, {
|
|
4183
4198
|
entry,
|
|
4199
|
+
configuredFrom: from,
|
|
4184
4200
|
artifactSchema,
|
|
4185
4201
|
command,
|
|
4186
4202
|
options: validatedOptions,
|
|
@@ -6142,13 +6158,16 @@ export function createPlaybookCaptainShell(
|
|
|
6142
6158
|
enablement,
|
|
6143
6159
|
hostCapabilitiesById,
|
|
6144
6160
|
);
|
|
6161
|
+
// A nested child inherits only its parent's selection, never the turn's
|
|
6162
|
+
// unselected references; only a root reads the turn's selection.
|
|
6163
|
+
const inherited = parent ? parent.frame.attachments : (activeTurn?.selectedAttachments ?? activeTurn?.attachments);
|
|
6145
6164
|
return {
|
|
6146
6165
|
entry,
|
|
6147
6166
|
enablement,
|
|
6148
6167
|
runtime,
|
|
6149
6168
|
sessionId,
|
|
6150
6169
|
rootSessionId: parent?.frame.rootSessionId ?? sessionId,
|
|
6151
|
-
...(
|
|
6170
|
+
...(inherited?.length ? { attachments: [...inherited] } : {}),
|
|
6152
6171
|
depth: parent ? parent.frame.depth + 1 : 0,
|
|
6153
6172
|
playerBindings,
|
|
6154
6173
|
...(parent ? { parent } : {}),
|
|
@@ -6486,7 +6505,6 @@ export function createPlaybookCaptainShell(
|
|
|
6486
6505
|
} finally {
|
|
6487
6506
|
if (leafFrame() === frame) {
|
|
6488
6507
|
frames.pop();
|
|
6489
|
-
if (!frame.parent) pendingAttachments = [];
|
|
6490
6508
|
if (frame.parent) {
|
|
6491
6509
|
pendingChildParents.delete(frame.parent.frame);
|
|
6492
6510
|
}
|
|
@@ -6930,6 +6948,23 @@ export function createPlaybookCaptainShell(
|
|
|
6930
6948
|
return;
|
|
6931
6949
|
}
|
|
6932
6950
|
assertRetainableResult(frame, result);
|
|
6951
|
+
// A deferred checkpoint refusal changes the runtime's control overlay
|
|
6952
|
+
// without publishing an FSM transition. Refresh its derived question
|
|
6953
|
+
// mirror from an exact safe snapshot before reporting or saving the stop.
|
|
6954
|
+
if (leafFrame() === frame && !frame.disposing &&
|
|
6955
|
+
retainedEffectReconciliation === undefined) {
|
|
6956
|
+
try {
|
|
6957
|
+
const exported = frame.runtime.exportSnapshot?.();
|
|
6958
|
+
if (exported !== undefined) {
|
|
6959
|
+
const snapshot = assertPlaybookRuntimeSnapshot(exported, frame.entry.id);
|
|
6960
|
+
if (isDeepStrictEqual(snapshot.state, result.state) &&
|
|
6961
|
+
isDeepStrictEqual(snapshot.effectLedger, currentEffectLedger())) {
|
|
6962
|
+
pendingBossQuestions = snapshot.pendingBossQuestions.length === 0 && pendingBossQuestions === undefined
|
|
6963
|
+
? undefined : mirroredBossQuestions(snapshot.pendingBossQuestions);
|
|
6964
|
+
}
|
|
6965
|
+
}
|
|
6966
|
+
} catch { /* An inconsistent runtime must still fail exact capture. */ }
|
|
6967
|
+
}
|
|
6933
6968
|
if (result.outcome === 'aborted' && runFailureFacts) {
|
|
6934
6969
|
runFailureFacts.push(
|
|
6935
6970
|
`${frameLabel(frame)} was aborted before its outcome could be confirmed; it was not repeated automatically.`,
|
|
@@ -7011,6 +7046,9 @@ export function createPlaybookCaptainShell(
|
|
|
7011
7046
|
);
|
|
7012
7047
|
}
|
|
7013
7048
|
|
|
7049
|
+
const reviewRefusal = missingPackagedReview(enablementById, entry.id);
|
|
7050
|
+
if (reviewRefusal !== undefined) throw new Error(reviewRefusal);
|
|
7051
|
+
|
|
7014
7052
|
pendingChildParents.add(parent);
|
|
7015
7053
|
let child: EngagementFrame;
|
|
7016
7054
|
try {
|
|
@@ -9004,7 +9042,13 @@ export function createPlaybookCaptainShell(
|
|
|
9004
9042
|
// its decision-call prose crosses the presentation boundary. Acting
|
|
9005
9043
|
// selections freeze after their work and before reporting begins.
|
|
9006
9044
|
if (selection.action === 'respond') freezeControllerEvidence();
|
|
9007
|
-
|
|
9045
|
+
signal.throwIfAborted();
|
|
9046
|
+
const reviewRefusal = selection.action === 'start' || selection.action === 'switch'
|
|
9047
|
+
? missingPackagedReview(enablementById, selection.playbookId)
|
|
9048
|
+
: undefined;
|
|
9049
|
+
let settlement = reviewRefusal === undefined
|
|
9050
|
+
? await executeSelection(selection, signal, automaticRecovery)
|
|
9051
|
+
: await rejectSelection(selection, reviewRefusal);
|
|
9008
9052
|
// One controller action can include bounded prerequisite recovery.
|
|
9009
9053
|
// Neither the model nor a retry manufactures an unadvertised transition.
|
|
9010
9054
|
if (turn && abortPreparation && settlement.status !== 'rejected' && !['respond', 'dismiss', 'recover'].includes(selection.action)) {
|
|
@@ -9548,6 +9592,9 @@ export function createPlaybookCaptainShell(
|
|
|
9548
9592
|
const selected = selection.attachmentIds === undefined ? turn.attachments : selection.attachmentIds.map((id) => available.get(id as SessionAssetRef['assetId'])!);
|
|
9549
9593
|
const nextAttachments = mergeAttachments(leaf.attachments ?? [], selected);
|
|
9550
9594
|
if (nextAttachments.length) leaf.attachments = nextAttachments;
|
|
9595
|
+
// Delivered references now belong to the leaf; the rest stay pending.
|
|
9596
|
+
const delivered = new Set(selected.map((reference) => reference.assetId));
|
|
9597
|
+
pendingAttachments = pendingAttachments.filter((reference) => !delivered.has(reference.assetId));
|
|
9551
9598
|
// CAPTAIN-8: delivery carries text only, and the shell is authoritative
|
|
9552
9599
|
// for that text — any text carried on the selection is ignored.
|
|
9553
9600
|
turn.settled = true;
|
|
@@ -102,9 +102,11 @@ export function createAssetStore(options) {
|
|
|
102
102
|
try {
|
|
103
103
|
const opened = await directoryHandle.stat();
|
|
104
104
|
if (opened.dev !== before.dev || opened.ino !== before.ino) throw new Error('asset owner directory changed during preparation');
|
|
105
|
-
|
|
105
|
+
// fchmod always moves ctime, which verified readers pin: change only modes that differ.
|
|
106
|
+
if (process.platform !== 'win32' && (opened.mode & 0o777) !== 0o700) await directoryHandle.chmod(0o700);
|
|
106
107
|
for (const name of await readdir(directory)) {
|
|
107
|
-
|
|
108
|
+
// Foreign names (Finder or editor files) are neither assets nor ours to modify.
|
|
109
|
+
if (!/^(?:[0-9a-f]{64}(?:\.json)?|\.import-[0-9a-f-]+\.tmp)$/.test(name)) continue;
|
|
108
110
|
const path = join(directory, name);
|
|
109
111
|
const stat = await lstat(path);
|
|
110
112
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) throw new Error('asset entry must be a single-link regular file');
|
|
@@ -114,7 +116,7 @@ export function createAssetStore(options) {
|
|
|
114
116
|
try {
|
|
115
117
|
const pinned = await handle.stat();
|
|
116
118
|
if (!sameFile(stat, pinned)) throw new Error('asset entry changed during preparation');
|
|
117
|
-
if (process.platform !== 'win32') await handle.chmod(0o600);
|
|
119
|
+
if (process.platform !== 'win32' && (pinned.mode & 0o777) !== 0o600) await handle.chmod(0o600);
|
|
118
120
|
assertFile(await handle.stat(), 'asset entry');
|
|
119
121
|
const final = await lstat(path);
|
|
120
122
|
if (final.dev !== pinned.dev || final.ino !== pinned.ino || final.nlink !== 1) throw new Error('asset entry changed during preparation');
|
|
@@ -286,9 +288,8 @@ export function createAssetStore(options) {
|
|
|
286
288
|
const entries = await readdir(directory);
|
|
287
289
|
const result = [];
|
|
288
290
|
for (const name of entries.sort()) {
|
|
289
|
-
|
|
290
|
-
if (
|
|
291
|
-
if (!/^[0-9a-f]{64}$/.test(name)) throw new Error(`unknown asset owner entry ${JSON.stringify(basename(name))}`);
|
|
291
|
+
// Descriptors, import temporaries and foreign files are not enumerated.
|
|
292
|
+
if (!/^[0-9a-f]{64}$/.test(name)) continue;
|
|
292
293
|
if (!entries.includes(`${name}.json`)) { const orphan = await checkedOpen(name, 'unreferenced asset content'); await orphan.handle.close(); continue; }
|
|
293
294
|
const reader = await openAsset(`sha256:${name}`); await reader.close();
|
|
294
295
|
result.push(Object.freeze({ assetId: reader.reference.assetId, path: name, metadataPath: `${name}.json`, byteLength: reader.reference.byteLength }));
|
|
@@ -43,8 +43,15 @@ export declare function installRetainedGenerationsForLaunch(options: {
|
|
|
43
43
|
export declare function executionConfigFromPlan(plan: any): SessionExecutionProjection;
|
|
44
44
|
export declare function validateFrozenExecutionConfig(structural: SessionStructuralProjection, execution: SessionExecutionProjection, dependencies: { loadModule: (specifier: string) => Promise<any>; prepareRegistryModule?: (request: {id: string; from: string; authoredFrom: string}) => Promise<string | void> }): Promise<SessionExecutionProjection>;
|
|
45
45
|
export declare function driveHeadlessCaptainTurn(options: any): Promise<any>;
|
|
46
|
-
|
|
47
|
-
export
|
|
46
|
+
/** Playbook id to registry module specifier, supplied at launch ahead of `playbooks.<id>.from` (DR-083). */
|
|
47
|
+
export type PlaybookModuleLocations = Readonly<Record<string, string>>;
|
|
48
|
+
export interface LaunchPlanModuleOptions {
|
|
49
|
+
/** Absolute paths become file URLs; file URLs and package specifiers are kept; relative paths are refused. */
|
|
50
|
+
readonly modules?: PlaybookModuleLocations;
|
|
51
|
+
readonly [key: string]: any;
|
|
52
|
+
}
|
|
53
|
+
export declare function normalizeLaunchPlan(top: any, options?: LaunchPlanModuleOptions): Promise<any>;
|
|
54
|
+
export declare function loadLaunchPlan(options: LaunchPlanModuleOptions): Promise<any>;
|
|
48
55
|
export declare function composeGenericConfig(top: any, loadModule: (specifier: string) => Promise<any>, configPath?: string): Promise<any>;
|
|
49
56
|
export declare function projectTmuxConfig(plan: any): any;
|
|
50
57
|
export declare function resolveLaunchSessionsDir(options: any): string;
|
package/slc/gears2fsm.md
CHANGED
|
@@ -602,6 +602,9 @@ safety beyond the guarded direct transition.
|
|
|
602
602
|
## Context and prompts
|
|
603
603
|
|
|
604
604
|
Context fields used to drive guards or compose prompts shall be **typed and named**.
|
|
605
|
+
Where Source owns array-valued retained discussion whose initial state is no history, initialize that context field to `[]` and reset it to `[]` only at its Source lifecycle boundaries.
|
|
606
|
+
Carry its declared array shape through ordinary and continuation `invoke.input`; do not leave the empty structural history absent so a prompt-contract probe mistakes it for a scalar.
|
|
607
|
+
This empty history is not a default caller intent, prerequisite report, domain evidence, approval, or prior-stage result; those remain unavailable until actually established.
|
|
605
608
|
The compiler shall not branch on untyped properties of `lastResult`; persistent routing decisions belong in typed context fields. (`lastResult` is for inspection only.)
|
|
606
609
|
Where Source declares a finite ordered plan, represent it as a typed readonly
|
|
607
610
|
JSON-safe array and validate that shape on the actor-output transition; an
|
|
@@ -835,8 +838,8 @@ Only
|
|
|
835
838
|
The canonical storage paths are `context.pendingBossQuestion` and `context.bossReply` for the scalar form, or `context.pendingBossQuestions[stateId]` and `context.bossReplies[stateId]` for the keyed form.
|
|
836
839
|
A private wrapper such as `context.continuation` shall not replace these fields directly on machine context.
|
|
837
840
|
|
|
838
|
-
A machine
|
|
839
|
-
|
|
841
|
+
A machine that declares no root parallel group shall use the scalar form;
|
|
842
|
+
keyed storage alone is not visible to the flat shared factory:
|
|
840
843
|
|
|
841
844
|
- An `awaitBossReply` state with stable `id: 'awaitBossReply'`, tag
|
|
842
845
|
`playbook.parked`, and description
|
|
@@ -847,7 +850,9 @@ form:
|
|
|
847
850
|
- `resumableStates(ids)`, `setPendingBossQuestion`, and
|
|
848
851
|
`clearBossReplyContext` helpers with the existing single-question behavior.
|
|
849
852
|
|
|
850
|
-
A machine
|
|
853
|
+
A machine that declares a root parallel group shall use the keyed form
|
|
854
|
+
throughout the machine, including any sequential working leaves and their
|
|
855
|
+
root reply wait:
|
|
851
856
|
|
|
852
857
|
- One local waiting leaf per branch, tagged `playbook.parked`.
|
|
853
858
|
- A `BOSS_REPLY` event carrying `{ questionId: string; answer: string }`.
|
|
@@ -884,6 +889,15 @@ pending question and reply selected for that working leaf as singular
|
|
|
884
889
|
`pendingBossQuestion` and `bossReply` fields, regardless of the scalar or keyed
|
|
885
890
|
context representation, so prompt composition has one stable contract.
|
|
886
891
|
When Source requires earlier discussion or constraints on a later invocation, the compiler shall persist that source-owned history in serializable machine context before replacing or clearing the pending Q/A fields.
|
|
892
|
+
Where that Source-owned value is the complete received Boss reply, retain it
|
|
893
|
+
directly from the current working leaf's canonical reply context at the
|
|
894
|
+
Source-authored accepted-outcome boundary, before clearing that leaf's Q/A.
|
|
895
|
+
Preserve its exact string, including surrounding whitespace, CRLF/LF separators,
|
|
896
|
+
blank lines and trailing separators; do not require a model-produced echo or
|
|
897
|
+
replace it with one. Acceptance remains the acting agent's semantic judgment,
|
|
898
|
+
guarded by the current matching question's state, Source-item and asker binding
|
|
899
|
+
and actual receipt of a nonempty reply. Neither stale question/reply context
|
|
900
|
+
nor a different actor-supplied value establishes that binding or approval.
|
|
887
901
|
The later `invoke.input` shall relay that persisted context on both resumed and fresh calls that Source says need it, preserving Source-owned relevance and format without imposing all-history semantics on sources that do not require it.
|
|
888
902
|
Shared Boss-reply continuation carries only the latest Q/A pair, and a backend continuation token is not durable Source history.
|
|
889
903
|
When both fields are present, the linked runtime shall compose the continuation preamble and labelled Q&A blocks per [link.md "Player prompt composition"](link.md#player-prompt-composition).
|
package/slc/text2gears.md
CHANGED
|
@@ -189,6 +189,13 @@ between the Source and the catalog: leave the Target unwritten and report it
|
|
|
189
189
|
as an incompatible compiler input rather than rename or invent.
|
|
190
190
|
A Source outside those ids is named from its own words as above.
|
|
191
191
|
|
|
192
|
+
A value Source assigns to the received Boss input or reply is already runtime
|
|
193
|
+
input, not a value the acting agent produces. Where Source requires that
|
|
194
|
+
complete value retained for later calls, preserve the exact input's retention
|
|
195
|
+
and the Source's acceptance condition in non-acting GEARS prose; do not declare
|
|
196
|
+
a Judge-extracted echo property for it. The semantic approval or refusal stays
|
|
197
|
+
in the acting result contract.
|
|
198
|
+
|
|
192
199
|
A produced value consumed later shall have a declared producer: where any
|
|
193
200
|
later item's blockquote reads a value through a `<placeholder>`, the item
|
|
194
201
|
whose behavior produces that value shall declare the `Results:` contract
|
|
@@ -866,12 +866,16 @@ export function defaultExtractRequiredFields(description) {
|
|
|
866
866
|
fields.push(m[1]);
|
|
867
867
|
return fields;
|
|
868
868
|
}
|
|
869
|
+
const CURRENT_CALL_ADJUDICATION_GUIDANCE = "Classify the invoked role's current call by the result it reports at call end, using the full output and declared outcome meanings. " +
|
|
870
|
+
'An earlier interim question is not outstanding when later prose explicitly resolves or supersedes it for this call; a prerequisite only for a later workflow stage does not block an affirmatively completed current call. ' +
|
|
871
|
+
'A genuinely unanswered question needed to finish the current call remains outstanding. A commit statement alone does not establish semantic completion; do not force contradictory or insufficient evidence into a completed outcome.';
|
|
869
872
|
/** Default delegated-player adjudicator prompt. */
|
|
870
873
|
export function defaultBuildJudgePrompt(input, finalText) {
|
|
871
874
|
const lines = [];
|
|
872
875
|
lines.push('This is hidden control work. Do not call tools, inspect files, or ' +
|
|
873
876
|
'seek external evidence. Decide only from the supplied player output ' +
|
|
874
877
|
'and outcome descriptions. Reply with exactly one JSON object and no prose.');
|
|
878
|
+
lines.push(CURRENT_CALL_ADJUDICATION_GUIDANCE);
|
|
875
879
|
lines.push('');
|
|
876
880
|
lines.push(`The ${input.role} role just produced this output:`);
|
|
877
881
|
lines.push('');
|
|
@@ -957,6 +961,7 @@ function buildGovernedJudgePrompt(input, finalText, outcomes, correction) {
|
|
|
957
961
|
const lines = [
|
|
958
962
|
'This is hidden control work. Do not call tools, inspect files, or seek external evidence.',
|
|
959
963
|
'Decide only from the supplied player output and declared outcomes.',
|
|
964
|
+
CURRENT_CALL_ADJUDICATION_GUIDANCE,
|
|
960
965
|
'Reply with exactly one JSON object and no prose.',
|
|
961
966
|
'',
|
|
962
967
|
`The ${input.role} role just produced this output:`,
|
|
@@ -1706,6 +1706,11 @@ export function defaultExtractRequiredFields(description: string): string[] {
|
|
|
1706
1706
|
return fields;
|
|
1707
1707
|
}
|
|
1708
1708
|
|
|
1709
|
+
const CURRENT_CALL_ADJUDICATION_GUIDANCE =
|
|
1710
|
+
"Classify the invoked role's current call by the result it reports at call end, using the full output and declared outcome meanings. " +
|
|
1711
|
+
'An earlier interim question is not outstanding when later prose explicitly resolves or supersedes it for this call; a prerequisite only for a later workflow stage does not block an affirmatively completed current call. ' +
|
|
1712
|
+
'A genuinely unanswered question needed to finish the current call remains outstanding. A commit statement alone does not establish semantic completion; do not force contradictory or insufficient evidence into a completed outcome.';
|
|
1713
|
+
|
|
1709
1714
|
/** Default delegated-player adjudicator prompt. */
|
|
1710
1715
|
export function defaultBuildJudgePrompt(
|
|
1711
1716
|
input: PlaybookPlayerInput,
|
|
@@ -1717,6 +1722,7 @@ export function defaultBuildJudgePrompt(
|
|
|
1717
1722
|
'seek external evidence. Decide only from the supplied player output ' +
|
|
1718
1723
|
'and outcome descriptions. Reply with exactly one JSON object and no prose.',
|
|
1719
1724
|
);
|
|
1725
|
+
lines.push(CURRENT_CALL_ADJUDICATION_GUIDANCE);
|
|
1720
1726
|
lines.push('');
|
|
1721
1727
|
lines.push(`The ${input.role} role just produced this output:`);
|
|
1722
1728
|
lines.push('');
|
|
@@ -1822,6 +1828,7 @@ function buildGovernedJudgePrompt(
|
|
|
1822
1828
|
const lines = [
|
|
1823
1829
|
'This is hidden control work. Do not call tools, inspect files, or seek external evidence.',
|
|
1824
1830
|
'Decide only from the supplied player output and declared outcomes.',
|
|
1831
|
+
CURRENT_CALL_ADJUDICATION_GUIDANCE,
|
|
1825
1832
|
'Reply with exactly one JSON object and no prose.',
|
|
1826
1833
|
'',
|
|
1827
1834
|
`The ${input.role} role just produced this output:`,
|