@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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
3
|
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { posix, win32 } from 'node:path';
|
|
5
6
|
import { isDeepStrictEqual } from 'node:util';
|
|
6
7
|
import PQueue from 'p-queue';
|
|
7
8
|
|
|
@@ -69,6 +70,7 @@ import { QUOTED_EVIDENCE_LIMIT, workerReportExcerpt, workerReportBlock } from '.
|
|
|
69
70
|
interface SessionAgent {
|
|
70
71
|
/** Mutable next-turn execution capability; never structural identity. */
|
|
71
72
|
readonly browser?: boolean;
|
|
73
|
+
readonly skills?: readonly string[];
|
|
72
74
|
readonly adapter: string;
|
|
73
75
|
readonly model: TuningSelection;
|
|
74
76
|
readonly effort: TuningSelection<Effort>;
|
|
@@ -127,7 +129,7 @@ interface PlaybookCaptainUnresolvedEffectSettlementInput {
|
|
|
127
129
|
readonly unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[];
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
type SnapshotAgentEnvelope = DeepReadonly<Omit<SessionAgent, TuningKey | 'browser'>>;
|
|
132
|
+
type SnapshotAgentEnvelope = DeepReadonly<Omit<SessionAgent, TuningKey | 'browser' | 'skills'>>;
|
|
131
133
|
|
|
132
134
|
type PlayerLedgerSnapshotEntry = DeepReadonly<PlayerLedgerEntry>;
|
|
133
135
|
|
|
@@ -1414,6 +1416,31 @@ function boundedPathList(paths: readonly string[]): string {
|
|
|
1414
1416
|
: printed.join(', ');
|
|
1415
1417
|
}
|
|
1416
1418
|
|
|
1419
|
+
// DR-090 §5: the report lines the shell appends to a reply carry marks and
|
|
1420
|
+
// identifiers, never an English sentence, so a reply in any language reads
|
|
1421
|
+
// them as structure. The settlement facts a model reads keep their words.
|
|
1422
|
+
const MARK_DONE = '✓';
|
|
1423
|
+
const MARK_FAILED = '✕';
|
|
1424
|
+
const MARK_TO = '⟶';
|
|
1425
|
+
const MARK_BRANCH = '⎇';
|
|
1426
|
+
const MARK_SEP = '·';
|
|
1427
|
+
const MARK_READY = '▸';
|
|
1428
|
+
const MARK_NOT_READY = '▹';
|
|
1429
|
+
|
|
1430
|
+
/** At most 24 items, then `+<n>` for the rest (DR-090 §5). */
|
|
1431
|
+
function markedList(items: readonly string[], separator = ', ', more = 0): string {
|
|
1432
|
+
const printed = items.slice(0, CARRIED_CHANGE_PATH_LIMIT);
|
|
1433
|
+
const omitted = items.length - printed.length + more;
|
|
1434
|
+
if (printed.length === 0) return omitted > 0 ? `+${omitted}` : '';
|
|
1435
|
+
return omitted > 0
|
|
1436
|
+
? `${printed.join(separator)}${separator}+${omitted}`
|
|
1437
|
+
: printed.join(separator);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function markedBranch(branch: string | undefined): string {
|
|
1441
|
+
return branch === undefined ? '' : ` ${MARK_BRANCH} ${branch}`;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1417
1444
|
/**
|
|
1418
1445
|
* The carried pre-existing changes of the boundaries completed after
|
|
1419
1446
|
* `boundaryPrefixCount`, one entry per commit OID in boundary sequence order.
|
|
@@ -1499,6 +1526,14 @@ interface CommitBeforeFailure {
|
|
|
1499
1526
|
readonly commitOid: string;
|
|
1500
1527
|
/** Every new commit, oldest first, where the step made more than one (DR-088). */
|
|
1501
1528
|
readonly commits?: readonly string[];
|
|
1529
|
+
/** The branch the commits landed on, where the receipt names one (DR-090 §4). */
|
|
1530
|
+
readonly branch?: string;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/** DR-090 §4: where a step's commits landed, as its receipt names it. */
|
|
1534
|
+
function onBranch(receipt: PlaybookRepositoryReceipt): string {
|
|
1535
|
+
const branch = receipt.after?.branch;
|
|
1536
|
+
return branch === undefined ? '' : ` on branch ${branch}`;
|
|
1502
1537
|
}
|
|
1503
1538
|
|
|
1504
1539
|
function commitsBeforeFailure(
|
|
@@ -1534,6 +1569,9 @@ function commitsBeforeFailure(
|
|
|
1534
1569
|
baselineHead: receipt.baseline.head,
|
|
1535
1570
|
commitOid: receipt.commitOid,
|
|
1536
1571
|
...(receipt.commits === undefined ? {} : { commits: receipt.commits }),
|
|
1572
|
+
...(receipt.after?.branch === undefined
|
|
1573
|
+
? {}
|
|
1574
|
+
: { branch: receipt.after.branch }),
|
|
1537
1575
|
});
|
|
1538
1576
|
}
|
|
1539
1577
|
return commits;
|
|
@@ -1544,29 +1582,35 @@ function commitBeforeFailureClause({
|
|
|
1544
1582
|
baselineHead,
|
|
1545
1583
|
commitOid,
|
|
1546
1584
|
commits,
|
|
1585
|
+
branch,
|
|
1547
1586
|
}: CommitBeforeFailure): string {
|
|
1548
1587
|
const made =
|
|
1549
1588
|
commits === undefined
|
|
1550
1589
|
? 'made one new commit'
|
|
1551
1590
|
: `made ${commits.length} new commits, oldest first: ${boundedPathList(commits)}`;
|
|
1552
|
-
|
|
1591
|
+
const landed = branch === undefined ? '' : ` on branch ${branch}`;
|
|
1592
|
+
return `${made}; HEAD moved from ${baselineHead} to the proven commit ${commitOid}${landed}.`;
|
|
1553
1593
|
}
|
|
1554
1594
|
|
|
1555
1595
|
function commitBeforeFailureFact(commit: CommitBeforeFailure): string {
|
|
1556
1596
|
return `Before the failure, a step ${commitBeforeFailureClause(commit)}`;
|
|
1557
1597
|
}
|
|
1558
1598
|
|
|
1599
|
+
// DR-090 §5: one marked line per receipt, `✓ <baselineHead> ⟶ <commits>`
|
|
1600
|
+
// and the branch they landed on.
|
|
1559
1601
|
function commitsBeforeFailureReport(
|
|
1560
1602
|
commits: readonly CommitBeforeFailure[],
|
|
1561
1603
|
): string | undefined {
|
|
1562
1604
|
if (commits.length === 0) return undefined;
|
|
1563
|
-
return
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1605
|
+
return commits
|
|
1606
|
+
.map(
|
|
1607
|
+
({ baselineHead, commitOid, commits: listed, branch }) =>
|
|
1608
|
+
`${MARK_DONE} ${baselineHead} ${MARK_TO} ${markedList(
|
|
1609
|
+
listed ?? [commitOid],
|
|
1610
|
+
` ${MARK_SEP} `,
|
|
1611
|
+
)}${markedBranch(branch)}`,
|
|
1612
|
+
)
|
|
1613
|
+
.join('\n');
|
|
1570
1614
|
}
|
|
1571
1615
|
|
|
1572
1616
|
/**
|
|
@@ -1583,19 +1627,29 @@ function finalTextNamesCommit(finalText: string, oid: string): boolean {
|
|
|
1583
1627
|
return false;
|
|
1584
1628
|
}
|
|
1585
1629
|
|
|
1630
|
+
/** One step's commit statement and the reply's marked line for it (DR-088 §3, DR-090 §4, §5). */
|
|
1631
|
+
interface StepCommitStatement {
|
|
1632
|
+
readonly text: string;
|
|
1633
|
+
/** The marked line appended to the presented reply, where one is. */
|
|
1634
|
+
readonly shown?: string;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1586
1637
|
/**
|
|
1587
|
-
* DR-088 §3: what the steps completed after `boundaryPrefixCount`
|
|
1588
|
-
* one statement each, in boundary sequence order
|
|
1589
|
-
*
|
|
1590
|
-
*
|
|
1591
|
-
*
|
|
1592
|
-
*
|
|
1638
|
+
* DR-088 §3, DR-090 §4: what the steps completed after `boundaryPrefixCount`
|
|
1639
|
+
* committed, one statement each, in boundary sequence order, naming the
|
|
1640
|
+
* branch where the receipt names one: every new commit where a receipt
|
|
1641
|
+
* proves more than one, the commit already at HEAD where a step whose
|
|
1642
|
+
* outcomes all commit or ask named it over an `unchanged` receipt, and —
|
|
1643
|
+
* as a fact for the closing reply only, and only where `oneCommit` asks —
|
|
1644
|
+
* the one new commit of a step that made one. A step in a deferred chain
|
|
1645
|
+
* reads the chain's cumulative receipt at its last boundary.
|
|
1593
1646
|
*/
|
|
1594
1647
|
function stepCommitStatements(
|
|
1595
1648
|
ledger: PlaybookEffectLedger,
|
|
1596
1649
|
boundaryPrefixCount: number,
|
|
1597
|
-
|
|
1598
|
-
|
|
1650
|
+
oneCommit = true,
|
|
1651
|
+
): readonly StepCommitStatement[] {
|
|
1652
|
+
const statements: StepCommitStatement[] = [];
|
|
1599
1653
|
for (const boundary of ledger.boundaries) {
|
|
1600
1654
|
if (boundary.sequence <= boundaryPrefixCount) continue;
|
|
1601
1655
|
let receipt = boundary.physicalReceipt;
|
|
@@ -1611,9 +1665,23 @@ function stepCommitStatements(
|
|
|
1611
1665
|
receipt.classification === 'one-descendant-commit' &&
|
|
1612
1666
|
receipt.commits !== undefined
|
|
1613
1667
|
) {
|
|
1614
|
-
statements.push(
|
|
1615
|
-
`The step made ${receipt.commits.length} new commits, oldest first: ${boundedPathList(receipt.commits)}.`,
|
|
1616
|
-
|
|
1668
|
+
statements.push({
|
|
1669
|
+
text: `The step made ${receipt.commits.length} new commits${onBranch(receipt)}, oldest first: ${boundedPathList(receipt.commits)}.`,
|
|
1670
|
+
shown: `${MARK_DONE} ${markedList(receipt.commits, ` ${MARK_SEP} `)}${markedBranch(receipt.after?.branch)}`,
|
|
1671
|
+
});
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
if (
|
|
1675
|
+
receipt.classification === 'one-descendant-commit' &&
|
|
1676
|
+
receipt.commitOid !== undefined
|
|
1677
|
+
) {
|
|
1678
|
+
if (oneCommit) {
|
|
1679
|
+
statements.push({
|
|
1680
|
+
text: `The step made one new commit, ${receipt.commitOid}${
|
|
1681
|
+
receipt.after?.branch === undefined ? '' : `,${onBranch(receipt)}`
|
|
1682
|
+
}.`,
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1617
1685
|
continue;
|
|
1618
1686
|
}
|
|
1619
1687
|
const candidate = boundary.semanticCandidate;
|
|
@@ -1634,9 +1702,10 @@ function stepCommitStatements(
|
|
|
1634
1702
|
guard !== BOSS_QUESTION_OUTCOME &&
|
|
1635
1703
|
finalTextNamesCommit(boundary.finalText?.trim() ?? '', head)
|
|
1636
1704
|
) {
|
|
1637
|
-
statements.push(
|
|
1638
|
-
`The step made no new commit; it named ${head}, already at HEAD, as its work.`,
|
|
1639
|
-
|
|
1705
|
+
statements.push({
|
|
1706
|
+
text: `The step made no new commit; it named ${head}, already at HEAD${onBranch(receipt)}, as its work.`,
|
|
1707
|
+
shown: `${MARK_DONE} = ${head}${markedBranch(receipt.after?.branch)}`,
|
|
1708
|
+
});
|
|
1640
1709
|
}
|
|
1641
1710
|
}
|
|
1642
1711
|
return statements;
|
|
@@ -1648,16 +1717,6 @@ function stepCommitStatements(
|
|
|
1648
1717
|
// content, player prose, or internal identity: paths, revisions, dispositions,
|
|
1649
1718
|
// and the reported error message are what a cause carries.
|
|
1650
1719
|
|
|
1651
|
-
/** The Boss-facing phrase for each closed control-action reason (DR-063 §3). */
|
|
1652
|
-
const CONTROL_REASON_PHRASES: Readonly<Record<string, string>> = {
|
|
1653
|
-
'receipt-complete': 'nothing has changed since it failed',
|
|
1654
|
-
};
|
|
1655
|
-
|
|
1656
|
-
function controlReasonPhrase(reason: string | undefined): string {
|
|
1657
|
-
if (reason === undefined) return 'no reason was published';
|
|
1658
|
-
return CONTROL_REASON_PHRASES[reason] ?? reason;
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
1720
|
function causePathList(
|
|
1662
1721
|
paths: readonly string[] | undefined,
|
|
1663
1722
|
truncated: number | undefined,
|
|
@@ -2170,11 +2229,70 @@ function failureControlLine(action: {
|
|
|
2170
2229
|
readonly standing?: PlaybookControlStanding;
|
|
2171
2230
|
readonly reason?: string;
|
|
2172
2231
|
}): string {
|
|
2173
|
-
// DR-063 §3: an action that publishes no standing is `ready`.
|
|
2232
|
+
// DR-063 §3: an action that publishes no standing is `ready`. DR-090 §5:
|
|
2233
|
+
// the standing and its reason read as their closed identifiers.
|
|
2174
2234
|
const standing = action.standing ?? 'ready';
|
|
2175
2235
|
return standing === 'ready'
|
|
2176
|
-
?
|
|
2177
|
-
:
|
|
2236
|
+
? `${MARK_READY} ${action.label}`
|
|
2237
|
+
: `${MARK_NOT_READY} ${action.label} ${MARK_SEP} ${standing}${
|
|
2238
|
+
action.reason === undefined ? '' : ` ${MARK_SEP} ${action.reason}`
|
|
2239
|
+
}`;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
/**
|
|
2243
|
+
* DR-090 §5: the failure line's marked record of a cause — its code and the
|
|
2244
|
+
* identities its evidence carries, a nested cause after its playbook. No
|
|
2245
|
+
* recorded reason or message reaches it: the settlement facts the closing
|
|
2246
|
+
* reply reads state the failure in words (CAPTAIN-71).
|
|
2247
|
+
*/
|
|
2248
|
+
function markedFailure(
|
|
2249
|
+
cause: PlaybookFailureCause,
|
|
2250
|
+
describeNestedPlaybook: (playbookId: string) => string,
|
|
2251
|
+
): string {
|
|
2252
|
+
const { evidence } = cause;
|
|
2253
|
+
const parts: string[] = [cause.code];
|
|
2254
|
+
if (evidence.roleId !== undefined) parts.push(evidence.roleId);
|
|
2255
|
+
if (evidence.playbookId !== undefined) {
|
|
2256
|
+
parts.push(describeNestedPlaybook(evidence.playbookId));
|
|
2257
|
+
}
|
|
2258
|
+
if (
|
|
2259
|
+
cause.code === 'attribution-ambiguous' &&
|
|
2260
|
+
evidence.required !== undefined &&
|
|
2261
|
+
evidence.observed !== undefined
|
|
2262
|
+
) {
|
|
2263
|
+
parts.push(`${evidence.required} ${MARK_TO} ${evidence.observed}`);
|
|
2264
|
+
}
|
|
2265
|
+
if (evidence.baselineHead !== undefined) {
|
|
2266
|
+
parts.push(
|
|
2267
|
+
evidence.afterHead === undefined
|
|
2268
|
+
? evidence.baselineHead
|
|
2269
|
+
: `${evidence.baselineHead} ${MARK_TO} ${evidence.afterHead}`,
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
2272
|
+
if (
|
|
2273
|
+
evidence.commitOid !== undefined &&
|
|
2274
|
+
evidence.commitOid !== evidence.afterHead
|
|
2275
|
+
) {
|
|
2276
|
+
parts.push(evidence.commitOid);
|
|
2277
|
+
}
|
|
2278
|
+
const paths = evidence.paths;
|
|
2279
|
+
if (paths !== undefined) {
|
|
2280
|
+
const listed = markedList(
|
|
2281
|
+
[
|
|
2282
|
+
...(paths.uncommitted ?? []),
|
|
2283
|
+
...(paths.altered ?? []),
|
|
2284
|
+
...(paths.lost ?? []),
|
|
2285
|
+
...(paths.changed ?? []),
|
|
2286
|
+
],
|
|
2287
|
+
', ',
|
|
2288
|
+
paths.truncated ?? 0,
|
|
2289
|
+
);
|
|
2290
|
+
if (listed !== '') parts.push(listed);
|
|
2291
|
+
}
|
|
2292
|
+
if (evidence.cause !== undefined) {
|
|
2293
|
+
parts.push(markedFailure(evidence.cause, describeNestedPlaybook));
|
|
2294
|
+
}
|
|
2295
|
+
return parts.join(` ${MARK_SEP} `);
|
|
2178
2296
|
}
|
|
2179
2297
|
|
|
2180
2298
|
/** A report appended to the visible reply (CAPTAIN-69). */
|
|
@@ -2192,7 +2310,11 @@ type PresentationReport =
|
|
|
2192
2310
|
| { readonly kind: 'effects'; readonly lines: readonly string[] };
|
|
2193
2311
|
|
|
2194
2312
|
interface FailureReport {
|
|
2195
|
-
/** The
|
|
2313
|
+
/** The failure line's marked record (DR-090 §5). */
|
|
2314
|
+
readonly mark: string;
|
|
2315
|
+
/** The cause's code, the whole record where the mark's words fail CAPTAIN-9. */
|
|
2316
|
+
readonly code: PlaybookFailureCode;
|
|
2317
|
+
/** The failure statement in words, which the settlement facts carry. */
|
|
2196
2318
|
readonly sentence: string;
|
|
2197
2319
|
/** The reason the line states, which weighs it (CAPTAIN-69). */
|
|
2198
2320
|
readonly reason: string;
|
|
@@ -2319,10 +2441,7 @@ function guardedStatement(
|
|
|
2319
2441
|
};
|
|
2320
2442
|
for (const reason of phrased) {
|
|
2321
2443
|
if (!fails(reason)) continue;
|
|
2322
|
-
guarded =
|
|
2323
|
-
guarded === `Failure: ${reason}`
|
|
2324
|
-
? `Failure: the step failed with ${WITHHELD_REASON}.`
|
|
2325
|
-
: guarded.split(`: ${reason}`).join(` with ${WITHHELD_REASON}.`);
|
|
2444
|
+
guarded = guarded.split(`: ${reason}`).join(` with ${WITHHELD_REASON}.`);
|
|
2326
2445
|
}
|
|
2327
2446
|
for (const subject of shellWords.subjects) {
|
|
2328
2447
|
if (guarded.startsWith(`${subject} `) && fails(subject)) {
|
|
@@ -2353,8 +2472,11 @@ function assembleStatements(
|
|
|
2353
2472
|
guard: (text: string) => string = (text) => text,
|
|
2354
2473
|
): readonly (string | undefined)[] {
|
|
2355
2474
|
const rendered = [...new Set(reasons)];
|
|
2475
|
+
// DR-090 §5: the marked failure line is no sentence and gains no mark.
|
|
2476
|
+
const finish = (kind: string, text: string): string =>
|
|
2477
|
+
kind === 'failure' ? text.trim() : oneTerminalMark(text);
|
|
2356
2478
|
const weighed = statements.map(({ text, kind = 'fact', reason }) => {
|
|
2357
|
-
const statement =
|
|
2479
|
+
const statement = finish(kind, text);
|
|
2358
2480
|
if (reason !== undefined) return { text: statement, kind, own: [reason] };
|
|
2359
2481
|
const held = rendered.filter((candidate) =>
|
|
2360
2482
|
alreadyStates(statement, candidate),
|
|
@@ -2378,7 +2500,7 @@ function assembleStatements(
|
|
|
2378
2500
|
return weighed.map(({ text, kind, own }, index) =>
|
|
2379
2501
|
stays[index] &&
|
|
2380
2502
|
(kind !== 'failure' || !carried(own, [...entries, ...facts]))
|
|
2381
|
-
?
|
|
2503
|
+
? finish(kind, guard(text))
|
|
2382
2504
|
: undefined,
|
|
2383
2505
|
);
|
|
2384
2506
|
}
|
|
@@ -2431,13 +2553,11 @@ function presentationSuffix(
|
|
|
2431
2553
|
});
|
|
2432
2554
|
const failure = turn.failureReport;
|
|
2433
2555
|
if (failure !== undefined) {
|
|
2434
|
-
|
|
2435
|
-
[
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
].join('\n'),
|
|
2440
|
-
);
|
|
2556
|
+
const lines = [
|
|
2557
|
+
...(assembled.failure === undefined ? [] : [assembled.failure]),
|
|
2558
|
+
...failure.controls,
|
|
2559
|
+
];
|
|
2560
|
+
if (lines.length > 0) parts.push(lines.join('\n'));
|
|
2441
2561
|
}
|
|
2442
2562
|
return parts.length === 0 ? undefined : parts.join('\n\n');
|
|
2443
2563
|
}
|
|
@@ -3941,6 +4061,13 @@ function snapshotNonblankTuning(
|
|
|
3941
4061
|
return value;
|
|
3942
4062
|
}
|
|
3943
4063
|
|
|
4064
|
+
function snapshotSkillLocators(value: JsonValue, path: string): readonly string[] {
|
|
4065
|
+
if (!Array.isArray(value) || value.some(item => typeof item !== 'string' || item.includes('\0') || !(posix.isAbsolute(item) || win32.isAbsolute(item)))) {
|
|
4066
|
+
throw new Error(`${path} must be an array of absolute skill paths`);
|
|
4067
|
+
}
|
|
4068
|
+
return [...value] as string[];
|
|
4069
|
+
}
|
|
4070
|
+
|
|
3944
4071
|
function snapshotSessionAgent(
|
|
3945
4072
|
value: JsonValue | undefined,
|
|
3946
4073
|
path: string,
|
|
@@ -3956,6 +4083,7 @@ function snapshotSessionAgent(
|
|
|
3956
4083
|
'subagentModel',
|
|
3957
4084
|
'subagentEffort',
|
|
3958
4085
|
'browser',
|
|
4086
|
+
'skills',
|
|
3959
4087
|
'instruction',
|
|
3960
4088
|
'permissions',
|
|
3961
4089
|
],
|
|
@@ -3970,7 +4098,8 @@ function snapshotSessionAgent(
|
|
|
3970
4098
|
key !== 'fastMode' &&
|
|
3971
4099
|
key !== 'subagentModel' &&
|
|
3972
4100
|
key !== 'subagentEffort' &&
|
|
3973
|
-
key !== 'browser'
|
|
4101
|
+
key !== 'browser' &&
|
|
4102
|
+
key !== 'skills',
|
|
3974
4103
|
),
|
|
3975
4104
|
) as JsonValue,
|
|
3976
4105
|
path,
|
|
@@ -3983,6 +4112,7 @@ function snapshotSessionAgent(
|
|
|
3983
4112
|
...(fixed.permissions === undefined
|
|
3984
4113
|
? {}
|
|
3985
4114
|
: { permissions: livePermissions(fixed.permissions) }),
|
|
4115
|
+
...(agent.skills === undefined ? {} : { skills: snapshotSkillLocators(agent.skills, `${path}.skills`) }),
|
|
3986
4116
|
...(agent.browser === undefined ? {} : { browser: snapshotFastMode(agent.browser, `${path}.browser`) }),
|
|
3987
4117
|
model: snapshotTuningSelection(agent.model, `${path}.model`),
|
|
3988
4118
|
effort: snapshotEffortSelection(agent.effort, `${path}.effort`),
|
|
@@ -4008,7 +4138,7 @@ function snapshotSessionAgent(
|
|
|
4008
4138
|
};
|
|
4009
4139
|
}
|
|
4010
4140
|
|
|
4011
|
-
function fixedAgent(agent: SessionAgent): Omit<SessionAgent, TuningKey | 'browser'> {
|
|
4141
|
+
function fixedAgent(agent: SessionAgent): Omit<SessionAgent, TuningKey | 'browser' | 'skills'> {
|
|
4012
4142
|
return {
|
|
4013
4143
|
adapter: agent.adapter,
|
|
4014
4144
|
...(agent.instruction === undefined ? {} : { instruction: agent.instruction }),
|
|
@@ -4029,6 +4159,7 @@ function callSettings(
|
|
|
4029
4159
|
model: tuning.model,
|
|
4030
4160
|
effort: tuning.effort,
|
|
4031
4161
|
browser: agent.browser ?? false,
|
|
4162
|
+
skills: agent.skills === undefined ? [] : [...agent.skills],
|
|
4032
4163
|
...(tuning.fastMode === undefined ? {} : { fastMode: tuning.fastMode }),
|
|
4033
4164
|
...(tuning.subagentModel === undefined
|
|
4034
4165
|
? {}
|
|
@@ -4042,7 +4173,7 @@ function callSettings(
|
|
|
4042
4173
|
}
|
|
4043
4174
|
|
|
4044
4175
|
function controlCallSettings(agent: SessionAgent): AgentCallSettings {
|
|
4045
|
-
return { ...callSettings(agent), browser: false, mcpServers: {} };
|
|
4176
|
+
return { ...callSettings(agent), browser: false, skills: [], mcpServers: {} };
|
|
4046
4177
|
}
|
|
4047
4178
|
|
|
4048
4179
|
function promptIdentity(binding: EffectivePlayerBinding): string {
|
|
@@ -4939,11 +5070,17 @@ export function createPlaybookCaptainShell(
|
|
|
4939
5070
|
const reportCarriedPreExistingChanges = (turn: ActiveTurn): void => {
|
|
4940
5071
|
if (turn.carriedEffectPrefix !== undefined) return;
|
|
4941
5072
|
let carried: readonly CarriedPreExistingChanges[];
|
|
4942
|
-
let commitStatements: readonly
|
|
5073
|
+
let commitStatements: readonly StepCommitStatement[];
|
|
4943
5074
|
try {
|
|
4944
5075
|
const ledger = assertPlaybookEffectLedger(currentEffectLedger());
|
|
4945
5076
|
carried = carriedPreExistingChanges(ledger, presentedEffectPrefix);
|
|
4946
|
-
|
|
5077
|
+
// DR-090 §4: a failed turn states its one-commit steps in the
|
|
5078
|
+
// commits-before-failure report instead (DR-089 §3), never twice.
|
|
5079
|
+
commitStatements = stepCommitStatements(
|
|
5080
|
+
ledger,
|
|
5081
|
+
presentedEffectPrefix,
|
|
5082
|
+
!leafParkedInFailure(),
|
|
5083
|
+
);
|
|
4947
5084
|
turn.carriedEffectPrefix = ledger.boundaries.length;
|
|
4948
5085
|
} catch {
|
|
4949
5086
|
return;
|
|
@@ -4956,16 +5093,22 @@ export function createPlaybookCaptainShell(
|
|
|
4956
5093
|
joinSettlementFact(turn, fact);
|
|
4957
5094
|
}
|
|
4958
5095
|
}
|
|
4959
|
-
// DR-088 §3: the commits each step took, read from the same
|
|
4960
|
-
|
|
5096
|
+
// DR-088 §3, DR-090 §4: the commits each step took, read from the same
|
|
5097
|
+
// boundaries; a step's one new commit reaches the closing reply's facts
|
|
5098
|
+
// only.
|
|
5099
|
+
const shownStatements = commitStatements.filter(
|
|
5100
|
+
(statement): statement is StepCommitStatement & { readonly shown: string } =>
|
|
5101
|
+
statement.shown !== undefined,
|
|
5102
|
+
);
|
|
5103
|
+
if (shownStatements.length > 0) {
|
|
4961
5104
|
appendMandatoryPresentationSuffix(
|
|
4962
5105
|
turn,
|
|
4963
|
-
|
|
4964
|
-
|
|
5106
|
+
shownStatements.map(({ shown }) => shown).join('\n'),
|
|
5107
|
+
shownStatements.map(({ text }) => text),
|
|
4965
5108
|
);
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
5109
|
+
}
|
|
5110
|
+
for (const { text } of commitStatements) {
|
|
5111
|
+
joinSettlementFact(turn, text);
|
|
4969
5112
|
}
|
|
4970
5113
|
};
|
|
4971
5114
|
|
|
@@ -8057,7 +8200,13 @@ export function createPlaybookCaptainShell(
|
|
|
8057
8200
|
? []
|
|
8058
8201
|
: [
|
|
8059
8202
|
{
|
|
8060
|
-
|
|
8203
|
+
// CAPTAIN-69: a mark whose words CAPTAIN-9 refuses — a path
|
|
8204
|
+
// naming control vocabulary, say — reads its code alone.
|
|
8205
|
+
text: `${MARK_FAILED} ${
|
|
8206
|
+
replyRejection(failure.mark) === undefined
|
|
8207
|
+
? failure.mark
|
|
8208
|
+
: failure.code
|
|
8209
|
+
}`,
|
|
8061
8210
|
kind: 'failure' as const,
|
|
8062
8211
|
reason: failure.reason,
|
|
8063
8212
|
},
|
|
@@ -9098,6 +9247,19 @@ export function createPlaybookCaptainShell(
|
|
|
9098
9247
|
// DR-063 §4: appended once, beside the unresolved-effect and pre-existing
|
|
9099
9248
|
// change reports, whenever the turn settles with the leaf parked in its
|
|
9100
9249
|
// failure state or behind the retained-effect fence.
|
|
9250
|
+
// The condition under which reportLeafFailure states a failure: the leaf
|
|
9251
|
+
// parked in its failure state, or behind the retained-effect fence.
|
|
9252
|
+
const leafParkedInFailure = (): boolean => {
|
|
9253
|
+
if (retainedEffectReconciliation !== undefined) return true;
|
|
9254
|
+
const leaf = leafFrame();
|
|
9255
|
+
if (!leaf || typeof leaf.runtime.describe !== 'function') return false;
|
|
9256
|
+
try {
|
|
9257
|
+
return leaf.runtime.describe().state.stateId === 'failed';
|
|
9258
|
+
} catch {
|
|
9259
|
+
return false;
|
|
9260
|
+
}
|
|
9261
|
+
};
|
|
9262
|
+
|
|
9101
9263
|
const reportLeafFailure = (
|
|
9102
9264
|
turn: ActiveTurn,
|
|
9103
9265
|
unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[],
|
|
@@ -9138,6 +9300,8 @@ export function createPlaybookCaptainShell(
|
|
|
9138
9300
|
: statement.restate(failure);
|
|
9139
9301
|
const speak = statement?.speak ?? ((text: string) => text);
|
|
9140
9302
|
turn.failureReport = {
|
|
9303
|
+
mark: markedFailure(cause, describeNestedPlaybook),
|
|
9304
|
+
code: cause.code,
|
|
9141
9305
|
...failureLine(
|
|
9142
9306
|
noteReason(
|
|
9143
9307
|
failureReason(failure, describeNestedPlaybook, speakableWords),
|
package/slc/link.md
CHANGED
|
@@ -332,6 +332,7 @@ interface PlaybookRepositoryObservation {
|
|
|
332
332
|
readonly worktree: string;
|
|
333
333
|
readonly gitDir: string;
|
|
334
334
|
readonly head: string;
|
|
335
|
+
readonly branch?: string;
|
|
335
336
|
readonly projection: Readonly<Record<string, JsonValue>>;
|
|
336
337
|
readonly projectionDigest: string;
|
|
337
338
|
}
|
|
@@ -521,6 +522,13 @@ interface PlaybookRuntimeSnapshot {
|
|
|
521
522
|
/** Interrupted invocation, captured before its external call (DR-069). */
|
|
522
523
|
recoveryCheckpoint?: PlaybookRecoveryCheckpoint;
|
|
523
524
|
suspendedCall?: PlaybookSuspendedCall;
|
|
525
|
+
/** The run's Boss exchanges so far, in order (DR-090). */
|
|
526
|
+
bossExchanges?: readonly PlaybookBossExchange[];
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
interface PlaybookBossExchange {
|
|
530
|
+
readonly question: string;
|
|
531
|
+
readonly reply: string;
|
|
524
532
|
}
|
|
525
533
|
|
|
526
534
|
type PlaybookRunResult =
|
|
@@ -1084,6 +1092,15 @@ A resumed call omits that question; a fresh call includes it before `Boss reply:
|
|
|
1084
1092
|
Both retain the authored task body exactly once, with two newlines between blocks.
|
|
1085
1093
|
These framework blocks never enter `invoke.input.prompt` or persisted FSM context.
|
|
1086
1094
|
|
|
1095
|
+
The shared runtime hands a composer the call's input without `pendingBossQuestion` and `bossReply` and builds the continuation itself, so a player of any language reads it as structure (DR-090).
|
|
1096
|
+
Before the composer's result it places, in order:
|
|
1097
|
+
|
|
1098
|
+
- `Role: <roleId> · Model: <promptIdentity>`, naming the call's local role and its current binding's prompt identity, or `Role: <roleId>` alone without bindings;
|
|
1099
|
+
- once the run has heard a Boss reply, the run's Boss exchanges so far, each a `? ⟶` line and the verbatim question, then a `Boss ⟶` line and the verbatim reply, whatever the role and whether or not the Source retains them, leaving out the one exchange the call's own continuation carries;
|
|
1100
|
+
- for a call after a Boss reply, the marked verbatim question, only for a fresh conversation, and the marked verbatim reply.
|
|
1101
|
+
|
|
1102
|
+
A run begins at the entry event or a finished machine's restart, and its exchanges travel in the runtime snapshot as `bossExchanges`.
|
|
1103
|
+
|
|
1087
1104
|
The runtime chooses the compact prompt only after selecting a conversation token and carries the complete prompt as optional `PlayerCallOptions.freshPrompt`.
|
|
1088
1105
|
A host starting fresh after a definite pre-execution token rejection uses `freshPrompt`, when supplied, instead of the compact prompt.
|
|
1089
1106
|
Without that option, the given prompt remains complete.
|
|
@@ -1290,9 +1307,11 @@ output is delivered.
|
|
|
1290
1307
|
That type is the value shape the compiled FSM's own validator admits for the
|
|
1291
1308
|
field: the shared engine reads it once at construction by presenting each
|
|
1292
1309
|
governed outcome's accepting arm with sample values of a closed set of shapes,
|
|
1293
|
-
a field whose shape the FSM admits as a string
|
|
1294
|
-
|
|
1295
|
-
|
|
1310
|
+
a field whose shape the FSM admits as a string stays a string, a field whose
|
|
1311
|
+
shape it does not reveal takes any JSON value for the FSM's own guard to
|
|
1312
|
+
decide, and the judge prompt and validation error name every non-string shape
|
|
1313
|
+
([DR-089](../specs/decisions/089-judge-replies-take-the-machines-shapes.md),
|
|
1314
|
+
[DR-091](../specs/decisions/091-an-unrevealed-shape-is-the-machines-to-judge.md)).
|
|
1296
1315
|
The judge prompt shall render each governed outcome's description with its
|
|
1297
1316
|
meaning verbatim and its `Output shall include` clause replaced by that reply
|
|
1298
1317
|
contract — exactly `guard` plus the semantic-owned fields, each keeping its
|
|
@@ -1972,6 +1991,9 @@ At a safe capture point it shall return a JSON-safe
|
|
|
1972
1991
|
`{ kind: 'captain' }` or `{ kind: 'role', roleId }`, empty when the
|
|
1973
1992
|
parked state awaits no reply. This list exists so hosts can surface the
|
|
1974
1993
|
question without parsing status lines or telemetry.
|
|
1994
|
+
- `bossExchanges`: omitted when the run has heard no Boss reply; otherwise
|
|
1995
|
+
every `{ question, reply }` exchange of the run so far, in order, which
|
|
1996
|
+
`restore` and adoption take back.
|
|
1975
1997
|
- `suspendedCall`: omitted when no nested call is pending; otherwise the
|
|
1976
1998
|
shared nested bridge's complete `callId`, source `stateId`, target
|
|
1977
1999
|
`playbookId`, exact handed-off `text`, and `childSessionId`, enriched with
|
package/src/runtime.d.ts
CHANGED
|
@@ -236,6 +236,12 @@ export interface PlaybookRepositoryObservation {
|
|
|
236
236
|
readonly worktree: string;
|
|
237
237
|
readonly gitDir: string;
|
|
238
238
|
readonly head: string;
|
|
239
|
+
/**
|
|
240
|
+
* The branch HEAD names, absent where HEAD is detached or the worktree is
|
|
241
|
+
* not a repository yet (DR-090 §4). Evidence for the Boss only: no
|
|
242
|
+
* comparison of two observations reads it.
|
|
243
|
+
*/
|
|
244
|
+
readonly branch?: string;
|
|
239
245
|
readonly projection: Readonly<Record<string, JsonValue>>;
|
|
240
246
|
readonly projectionDigest: string;
|
|
241
247
|
}
|
|
@@ -367,6 +373,11 @@ export interface PlaybookEffectLedgerCapability {
|
|
|
367
373
|
snapshot(): PlaybookEffectLedger;
|
|
368
374
|
writeAhead(commands: PlaybookEffectLedgerCommandBatch): Promise<PlaybookEffectLedger>;
|
|
369
375
|
}
|
|
376
|
+
/** One Boss exchange of a run: the question as asked and the reply verbatim (DR-090 §1). */
|
|
377
|
+
export interface PlaybookBossExchange {
|
|
378
|
+
readonly question: string;
|
|
379
|
+
readonly reply: string;
|
|
380
|
+
}
|
|
370
381
|
export interface PlaybookRuntimeSnapshot {
|
|
371
382
|
schemaVersion: 4;
|
|
372
383
|
playbookId: string;
|
|
@@ -402,6 +413,8 @@ export interface PlaybookRuntimeSnapshot {
|
|
|
402
413
|
/** Interrupted invocation, captured before its external call (DR-069). */
|
|
403
414
|
recoveryCheckpoint?: PlaybookRecoveryCheckpoint;
|
|
404
415
|
suspendedCall?: PlaybookSuspendedCall;
|
|
416
|
+
/** The run's Boss exchanges so far, in order; absent when there are none (DR-090 §1). */
|
|
417
|
+
bossExchanges?: readonly PlaybookBossExchange[];
|
|
405
418
|
}
|
|
406
419
|
export interface PlaybookRecoveryCheckpoint {
|
|
407
420
|
readonly id?: string;
|
package/src/runtime.ts
CHANGED
|
@@ -590,6 +590,12 @@ export interface PlaybookRepositoryObservation {
|
|
|
590
590
|
readonly worktree: string;
|
|
591
591
|
readonly gitDir: string;
|
|
592
592
|
readonly head: string;
|
|
593
|
+
/**
|
|
594
|
+
* The branch HEAD names, absent where HEAD is detached or the worktree is
|
|
595
|
+
* not a repository yet (DR-090 §4). Evidence for the Boss only: no
|
|
596
|
+
* comparison of two observations reads it.
|
|
597
|
+
*/
|
|
598
|
+
readonly branch?: string;
|
|
593
599
|
readonly projection: Readonly<Record<string, JsonValue>>;
|
|
594
600
|
readonly projectionDigest: string;
|
|
595
601
|
}
|
|
@@ -752,6 +758,12 @@ export interface PlaybookEffectLedgerCapability {
|
|
|
752
758
|
): Promise<PlaybookEffectLedger>;
|
|
753
759
|
}
|
|
754
760
|
|
|
761
|
+
/** One Boss exchange of a run: the question as asked and the reply verbatim (DR-090 §1). */
|
|
762
|
+
export interface PlaybookBossExchange {
|
|
763
|
+
readonly question: string;
|
|
764
|
+
readonly reply: string;
|
|
765
|
+
}
|
|
766
|
+
|
|
755
767
|
// DR-014 §1 / DR-031 §5 / DR-032: JSON-safe capture of a parked or nested-call
|
|
756
768
|
// suspended session. `machine` is the opaque XState persisted snapshot;
|
|
757
769
|
// pending Boss questions and a suspended call are first-class so a
|
|
@@ -789,6 +801,8 @@ export interface PlaybookRuntimeSnapshot {
|
|
|
789
801
|
/** Interrupted invocation, captured before its external call (DR-069). */
|
|
790
802
|
recoveryCheckpoint?: PlaybookRecoveryCheckpoint;
|
|
791
803
|
suspendedCall?: PlaybookSuspendedCall;
|
|
804
|
+
/** The run's Boss exchanges so far, in order; absent when there are none (DR-090 §1). */
|
|
805
|
+
bossExchanges?: readonly PlaybookBossExchange[];
|
|
792
806
|
}
|
|
793
807
|
|
|
794
808
|
export interface PlaybookRecoveryCheckpoint {
|
|
@@ -565,9 +565,10 @@ export declare function stateDescriptionsFromMachine(machine: AnyStateMachine):
|
|
|
565
565
|
*/
|
|
566
566
|
export declare function terminalOutcomesFromMachine(machine: AnyStateMachine, label?: string): ReadonlyMap<string, 'success' | 'failure'>;
|
|
567
567
|
/**
|
|
568
|
-
* DR-089: the
|
|
569
|
-
*
|
|
570
|
-
*
|
|
568
|
+
* DR-089: the JSON value shape the compiled machine admits for each
|
|
569
|
+
* semantic-owned field of each governed outcome, keyed by state id and then
|
|
570
|
+
* by outcome. A field, outcome, or state the probe reveals no shape for is
|
|
571
|
+
* left out (DR-091). Exported so linked machinery of an artifact's own reads
|
|
571
572
|
* the same contract the shared factory enforces.
|
|
572
573
|
*/
|
|
573
574
|
export declare function inferSemanticValueShapes(machine: AnyStateMachine, authority: XStateOutcomeAuthoritySpec): ReadonlyMap<string, PlaybookSemanticValueShapes>;
|