@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
|
@@ -19,6 +19,7 @@ import type { Effort, PermissionPolicy } from '@sublang/cligent';
|
|
|
19
19
|
import type {
|
|
20
20
|
JsonValue,
|
|
21
21
|
NormalizedError,
|
|
22
|
+
PlaybookControlStanding,
|
|
22
23
|
PlaybookEffectLedger,
|
|
23
24
|
PlaybookEffectLedgerCommandBatch,
|
|
24
25
|
PlaybookCallRequest,
|
|
@@ -26,6 +27,8 @@ import type {
|
|
|
26
27
|
PlaybookCallStart,
|
|
27
28
|
PlaybookControlAction,
|
|
28
29
|
PlaybookControlView,
|
|
30
|
+
PlaybookFailureCause,
|
|
31
|
+
PlaybookFailureCode,
|
|
29
32
|
PlaybookPorts,
|
|
30
33
|
PlaybookRunResult,
|
|
31
34
|
PlaybookRuntime,
|
|
@@ -34,6 +37,7 @@ import type {
|
|
|
34
37
|
PlayerSessionStore,
|
|
35
38
|
PlaybookState,
|
|
36
39
|
} from '@sublang/playbook/runtime';
|
|
40
|
+
import { assertPlaybookFailureCause } from '@sublang/playbook/runtime';
|
|
37
41
|
import {
|
|
38
42
|
assertPlaybookRuntimeSnapshot,
|
|
39
43
|
assertPlaybookEffectLedger,
|
|
@@ -273,7 +277,12 @@ type PlaybookCaptainShellSnapshotValue =
|
|
|
273
277
|
readonly frames: readonly PlaybookCaptainFrameSnapshot[];
|
|
274
278
|
readonly retainedEffectReconciliation?: PlaybookCaptainRetainedEffectReconciliation;
|
|
275
279
|
readonly pendingBossQuestions?: JsonValue;
|
|
276
|
-
|
|
280
|
+
/** DR-063 §2: the parked failure's cause travels with its error. */
|
|
281
|
+
readonly lastError?: {
|
|
282
|
+
readonly name: string;
|
|
283
|
+
readonly message: string;
|
|
284
|
+
readonly cause?: PlaybookFailureCause;
|
|
285
|
+
};
|
|
277
286
|
}
|
|
278
287
|
);
|
|
279
288
|
|
|
@@ -517,7 +526,7 @@ interface ControlLedger {
|
|
|
517
526
|
latestSubRuntimeStateId?: string;
|
|
518
527
|
latestSubRuntimeState?: PlaybookState;
|
|
519
528
|
pendingBossQuestions?: unknown;
|
|
520
|
-
lastError?: { name: string; message: string };
|
|
529
|
+
lastError?: { name: string; message: string; cause?: PlaybookFailureCause };
|
|
521
530
|
// CAPTAIN-5/CAPTAIN-6: the session Captain's own identity plus the durable
|
|
522
531
|
// conversation and journal by presence only — never the pinned token value.
|
|
523
532
|
captainSessionId?: string;
|
|
@@ -708,6 +717,17 @@ interface ActiveTurn {
|
|
|
708
717
|
outcomeRecorded: boolean;
|
|
709
718
|
/** Canonical host evidence frozen before the controller result phase. */
|
|
710
719
|
unresolvedEffects?: readonly PlaybookCaptainUnresolvedEffect[];
|
|
720
|
+
/**
|
|
721
|
+
* DR-062 §5: the effect ledger's boundary count when this turn was
|
|
722
|
+
* admitted. A boundary beyond it completed during this turn; `undefined`
|
|
723
|
+
* where the count could not be read, which reports nothing rather than
|
|
724
|
+
* report a boundary an earlier turn already reported.
|
|
725
|
+
*/
|
|
726
|
+
readonly effectBoundaryPrefixCount?: number;
|
|
727
|
+
/** Set once this turn's carried pre-existing changes have been reported. */
|
|
728
|
+
carriedPreExistingReported?: boolean;
|
|
729
|
+
/** DR-063 §4: set once this turn's failure report has been appended. */
|
|
730
|
+
failureReported?: boolean;
|
|
711
731
|
}
|
|
712
732
|
|
|
713
733
|
function parseRegisteredCommand(
|
|
@@ -1203,6 +1223,199 @@ function unresolvedEffectBossReport(
|
|
|
1203
1223
|
].join('\n');
|
|
1204
1224
|
}
|
|
1205
1225
|
|
|
1226
|
+
// DR-062 §5: a settled turn whose accepted commit absorbed or altered the
|
|
1227
|
+
// Boss's own uncommitted changes says so, bounded to paths.
|
|
1228
|
+
const CARRIED_CHANGE_PATH_LIMIT = 24;
|
|
1229
|
+
|
|
1230
|
+
interface CarriedPreExistingChanges {
|
|
1231
|
+
readonly commitOid: string;
|
|
1232
|
+
readonly absorbed: readonly string[];
|
|
1233
|
+
readonly altered: readonly string[];
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function boundedPathList(paths: readonly string[]): string {
|
|
1237
|
+
const printed = paths.slice(0, CARRIED_CHANGE_PATH_LIMIT);
|
|
1238
|
+
const omitted = paths.length - printed.length;
|
|
1239
|
+
return omitted > 0
|
|
1240
|
+
? `${printed.join(', ')}, … and ${omitted} more`
|
|
1241
|
+
: printed.join(', ');
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* The carried pre-existing changes of the boundaries completed after
|
|
1246
|
+
* `boundaryPrefixCount`, one entry per commit OID in boundary sequence order.
|
|
1247
|
+
*/
|
|
1248
|
+
function carriedPreExistingChanges(
|
|
1249
|
+
ledger: PlaybookEffectLedger,
|
|
1250
|
+
boundaryPrefixCount: number,
|
|
1251
|
+
): readonly CarriedPreExistingChanges[] {
|
|
1252
|
+
const byCommit = new Map<string, { absorbed: string[]; altered: string[] }>();
|
|
1253
|
+
for (const boundary of ledger.boundaries) {
|
|
1254
|
+
if (boundary.sequence <= boundaryPrefixCount) continue;
|
|
1255
|
+
const receipt = boundary.physicalReceipt;
|
|
1256
|
+
if (
|
|
1257
|
+
receipt?.classification !== 'one-descendant-commit' ||
|
|
1258
|
+
receipt.commitOid === undefined ||
|
|
1259
|
+
receipt.preExisting === undefined
|
|
1260
|
+
) {
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
const { absorbed, altered } = receipt.preExisting;
|
|
1264
|
+
if (absorbed.length === 0 && altered.length === 0) continue;
|
|
1265
|
+
const carried = byCommit.get(receipt.commitOid) ?? {
|
|
1266
|
+
absorbed: [],
|
|
1267
|
+
altered: [],
|
|
1268
|
+
};
|
|
1269
|
+
carried.absorbed.push(...absorbed);
|
|
1270
|
+
carried.altered.push(...altered);
|
|
1271
|
+
byCommit.set(receipt.commitOid, carried);
|
|
1272
|
+
}
|
|
1273
|
+
return [...byCommit].map(([commitOid, { absorbed, altered }]) => ({
|
|
1274
|
+
commitOid,
|
|
1275
|
+
absorbed: [...new Set(absorbed)].sort(),
|
|
1276
|
+
altered: [...new Set(altered)].sort(),
|
|
1277
|
+
}));
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function carriedPreExistingReport(
|
|
1281
|
+
carried: readonly CarriedPreExistingChanges[],
|
|
1282
|
+
): string | undefined {
|
|
1283
|
+
if (carried.length === 0) return undefined;
|
|
1284
|
+
return carried
|
|
1285
|
+
.map(({ commitOid, absorbed, altered }) =>
|
|
1286
|
+
[
|
|
1287
|
+
`Pre-existing changes carried by commit ${commitOid}:`,
|
|
1288
|
+
...(absorbed.length === 0
|
|
1289
|
+
? []
|
|
1290
|
+
: [`- absorbed as found: ${boundedPathList(absorbed)}`]),
|
|
1291
|
+
...(altered.length === 0
|
|
1292
|
+
? []
|
|
1293
|
+
: [`- altered before committing: ${boundedPathList(altered)}`]),
|
|
1294
|
+
'These changes were uncommitted before the step.',
|
|
1295
|
+
].join('\n'),
|
|
1296
|
+
)
|
|
1297
|
+
.join('\n\n');
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function carriedPreExistingFacts(
|
|
1301
|
+
carried: readonly CarriedPreExistingChanges[],
|
|
1302
|
+
): readonly string[] {
|
|
1303
|
+
return carried.map(({ commitOid, absorbed, altered }) => {
|
|
1304
|
+
const paths = [...new Set([...absorbed, ...altered])].sort();
|
|
1305
|
+
return `The commit ${commitOid} carries ${paths.length} changes that were uncommitted before the step: ${boundedPathList(paths)}.`;
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// DR-063 §4: the Boss-visible failure report. Every host sees the same
|
|
1310
|
+
// sentences because the shell composes them from the structured cause instead
|
|
1311
|
+
// of asking a model to describe facts it may drop. The report exposes no file
|
|
1312
|
+
// content, player prose, or internal identity: paths, revisions, dispositions,
|
|
1313
|
+
// and the reported error message are what a cause carries.
|
|
1314
|
+
|
|
1315
|
+
/** The Boss-facing phrase for each closed control-action reason (DR-063 §3). */
|
|
1316
|
+
const CONTROL_REASON_PHRASES: Readonly<Record<string, string>> = {
|
|
1317
|
+
'receipt-complete': 'nothing has changed since it failed',
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
function controlReasonPhrase(reason: string | undefined): string {
|
|
1321
|
+
if (reason === undefined) return 'no reason was published';
|
|
1322
|
+
return CONTROL_REASON_PHRASES[reason] ?? reason;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function causePathList(
|
|
1326
|
+
paths: readonly string[] | undefined,
|
|
1327
|
+
truncated: number | undefined,
|
|
1328
|
+
): string {
|
|
1329
|
+
const list = paths ?? [];
|
|
1330
|
+
const printed = list.slice(0, CARRIED_CHANGE_PATH_LIMIT);
|
|
1331
|
+
const omitted = list.length - printed.length + (truncated ?? 0);
|
|
1332
|
+
return omitted > 0
|
|
1333
|
+
? `${printed.join(', ')}, … and ${omitted} more`
|
|
1334
|
+
: printed.join(', ');
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/** One sentence per code, with that code's bounded evidence (DR-063 §4). */
|
|
1338
|
+
function failureCauseSentence(
|
|
1339
|
+
cause: PlaybookFailureCause,
|
|
1340
|
+
describePlaybook: (playbookId: string) => string,
|
|
1341
|
+
): string {
|
|
1342
|
+
const evidence = cause.evidence;
|
|
1343
|
+
const paths = evidence.paths;
|
|
1344
|
+
const code: PlaybookFailureCode = cause.code;
|
|
1345
|
+
switch (code) {
|
|
1346
|
+
case 'commit-missing':
|
|
1347
|
+
return (paths?.uncommitted ?? []).length === 0
|
|
1348
|
+
? 'the step had to commit its work and committed nothing.'
|
|
1349
|
+
: `the step had to commit its work and committed nothing; these changes are uncommitted: ${causePathList(paths?.uncommitted, paths?.truncated)}.`;
|
|
1350
|
+
case 'commit-residual': {
|
|
1351
|
+
const subject =
|
|
1352
|
+
evidence.commitOid === undefined
|
|
1353
|
+
? "the step's commit"
|
|
1354
|
+
: `the commit ${evidence.commitOid}`;
|
|
1355
|
+
if ((paths?.uncommitted ?? []).length > 0) {
|
|
1356
|
+
return `${subject} left changes uncommitted: ${causePathList(paths?.uncommitted, paths?.truncated)}.`;
|
|
1357
|
+
}
|
|
1358
|
+
if ((paths?.altered ?? []).length > 0) {
|
|
1359
|
+
return `${subject} left altered changes uncommitted: ${causePathList(paths?.altered, paths?.truncated)}.`;
|
|
1360
|
+
}
|
|
1361
|
+
return `${subject} left changes uncommitted beside it.`;
|
|
1362
|
+
}
|
|
1363
|
+
case 'pre-existing-lost':
|
|
1364
|
+
return `uncommitted changes you had were lost: ${causePathList(paths?.lost, paths?.truncated)}.`;
|
|
1365
|
+
case 'commits-more-than-one':
|
|
1366
|
+
return `the step made more than one commit; HEAD moved from ${evidence.baselineHead} to ${evidence.afterHead}.`;
|
|
1367
|
+
case 'history-rewritten':
|
|
1368
|
+
return `the repository history was rewritten; HEAD moved from ${evidence.baselineHead} to ${evidence.afterHead}.`;
|
|
1369
|
+
case 'foreign-change':
|
|
1370
|
+
return `the repository changed outside this step: ${causePathList(paths?.changed, paths?.truncated)}.`;
|
|
1371
|
+
case 'observation-unstable':
|
|
1372
|
+
return `the repository could not be observed after the step; it stood at ${evidence.baselineHead} before it.`;
|
|
1373
|
+
case 'attribution-ambiguous':
|
|
1374
|
+
return `the repository change could not be attributed to this step; it required ${evidence.required} and observed ${evidence.observed}.`;
|
|
1375
|
+
case 'receipt-missing':
|
|
1376
|
+
return `a step left no complete repository receipt; the repository stood at ${evidence.baselineHead} before it.`;
|
|
1377
|
+
case 'judge-failed':
|
|
1378
|
+
return evidence.error === undefined
|
|
1379
|
+
? `the hidden adjudication failed: ${evidence.reason}.`
|
|
1380
|
+
: `the hidden adjudication failed: ${evidence.reason} (${evidence.error.name}: ${compactEvidence(evidence.error.message)}).`;
|
|
1381
|
+
case 'player-failed':
|
|
1382
|
+
return `the ${evidence.roleId} call failed: ${compactEvidence(evidence.error?.message ?? 'no detail was reported')}.`;
|
|
1383
|
+
case 'aborted':
|
|
1384
|
+
return 'the turn was aborted before the work settled.';
|
|
1385
|
+
case 'child-failed': {
|
|
1386
|
+
const nested = describePlaybook(evidence.playbookId ?? 'nested playbook');
|
|
1387
|
+
return evidence.cause === undefined
|
|
1388
|
+
? `the nested ${nested} run failed.`
|
|
1389
|
+
: `the nested ${nested} run failed: ${failureCauseSentence(evidence.cause, describePlaybook)}`;
|
|
1390
|
+
}
|
|
1391
|
+
case 'runtime-defect':
|
|
1392
|
+
return `the runtime could not settle the step: ${compactEvidence(evidence.reason ?? 'no reason was recorded')}.`;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function failureControlLine(action: {
|
|
1397
|
+
readonly label: string;
|
|
1398
|
+
readonly standing?: PlaybookControlStanding;
|
|
1399
|
+
readonly reason?: string;
|
|
1400
|
+
}): string {
|
|
1401
|
+
// DR-063 §3: an action that publishes no standing is `ready`.
|
|
1402
|
+
const standing = action.standing ?? 'ready';
|
|
1403
|
+
return standing === 'ready'
|
|
1404
|
+
? `- ${action.label} (ready)`
|
|
1405
|
+
: `- ${action.label} (${standing}: ${controlReasonPhrase(action.reason)})`;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function failureReportText(
|
|
1409
|
+
sentence: string,
|
|
1410
|
+
controls: readonly string[],
|
|
1411
|
+
): string {
|
|
1412
|
+
return [
|
|
1413
|
+
`Failure: ${sentence}`,
|
|
1414
|
+
'Controls:',
|
|
1415
|
+
...(controls.length === 0 ? ['- none'] : controls),
|
|
1416
|
+
].join('\n');
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1206
1419
|
function appendMandatoryPresentationSuffix(
|
|
1207
1420
|
turn: ActiveTurn,
|
|
1208
1421
|
suffix: string,
|
|
@@ -2374,16 +2587,23 @@ export function assertPlaybookCaptainShellSnapshot(
|
|
|
2374
2587
|
});
|
|
2375
2588
|
}
|
|
2376
2589
|
let normalizedLastError:
|
|
2377
|
-
| {
|
|
2590
|
+
| {
|
|
2591
|
+
readonly name: string;
|
|
2592
|
+
readonly message: string;
|
|
2593
|
+
readonly cause?: PlaybookFailureCause;
|
|
2594
|
+
}
|
|
2378
2595
|
| undefined;
|
|
2379
2596
|
if (snapshot.lastError !== undefined) {
|
|
2380
2597
|
const error = snapshotRecord(
|
|
2381
2598
|
snapshot.lastError,
|
|
2382
2599
|
'Captain shell snapshot.lastError',
|
|
2383
2600
|
);
|
|
2601
|
+
// DR-063 §2: the cause enters the stored record inside `lastError`, so a
|
|
2602
|
+
// parked failure explains itself after a restart. An unrecognized shape is
|
|
2603
|
+
// not a cause and the closed validator refuses the snapshot.
|
|
2384
2604
|
rejectSnapshotKeys(
|
|
2385
2605
|
error,
|
|
2386
|
-
['name', 'message'],
|
|
2606
|
+
['name', 'message', 'cause'],
|
|
2387
2607
|
'Captain shell snapshot.lastError',
|
|
2388
2608
|
);
|
|
2389
2609
|
normalizedLastError = {
|
|
@@ -2397,6 +2617,9 @@ export function assertPlaybookCaptainShellSnapshot(
|
|
|
2397
2617
|
'Captain shell snapshot.lastError.message',
|
|
2398
2618
|
true,
|
|
2399
2619
|
),
|
|
2620
|
+
...(error.cause === undefined
|
|
2621
|
+
? {}
|
|
2622
|
+
: { cause: assertPlaybookFailureCause(error.cause) }),
|
|
2400
2623
|
};
|
|
2401
2624
|
}
|
|
2402
2625
|
const activePlaybooks = new Set<string>();
|
|
@@ -3083,7 +3306,9 @@ export function createPlaybookCaptainShell(
|
|
|
3083
3306
|
| PlaybookCaptainRetainedEffectReconciliation
|
|
3084
3307
|
| undefined;
|
|
3085
3308
|
let pendingBossQuestions: unknown;
|
|
3086
|
-
let lastError:
|
|
3309
|
+
let lastError:
|
|
3310
|
+
| { name: string; message: string; cause?: PlaybookFailureCause }
|
|
3311
|
+
| undefined;
|
|
3087
3312
|
let activeTurnSummary: ActiveTurnSummary | undefined;
|
|
3088
3313
|
let activeTurnHostCalls: Set<Promise<unknown>> | undefined;
|
|
3089
3314
|
const issuedSessionIds = new Set<string>();
|
|
@@ -3351,6 +3576,15 @@ export function createPlaybookCaptainShell(
|
|
|
3351
3576
|
original.projection,
|
|
3352
3577
|
after.projection,
|
|
3353
3578
|
);
|
|
3579
|
+
// DR-062 §3: a pure projection never downgrades a classification the
|
|
3580
|
+
// physical receipt proved. The latest receipt already accounted for every
|
|
3581
|
+
// pre-existing entry of its own baseline, so when that baseline is the
|
|
3582
|
+
// original one its accounting is the operation's accounting, and the
|
|
3583
|
+
// projection may not demand a byte-equal after projection on top of it.
|
|
3584
|
+
const receiptAccountsFromOriginal = isDeepStrictEqual(
|
|
3585
|
+
receipt.baseline.projection,
|
|
3586
|
+
original.projection,
|
|
3587
|
+
);
|
|
3354
3588
|
if (after.head === original.head) {
|
|
3355
3589
|
if (
|
|
3356
3590
|
receipt.classification !== 'unchanged' &&
|
|
@@ -3369,9 +3603,12 @@ export function createPlaybookCaptainShell(
|
|
|
3369
3603
|
isDeepStrictEqual(entry, after.projection[path]),
|
|
3370
3604
|
);
|
|
3371
3605
|
return {
|
|
3372
|
-
classification:
|
|
3373
|
-
|
|
3374
|
-
|
|
3606
|
+
classification:
|
|
3607
|
+
preservesOriginal ||
|
|
3608
|
+
(receipt.classification === 'worktree-only-change' &&
|
|
3609
|
+
receiptAccountsFromOriginal)
|
|
3610
|
+
? 'worktree-only-change'
|
|
3611
|
+
: 'observation-ambiguous',
|
|
3375
3612
|
baselineHead: original.head,
|
|
3376
3613
|
afterHead: after.head,
|
|
3377
3614
|
};
|
|
@@ -3379,7 +3616,7 @@ export function createPlaybookCaptainShell(
|
|
|
3379
3616
|
|
|
3380
3617
|
if (
|
|
3381
3618
|
receipt.classification === 'one-descendant-commit' &&
|
|
3382
|
-
sameProjection
|
|
3619
|
+
(sameProjection || receiptAccountsFromOriginal)
|
|
3383
3620
|
) {
|
|
3384
3621
|
return {
|
|
3385
3622
|
classification: 'one-descendant-commit',
|
|
@@ -3523,6 +3760,44 @@ export function createPlaybookCaptainShell(
|
|
|
3523
3760
|
return frozen;
|
|
3524
3761
|
};
|
|
3525
3762
|
|
|
3763
|
+
/**
|
|
3764
|
+
* DR-062 §5: one deterministic Boss-visible report, once per turn, for the
|
|
3765
|
+
* pre-existing changes the commits completed during this turn carried, and
|
|
3766
|
+
* the same information as settlement facts the closing-reply prompt reads.
|
|
3767
|
+
* It supplements the unresolved-effect report and enters no run result.
|
|
3768
|
+
*/
|
|
3769
|
+
const reportCarriedPreExistingChanges = (turn: ActiveTurn): void => {
|
|
3770
|
+
if (turn.carriedPreExistingReported) return;
|
|
3771
|
+
if (turn.effectBoundaryPrefixCount === undefined) return;
|
|
3772
|
+
let carried: readonly CarriedPreExistingChanges[];
|
|
3773
|
+
try {
|
|
3774
|
+
carried = carriedPreExistingChanges(
|
|
3775
|
+
assertPlaybookEffectLedger(currentEffectLedger()),
|
|
3776
|
+
turn.effectBoundaryPrefixCount,
|
|
3777
|
+
);
|
|
3778
|
+
} catch {
|
|
3779
|
+
return;
|
|
3780
|
+
}
|
|
3781
|
+
const report = carriedPreExistingReport(carried);
|
|
3782
|
+
if (report === undefined) return;
|
|
3783
|
+
turn.carriedPreExistingReported = true;
|
|
3784
|
+
appendMandatoryPresentationSuffix(turn, report);
|
|
3785
|
+
const facts = carriedPreExistingFacts(carried);
|
|
3786
|
+
turn.settlementFacts.push(...facts);
|
|
3787
|
+
// The action's own report was assembled from the facts as they stood when
|
|
3788
|
+
// it settled; the result-phase prompt reads this report, so the same facts
|
|
3789
|
+
// join it here rather than only the turn's running list.
|
|
3790
|
+
if (turn.report !== undefined) {
|
|
3791
|
+
turn.report = {
|
|
3792
|
+
...turn.report,
|
|
3793
|
+
facts: [...turn.report.facts, ...facts],
|
|
3794
|
+
...(turn.report.bossFacts === undefined
|
|
3795
|
+
? {}
|
|
3796
|
+
: { bossFacts: [...turn.report.bossFacts, ...facts] }),
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
};
|
|
3800
|
+
|
|
3526
3801
|
const normalizeInstalledRetainedGenerations = (
|
|
3527
3802
|
value: Readonly<Record<string, PlaybookCaptainRetainedGeneration>>,
|
|
3528
3803
|
): Map<string, PlaybookCaptainRetainedGeneration> => {
|
|
@@ -5766,6 +6041,30 @@ export function createPlaybookCaptainShell(
|
|
|
5766
6041
|
];
|
|
5767
6042
|
};
|
|
5768
6043
|
|
|
6044
|
+
// DR-063 §3: every advertised action names its standing in the digest, so a
|
|
6045
|
+
// model is never invited to select an action that changes nothing.
|
|
6046
|
+
const advertisedActionDigest = (
|
|
6047
|
+
actions: readonly PlaybookControlAction[],
|
|
6048
|
+
): string => {
|
|
6049
|
+
if (actions.length === 0) return 'Advertised actions: none.';
|
|
6050
|
+
const lines = [
|
|
6051
|
+
'Advertised actions:',
|
|
6052
|
+
...actions.map((action) =>
|
|
6053
|
+
(action.standing ?? 'ready') === 'ready'
|
|
6054
|
+
? digestLine`- ${action.id}: ${action.label}`
|
|
6055
|
+
: digestLine`- ${action.id}: ${action.label} (${action.standing!}: ${
|
|
6056
|
+
action.reason ?? 'no reason was published'
|
|
6057
|
+
})`,
|
|
6058
|
+
),
|
|
6059
|
+
];
|
|
6060
|
+
if (actions.some(({ standing }) => standing !== undefined && standing !== 'ready')) {
|
|
6061
|
+
lines.push(
|
|
6062
|
+
'An action marked no-op runs and changes nothing; a blocked action cannot run at all.',
|
|
6063
|
+
);
|
|
6064
|
+
}
|
|
6065
|
+
return lines.join('\n');
|
|
6066
|
+
};
|
|
6067
|
+
|
|
5769
6068
|
const retainedResumptionDigest = (): string => {
|
|
5770
6069
|
if (rootFrame() !== undefined) {
|
|
5771
6070
|
return 'Retained resumptions: unavailable while a playbook is engaged.';
|
|
@@ -5798,10 +6097,7 @@ export function createPlaybookCaptainShell(
|
|
|
5798
6097
|
}
|
|
5799
6098
|
refreshRetainedEffectFence();
|
|
5800
6099
|
if (retainedEffectReconciliation !== undefined) {
|
|
5801
|
-
let reconciliationActions: readonly
|
|
5802
|
-
readonly id: string;
|
|
5803
|
-
readonly label: string;
|
|
5804
|
-
}[] = [];
|
|
6100
|
+
let reconciliationActions: readonly PlaybookControlAction[] = [];
|
|
5805
6101
|
if (
|
|
5806
6102
|
typeof leaf.runtime.describe === 'function' &&
|
|
5807
6103
|
typeof leaf.runtime.apply === 'function'
|
|
@@ -5825,16 +6121,7 @@ export function createPlaybookCaptainShell(
|
|
|
5825
6121
|
`Leaf ${frameLabel(leaf)} is parked for repository-effect reconciliation.`,
|
|
5826
6122
|
);
|
|
5827
6123
|
lines.push('Pending Boss questions: withheld until reconciliation.');
|
|
5828
|
-
lines.push(
|
|
5829
|
-
reconciliationActions.length === 0
|
|
5830
|
-
? 'Advertised actions: none.'
|
|
5831
|
-
: [
|
|
5832
|
-
'Advertised actions:',
|
|
5833
|
-
...reconciliationActions.map(
|
|
5834
|
-
(action) => digestLine`- ${action.id}: ${action.label}`,
|
|
5835
|
-
),
|
|
5836
|
-
].join('\n'),
|
|
5837
|
-
);
|
|
6124
|
+
lines.push(advertisedActionDigest(reconciliationActions));
|
|
5838
6125
|
lines.push(
|
|
5839
6126
|
'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.',
|
|
5840
6127
|
);
|
|
@@ -5939,16 +6226,7 @@ export function createPlaybookCaptainShell(
|
|
|
5939
6226
|
})}`,
|
|
5940
6227
|
);
|
|
5941
6228
|
}
|
|
5942
|
-
lines.push(
|
|
5943
|
-
view.actions.length === 0
|
|
5944
|
-
? 'Advertised actions: none.'
|
|
5945
|
-
: [
|
|
5946
|
-
'Advertised actions:',
|
|
5947
|
-
...view.actions.map(
|
|
5948
|
-
(action) => digestLine`- ${action.id}: ${action.label}`,
|
|
5949
|
-
),
|
|
5950
|
-
].join('\n'),
|
|
5951
|
-
);
|
|
6229
|
+
lines.push(advertisedActionDigest(view.actions));
|
|
5952
6230
|
lines.push(retainedResumptionDigest());
|
|
5953
6231
|
return lines.join('\n');
|
|
5954
6232
|
};
|
|
@@ -5989,7 +6267,17 @@ export function createPlaybookCaptainShell(
|
|
|
5989
6267
|
action.id === UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID ||
|
|
5990
6268
|
action.id === UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID,
|
|
5991
6269
|
)
|
|
5992
|
-
|
|
6270
|
+
// DR-063 §3: the leaf's own standing is published with the pair, so a
|
|
6271
|
+
// host drawing the control says what running it would do. A leaf that
|
|
6272
|
+
// publishes none advertises `ready`, which is what it always meant.
|
|
6273
|
+
.map((action) =>
|
|
6274
|
+
Object.freeze({
|
|
6275
|
+
id: action.id,
|
|
6276
|
+
label: action.label,
|
|
6277
|
+
standing: action.standing ?? 'ready',
|
|
6278
|
+
...(action.reason === undefined ? {} : { reason: action.reason }),
|
|
6279
|
+
}),
|
|
6280
|
+
),
|
|
5993
6281
|
);
|
|
5994
6282
|
};
|
|
5995
6283
|
|
|
@@ -6016,6 +6304,9 @@ export function createPlaybookCaptainShell(
|
|
|
6016
6304
|
// way the Boss started it rather than the frame that happens to be
|
|
6017
6305
|
// parked inside it.
|
|
6018
6306
|
label: `Stop ${frameLabel(root)}`,
|
|
6307
|
+
// DR-063 §3: giving up always ends the engagement, whatever the leaf
|
|
6308
|
+
// is parked on.
|
|
6309
|
+
standing: 'ready' as const,
|
|
6019
6310
|
}),
|
|
6020
6311
|
]);
|
|
6021
6312
|
};
|
|
@@ -7186,6 +7477,123 @@ export function createPlaybookCaptainShell(
|
|
|
7186
7477
|
);
|
|
7187
7478
|
};
|
|
7188
7479
|
|
|
7480
|
+
// DR-063 §4: the leaf's failure as the shell reads it — the cause the runtime
|
|
7481
|
+
// decided, or, behind the retained-effect fence where the leaf itself holds
|
|
7482
|
+
// no error, the incomplete receipt the frozen evidence names.
|
|
7483
|
+
const leafFailureCause = (
|
|
7484
|
+
view: PlaybookControlView | undefined,
|
|
7485
|
+
fenced: boolean,
|
|
7486
|
+
unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[],
|
|
7487
|
+
): PlaybookFailureCause | undefined => {
|
|
7488
|
+
const recorded = view?.lastError ?? lastError;
|
|
7489
|
+
if (recorded?.cause !== undefined) {
|
|
7490
|
+
try {
|
|
7491
|
+
return assertPlaybookFailureCause(recorded.cause);
|
|
7492
|
+
} catch {
|
|
7493
|
+
// A cause the closed validator refuses is not a cause.
|
|
7494
|
+
}
|
|
7495
|
+
}
|
|
7496
|
+
if (fenced) {
|
|
7497
|
+
const incomplete = unresolvedEffects.find(
|
|
7498
|
+
({ classification }) => classification === 'incomplete',
|
|
7499
|
+
);
|
|
7500
|
+
if (incomplete !== undefined) {
|
|
7501
|
+
return assertPlaybookFailureCause({
|
|
7502
|
+
code: 'receipt-missing',
|
|
7503
|
+
evidence: { baselineHead: incomplete.baselineHead },
|
|
7504
|
+
});
|
|
7505
|
+
}
|
|
7506
|
+
}
|
|
7507
|
+
// There is no failure without a cause: a parked failure whose error
|
|
7508
|
+
// carries none is a runtime defect named by that error.
|
|
7509
|
+
if (view?.state.stateId === 'failed' && recorded !== undefined) {
|
|
7510
|
+
return assertPlaybookFailureCause({
|
|
7511
|
+
code: 'runtime-defect',
|
|
7512
|
+
evidence: {
|
|
7513
|
+
reason:
|
|
7514
|
+
recorded.message.trim().length > 0
|
|
7515
|
+
? recorded.message
|
|
7516
|
+
: recorded.name,
|
|
7517
|
+
},
|
|
7518
|
+
});
|
|
7519
|
+
}
|
|
7520
|
+
return undefined;
|
|
7521
|
+
};
|
|
7522
|
+
|
|
7523
|
+
const failureControlLines = (
|
|
7524
|
+
view: PlaybookControlView | undefined,
|
|
7525
|
+
fenced: boolean,
|
|
7526
|
+
): readonly string[] => {
|
|
7527
|
+
const runtimeActions = (view?.actions ?? []).filter(
|
|
7528
|
+
(action) =>
|
|
7529
|
+
!fenced ||
|
|
7530
|
+
action.id === UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID ||
|
|
7531
|
+
action.id === UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID,
|
|
7532
|
+
);
|
|
7533
|
+
const root = rootFrame();
|
|
7534
|
+
return [
|
|
7535
|
+
...runtimeActions.map((action) => failureControlLine(action)),
|
|
7536
|
+
// The shell's own control is published beside the leaf's, so the report
|
|
7537
|
+
// names every door out of this failure (CAPTAIN-62).
|
|
7538
|
+
...(root === undefined
|
|
7539
|
+
? []
|
|
7540
|
+
: [
|
|
7541
|
+
failureControlLine({
|
|
7542
|
+
label: `Stop ${frameLabel(root)}`,
|
|
7543
|
+
standing: 'ready',
|
|
7544
|
+
}),
|
|
7545
|
+
]),
|
|
7546
|
+
];
|
|
7547
|
+
};
|
|
7548
|
+
|
|
7549
|
+
const describeNestedPlaybook = (playbookId: string): string => {
|
|
7550
|
+
const enablement = enablementById.get(playbookId);
|
|
7551
|
+
return enablement === undefined ? playbookId : `/${enablement.command}`;
|
|
7552
|
+
};
|
|
7553
|
+
|
|
7554
|
+
// DR-063 §4: appended once, beside the unresolved-effect and pre-existing
|
|
7555
|
+
// change reports, whenever the turn settles with the leaf parked in its
|
|
7556
|
+
// failure state or behind the retained-effect fence.
|
|
7557
|
+
const reportLeafFailure = (
|
|
7558
|
+
turn: ActiveTurn,
|
|
7559
|
+
unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[],
|
|
7560
|
+
): void => {
|
|
7561
|
+
if (turn.failureReported) return;
|
|
7562
|
+
const leaf = leafFrame();
|
|
7563
|
+
if (!leaf) return;
|
|
7564
|
+
const fenced = retainedEffectReconciliation !== undefined;
|
|
7565
|
+
let view: PlaybookControlView | undefined;
|
|
7566
|
+
if (typeof leaf.runtime.describe === 'function') {
|
|
7567
|
+
try {
|
|
7568
|
+
view = leaf.runtime.describe();
|
|
7569
|
+
} catch {
|
|
7570
|
+
// An unreadable control view leaves the mirrored error as evidence.
|
|
7571
|
+
}
|
|
7572
|
+
}
|
|
7573
|
+
if (!fenced && view?.state.stateId !== 'failed') return;
|
|
7574
|
+
const cause = leafFailureCause(view, fenced, unresolvedEffects);
|
|
7575
|
+
if (cause === undefined) return;
|
|
7576
|
+
turn.failureReported = true;
|
|
7577
|
+
const sentence = failureCauseSentence(cause, describeNestedPlaybook);
|
|
7578
|
+
appendMandatoryPresentationSuffix(
|
|
7579
|
+
turn,
|
|
7580
|
+
failureReportText(sentence, failureControlLines(view, fenced)),
|
|
7581
|
+
);
|
|
7582
|
+
// The result-phase prompt reads the turn's report, so the same fact joins
|
|
7583
|
+
// it here rather than only the turn's running list.
|
|
7584
|
+
const fact = `The workflow failed: ${sentence}`;
|
|
7585
|
+
turn.settlementFacts.push(fact);
|
|
7586
|
+
if (turn.report !== undefined) {
|
|
7587
|
+
turn.report = {
|
|
7588
|
+
...turn.report,
|
|
7589
|
+
facts: [...turn.report.facts, fact],
|
|
7590
|
+
...(turn.report.bossFacts === undefined
|
|
7591
|
+
? {}
|
|
7592
|
+
: { bossFacts: [...turn.report.bossFacts, fact] }),
|
|
7593
|
+
};
|
|
7594
|
+
}
|
|
7595
|
+
};
|
|
7596
|
+
|
|
7189
7597
|
const settleSelection = async (
|
|
7190
7598
|
selection: CaptainControllerSelection,
|
|
7191
7599
|
signal: AbortSignal,
|
|
@@ -7202,6 +7610,10 @@ export function createPlaybookCaptainShell(
|
|
|
7202
7610
|
if (turn !== undefined && report !== undefined) {
|
|
7203
7611
|
appendMandatoryPresentationSuffix(turn, report);
|
|
7204
7612
|
}
|
|
7613
|
+
if (turn !== undefined) reportCarriedPreExistingChanges(turn);
|
|
7614
|
+
if (turn !== undefined) {
|
|
7615
|
+
reportLeafFailure(turn, frozenUnresolvedEffects);
|
|
7616
|
+
}
|
|
7205
7617
|
return frozenUnresolvedEffects;
|
|
7206
7618
|
};
|
|
7207
7619
|
const finalizeSettlement = (
|
|
@@ -9012,12 +9424,25 @@ export function createPlaybookCaptainShell(
|
|
|
9012
9424
|
const hostGiveUp = decided?.kind === 'give-up' ? true : undefined;
|
|
9013
9425
|
const parsed =
|
|
9014
9426
|
decided === undefined ? resolveCommandTurn(turn.prompt) : undefined;
|
|
9427
|
+
// DR-062 §5: the boundary count this turn starts from. Every boundary
|
|
9428
|
+
// beyond it completed during this turn.
|
|
9429
|
+
let effectBoundaryPrefixCount: number | undefined;
|
|
9430
|
+
try {
|
|
9431
|
+
effectBoundaryPrefixCount = assertPlaybookEffectLedger(
|
|
9432
|
+
currentEffectLedger(),
|
|
9433
|
+
).boundaries.length;
|
|
9434
|
+
} catch {
|
|
9435
|
+
effectBoundaryPrefixCount = undefined;
|
|
9436
|
+
}
|
|
9015
9437
|
activeTurn = {
|
|
9016
9438
|
id: ++turnSequence,
|
|
9017
9439
|
captainSyncedJournalSeq: journalSeq,
|
|
9018
9440
|
bossText: turn.prompt,
|
|
9019
9441
|
authoritativeText: parsed?.authoritativeText ?? turn.prompt,
|
|
9020
9442
|
...(parsed ? { resolution: parsed.resolution } : {}),
|
|
9443
|
+
...(effectBoundaryPrefixCount === undefined
|
|
9444
|
+
? {}
|
|
9445
|
+
: { effectBoundaryPrefixCount }),
|
|
9021
9446
|
...(hostRuntimeActionId === undefined ? {} : { hostRuntimeActionId }),
|
|
9022
9447
|
...(hostGiveUp === undefined ? {} : { hostGiveUp }),
|
|
9023
9448
|
settled: false,
|
|
@@ -72,7 +72,9 @@ export interface SessionHostController {
|
|
|
72
72
|
/**
|
|
73
73
|
* The active leaf's currently advertised runtime actions, read live between
|
|
74
74
|
* turns; empty whenever nothing is advertised, a turn is active, or the
|
|
75
|
-
* leaf's control view cannot be read.
|
|
75
|
+
* leaf's control view cannot be read. Each pair carries the leaf's own
|
|
76
|
+
* `standing` and its `reason`, so a host never draws a control without
|
|
77
|
+
* saying what running it would do (DR-063).
|
|
76
78
|
*/
|
|
77
79
|
listRuntimeActions(): readonly PlaybookControlAction[];
|
|
78
80
|
/**
|
|
@@ -83,8 +85,9 @@ export interface SessionHostController {
|
|
|
83
85
|
submitRuntimeAction(actionId: string): Promise<SessionRecovery>;
|
|
84
86
|
/**
|
|
85
87
|
* The controls the shell offers on its own behalf, read live between turns;
|
|
86
|
-
* today one give-up while a root is engaged, and empty while idle or
|
|
87
|
-
* turn is active. Disjoint from `listRuntimeActions`, which reads the
|
|
88
|
+
* today one `ready` give-up while a root is engaged, and empty while idle or
|
|
89
|
+
* while a turn is active. Disjoint from `listRuntimeActions`, which reads the
|
|
90
|
+
* leaf.
|
|
88
91
|
*/
|
|
89
92
|
listShellActions(): readonly PlaybookControlAction[];
|
|
90
93
|
/**
|
package/reference/sdlc/decide.md
CHANGED
|
@@ -43,7 +43,7 @@ Do not change code or implement the design.
|
|
|
43
43
|
Commit the result as one new commit, following @specs/packages/git.md.
|
|
44
44
|
Make the commit message explain concisely what changed and why.
|
|
45
45
|
Identify every new commit you make.
|
|
46
|
-
Coder
|
|
46
|
+
Credit every AI that contributed to this commit: Coder <coder-llm> and Reviewer <reviewer-llm>, whose proposal it carries.
|
|
47
47
|
|
|
48
48
|
> Original topic: \<caller-topic\>
|
|
49
49
|
> Reviewer's independent proposal: \<reviewer-proposal\>
|