@sublang/playbook 13.3.0 → 14.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 +5 -5
- package/docs/configuration.md +10 -2
- package/docs/embedding.md +20 -1
- package/package.json +6 -1
- package/reference/sdlc/code.playbook/bin/launch-config.js +88 -5
- package/reference/sdlc/code.playbook/bin/playbook.js +4 -0
- package/reference/sdlc/code.playbook/bin/repository-effects.js +11 -3
- package/reference/sdlc/code.playbook/bin/run.js +4 -0
- package/reference/sdlc/code.playbook/bin/session-store.js +2 -2
- package/reference/sdlc/code.playbook/playbook-captain.js +17 -4
- package/reference/sdlc/code.playbook/playbook-captain.ts +19 -4
- package/reference/sdlc/dev.playbook/dev.playbook.d.ts +3 -10
- package/reference/sdlc/dev.playbook/dev.playbook.js +5 -5
- package/reference/sdlc/dev.playbook/dev.playbook.ts +7 -7
- package/reference/sdlc/pr.md +17 -4
- package/reference/sdlc/pr.playbook/pr.fsm.d.ts +17 -3
- package/reference/sdlc/pr.playbook/pr.fsm.js +49 -18
- package/reference/sdlc/pr.playbook/pr.fsm.ts +63 -20
- package/reference/sdlc/pr.playbook/pr.gears.md +20 -7
- package/reference/sdlc/pr.playbook/pr.playbook.js +2 -1
- package/reference/sdlc/pr.playbook/pr.playbook.ts +2 -1
- package/slc/experiments/fsm-scaffold-guidance.md +31 -0
- package/slc/gears2fsm.md +145 -58
- package/slc/link.md +158 -7
- package/slc/materialize-link.mjs +460 -0
- package/slc/optimize.md +46 -6
- package/slc/scaffold-fsm.mjs +225 -0
- package/slc/slc.pin-inputs.json +13 -0
- package/slc/text2gears.md +38 -3
- package/slc/workflow-contracts.json +371 -0
- package/src/xstate-playbook-runtime.js +35 -1
- package/src/xstate-playbook-runtime.ts +54 -1
package/README.md
CHANGED
|
@@ -32,9 +32,10 @@ Run `playbook` for an interactive tmux UI powered by [cligent](https://github.co
|
|
|
32
32
|
Out of the box, Playbook includes **CODE** for implementation, **REVIEW** for commit-based review and fixes, **DECIDE** for independently proposed and reviewed specification decisions, **DEV** for repository-aware planning of a development request, **BRANCH** for checking out a new branch for a GitHub issue, and **PR** for publishing, checking, and merging that branch through a pull request.
|
|
33
33
|
CODE and DECIDE call REVIEW as a nested playbook; DEV calls CODE, or DECIDE and then CODE, and wraps them in BRANCH and PR when the request names a GitHub issue or asks for a pull request.
|
|
34
34
|
|
|
35
|
-
The shared starter config
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
The shared starter config selects one adapter for the Captain and all players
|
|
36
|
+
from locally visible credentials: Claude first, then Codex. If neither is
|
|
37
|
+
configured, it seeds Claude and prints a notice. CODE, REVIEW, DECIDE, BRANCH, and PR bind
|
|
38
|
+
their local roles explicitly to `dev.coder` and `dev.reviewer`, so nested and later
|
|
38
39
|
engagements share a conversation only where their bindings name the same
|
|
39
40
|
player ID; DEV's Analyst binds a distinct `dev.analyst` player.
|
|
40
41
|
|
|
@@ -49,8 +50,7 @@ If an SDK is missing or older than cligent supports, Playbook prints the pinned
|
|
|
49
50
|
Prerequisites:
|
|
50
51
|
|
|
51
52
|
- Node.js >= 20.6.0
|
|
52
|
-
-
|
|
53
|
-
- Authenticated [Codex CLI](https://github.com/openai/codex) or `OPENAI_API_KEY`
|
|
53
|
+
- Credentials for the configured adapter: authenticated [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) or `ANTHROPIC_API_KEY` for Claude; authenticated [Codex CLI](https://github.com/openai/codex) or `OPENAI_API_KEY` for Codex
|
|
54
54
|
- [GitHub CLI](https://cli.github.com/) (`gh`) authenticated for the repository's GitHub remote, for `/branch`, `/pr`, and `/dev` requests that name an issue or ask for a pull request
|
|
55
55
|
|
|
56
56
|
Interactive `playbook` additionally needs tmux 3.3 or newer and [`glow`](https://github.com/charmbracelet/glow#installation) on `PATH`; headless `playbook run` does not.
|
package/docs/configuration.md
CHANGED
|
@@ -55,8 +55,16 @@ key is that playbook's option slice. Every manifest role must be present
|
|
|
55
55
|
exactly once. The launcher injects the rest — you do not write host wiring by
|
|
56
56
|
hand.
|
|
57
57
|
|
|
58
|
-
The
|
|
59
|
-
|
|
58
|
+
The launcher seeds one adapter for the Captain and all stable players from
|
|
59
|
+
locally visible credentials: Claude first, then Codex. It uses Claude Opus 5
|
|
60
|
+
for Claude or GPT-5.6 Sol for Codex, with Captain and Coder at `high` effort
|
|
61
|
+
and Reviewer and Analyst at `xhigh`. If neither adapter is configured, it
|
|
62
|
+
seeds Claude and prints a notice. Credentials do not prove SDK availability
|
|
63
|
+
or remaining quota; launch still checks the configured adapter. Existing
|
|
64
|
+
configs are left unchanged.
|
|
65
|
+
|
|
66
|
+
The Claude default is shown below. A Codex seed also adds
|
|
67
|
+
`permissions.writablePaths: ['.git']` to each agent:
|
|
60
68
|
|
|
61
69
|
```yaml
|
|
62
70
|
captain:
|
package/docs/embedding.md
CHANGED
|
@@ -433,7 +433,7 @@ defines the files, versions and compatibility rules.
|
|
|
433
433
|
|
|
434
434
|
## Reading the published spec contracts
|
|
435
435
|
|
|
436
|
-
The
|
|
436
|
+
The compiler definitions and supporting assets ship in the package and are exposed
|
|
437
437
|
as a public, semver-stable surface under `@sublang/playbook/slc/*`.
|
|
438
438
|
Resolve and read one with `import.meta.resolve` plus `fs`:
|
|
439
439
|
|
|
@@ -450,3 +450,22 @@ The four specs are [`slc/text2gears.md`](../slc/text2gears.md),
|
|
|
450
450
|
[`slc/link.md`](../slc/link.md) — the FSM-to-runtime contract that
|
|
451
451
|
`@sublang/playbook/runtime` projects into TypeScript — and
|
|
452
452
|
[`slc/optimize.md`](../slc/optimize.md).
|
|
453
|
+
|
|
454
|
+
Five supporting assets use the same resolution mechanism:
|
|
455
|
+
|
|
456
|
+
| Asset | Purpose |
|
|
457
|
+
| --- | --- |
|
|
458
|
+
| [`slc/workflow-contracts.json`](../slc/workflow-contracts.json) | Public output schemas and default literal target bindings for the builtin REVIEW, DECIDE, CODE, BRANCH, and PR workflows; compilers and embedders can read them without importing workflow implementations. |
|
|
459
|
+
| [`slc/materialize-link.mjs`](../slc/materialize-link.mjs) | Optional CLI that emits a thin linked module from a supported FSM and JSON descriptor; see the profiles and invocation in `slc/link.md`. |
|
|
460
|
+
| [`slc/scaffold-fsm.mjs`](../slc/scaffold-fsm.mjs) | Optional CLI that initializes an incomplete typed FSM from supported GEARS source. |
|
|
461
|
+
| [`slc/experiments/fsm-scaffold-guidance.md`](../slc/experiments/fsm-scaffold-guidance.md) | Opt-in compiler guidance for the initializer, outside ordinary phase discovery. |
|
|
462
|
+
| [`slc/slc.pin-inputs.json`](../slc/slc.pin-inputs.json) | SLC semantic-input closures for `gears2fsm` and `link`, including the workflow catalog and, for `link`, the materializer. Other phases use SLC's inline-input discovery. |
|
|
463
|
+
|
|
464
|
+
For example, resolve `@sublang/playbook/slc/workflow-contracts.json`, read it
|
|
465
|
+
with `readFile`, and parse the result as JSON to inspect the published workflow
|
|
466
|
+
interfaces. Execute the `.mjs` helpers with Node and their documented CLI
|
|
467
|
+
arguments: their supported public interfaces are their resolvable paths and CLI
|
|
468
|
+
contracts. Their JavaScript module exports are internal implementation details
|
|
469
|
+
and carry no SemVer compatibility guarantee.
|
|
470
|
+
The initializer requires further semantic compilation and verification
|
|
471
|
+
before its output is runnable.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sublang/playbook",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "14.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Composable XState v5 playbook runtime with compiled Captain, CODE, REVIEW, DECIDE, DEV, BRANCH, and PR workflows driven by GEARS specs.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
"src/xstate-playbook-runtime.js",
|
|
39
39
|
"src/xstate-playbook-runtime.d.ts",
|
|
40
40
|
"slc/link.md",
|
|
41
|
+
"slc/materialize-link.mjs",
|
|
42
|
+
"slc/scaffold-fsm.mjs",
|
|
43
|
+
"slc/experiments/fsm-scaffold-guidance.md",
|
|
44
|
+
"slc/slc.pin-inputs.json",
|
|
45
|
+
"slc/workflow-contracts.json",
|
|
41
46
|
"slc/gears2fsm.md",
|
|
42
47
|
"slc/text2gears.md",
|
|
43
48
|
"slc/optimize.md",
|
|
@@ -180,7 +180,7 @@ export function resolveLaunchSessionsDir({
|
|
|
180
180
|
if (sessionsDir !== undefined) return sessionsDir;
|
|
181
181
|
|
|
182
182
|
if (preparePrimary) {
|
|
183
|
-
seedUserConfigIfMissing(userConfigPath, templatePath, onNotice);
|
|
183
|
+
seedUserConfigIfMissing(userConfigPath, templatePath, onNotice, env, homeDir);
|
|
184
184
|
migrateUserConfigIfRetired(userConfigPath, onNotice);
|
|
185
185
|
}
|
|
186
186
|
|
|
@@ -248,8 +248,10 @@ export async function loadLaunchPlan({
|
|
|
248
248
|
templatePath = DEFAULT_TEMPLATE_PATH,
|
|
249
249
|
onNotice = () => {},
|
|
250
250
|
selectedMembers,
|
|
251
|
+
env = process.env,
|
|
252
|
+
homeDir = env.HOME ?? homedir(),
|
|
251
253
|
}) {
|
|
252
|
-
seedUserConfigIfMissing(userConfigPath, templatePath, onNotice);
|
|
254
|
+
seedUserConfigIfMissing(userConfigPath, templatePath, onNotice, env, homeDir);
|
|
253
255
|
// PBCLI-22/46: a reopen must not rewrite, validate, or otherwise inspect
|
|
254
256
|
// config members outside the stored projection.
|
|
255
257
|
if (selectedMembers === undefined) {
|
|
@@ -288,10 +290,12 @@ export async function loadSelectedLaunchPlanDataOnly({
|
|
|
288
290
|
structuralProjection,
|
|
289
291
|
templatePath = DEFAULT_TEMPLATE_PATH,
|
|
290
292
|
onNotice = () => {},
|
|
293
|
+
env = process.env,
|
|
294
|
+
homeDir = env.HOME ?? homedir(),
|
|
291
295
|
}) {
|
|
292
296
|
const stored = validateStoredStructuralProjection(structuralProjection);
|
|
293
297
|
const selectedMembers = selectedMembersFromStoredStructure(stored);
|
|
294
|
-
seedUserConfigIfMissing(userConfigPath, templatePath, onNotice);
|
|
298
|
+
seedUserConfigIfMissing(userConfigPath, templatePath, onNotice, env, homeDir);
|
|
295
299
|
|
|
296
300
|
let top = parseYaml(readFileSync(userConfigPath, "utf8")) ?? {};
|
|
297
301
|
if (overlayPaths.length > 0 && !isObject(top)) {
|
|
@@ -1335,11 +1339,90 @@ function assertLegacyRelocationLocatorsSafe(
|
|
|
1335
1339
|
);
|
|
1336
1340
|
}
|
|
1337
1341
|
|
|
1338
|
-
|
|
1342
|
+
// PBCLI-11 / DR-053: the adapters a seeded lineup may take, highest
|
|
1343
|
+
// precedence first. A fixed order — not whichever the probe answers
|
|
1344
|
+
// first — is what makes one machine seed one file twice.
|
|
1345
|
+
const SEED_LINEUP = Object.freeze([
|
|
1346
|
+
Object.freeze({ adapter: "claude", model: "claude-opus-5" }),
|
|
1347
|
+
Object.freeze({ adapter: "codex", model: "gpt-5.6-sol" }),
|
|
1348
|
+
]);
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* PBCLI-11: the lineup a seed would write here, and whether anything
|
|
1352
|
+
* actually probed ready.
|
|
1353
|
+
*
|
|
1354
|
+
* Only the credential check participates. The SDK probe constructs an
|
|
1355
|
+
* adapter and awaits `isAvailable()`, which reads whatever happens to
|
|
1356
|
+
* be installed and spawns a subprocess for CLI-backed adapters: a seed
|
|
1357
|
+
* consulting it would differ between a developer's machine and a clean
|
|
1358
|
+
* one, and would make creating a file asynchronous.
|
|
1359
|
+
*/
|
|
1360
|
+
export function selectSeedLineup(env = process.env, home = homedir()) {
|
|
1361
|
+
const ready = SEED_LINEUP.filter(
|
|
1362
|
+
(entry) =>
|
|
1363
|
+
checkReadiness([entry.adapter], env, home).failingAdapters.length === 0,
|
|
1364
|
+
);
|
|
1365
|
+
return { lineup: ready[0] ?? SEED_LINEUP[0], anyReady: ready.length > 0 };
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* The template is the documented default and the only source of the
|
|
1370
|
+
* starter's structure and comments, so the default choice is still the
|
|
1371
|
+
* byte copy it always was. A different adapter rewrites the agent
|
|
1372
|
+
* scalars in place, which the Document API carries comments through.
|
|
1373
|
+
*/
|
|
1374
|
+
function seedTextForLineup(templatePath, lineup) {
|
|
1375
|
+
const template = readFileSync(templatePath, "utf8");
|
|
1376
|
+
if (lineup.adapter === SEED_LINEUP[0].adapter) return template;
|
|
1377
|
+
const doc = parseYamlDocument(template);
|
|
1378
|
+
const players = doc.getIn(["players"]);
|
|
1379
|
+
const agentPaths = [
|
|
1380
|
+
["captain"],
|
|
1381
|
+
...[...(players?.items ?? [])].map((pair) => [
|
|
1382
|
+
"players",
|
|
1383
|
+
String(pair.key),
|
|
1384
|
+
]),
|
|
1385
|
+
];
|
|
1386
|
+
for (const path of agentPaths) {
|
|
1387
|
+
doc.setIn([...path, "adapter"], lineup.adapter);
|
|
1388
|
+
doc.setIn([...path, "model"], lineup.model);
|
|
1389
|
+
// A codex agent writes git metadata under its sandbox; claude's
|
|
1390
|
+
// auto mode needs no grant, so the key exists for one adapter only.
|
|
1391
|
+
if (lineup.adapter === "codex") {
|
|
1392
|
+
doc.setIn([...path, "permissions", "writablePaths"], [".git"]);
|
|
1393
|
+
} else {
|
|
1394
|
+
doc.deleteIn([...path, "permissions", "writablePaths"]);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
return String(doc);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function seedUserConfigIfMissing(
|
|
1401
|
+
userConfigPath,
|
|
1402
|
+
templatePath,
|
|
1403
|
+
onNotice,
|
|
1404
|
+
env = process.env,
|
|
1405
|
+
homeDir = env.HOME ?? homedir(),
|
|
1406
|
+
) {
|
|
1339
1407
|
if (existsSync(userConfigPath)) return;
|
|
1340
1408
|
mkdirSync(dirname(userConfigPath), { recursive: true });
|
|
1341
|
-
|
|
1409
|
+
const { lineup, anyReady } = selectSeedLineup(env, homeDir);
|
|
1410
|
+
if (lineup.adapter === SEED_LINEUP[0].adapter && anyReady) {
|
|
1411
|
+
// Unchanged default path: one exclusive create of the template bytes.
|
|
1412
|
+
copyFileSync(templatePath, userConfigPath, constants.COPYFILE_EXCL);
|
|
1413
|
+
} else {
|
|
1414
|
+
// Still one exclusive create, so two seeding calls cannot disagree.
|
|
1415
|
+
writeFileSync(userConfigPath, seedTextForLineup(templatePath, lineup), {
|
|
1416
|
+
encoding: "utf8",
|
|
1417
|
+
flag: "wx",
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1342
1420
|
onNotice(`playbook: created config at ${userConfigPath}\n`);
|
|
1421
|
+
if (!anyReady) {
|
|
1422
|
+
onNotice(
|
|
1423
|
+
`playbook: no adapter probed ready; seeded ${lineup.adapter}\n`,
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1343
1426
|
}
|
|
1344
1427
|
|
|
1345
1428
|
// DR-021 §3: migrate once, keeping the original before any rewrite.
|
|
@@ -346,10 +346,14 @@ export async function runPlaybookCli(options = {}) {
|
|
|
346
346
|
overlayPaths: withPaths,
|
|
347
347
|
structuralProjection: selectedRecord.structuralProjection,
|
|
348
348
|
onNotice: (line) => stderr.write(line),
|
|
349
|
+
env,
|
|
350
|
+
homeDir: home,
|
|
349
351
|
})
|
|
350
352
|
: await loadLaunchPlan({
|
|
351
353
|
userConfigPath,
|
|
352
354
|
overlayPaths: withPaths,
|
|
355
|
+
env,
|
|
356
|
+
homeDir: home,
|
|
353
357
|
loadModule,
|
|
354
358
|
prepareRegistryModule:
|
|
355
359
|
options.prepareRegistryModule ??
|
|
@@ -659,10 +659,18 @@ export async function resolveCanonicalGitWorktree(cwd) {
|
|
|
659
659
|
}
|
|
660
660
|
|
|
661
661
|
async function sampleWorktree(worktree, env) {
|
|
662
|
-
const
|
|
663
|
-
|
|
662
|
+
const [headResult, visibilityResult, statusResult] = await Promise.allSettled([
|
|
663
|
+
resolveHead(worktree, env),
|
|
664
|
+
rawIndexVisibility(worktree, env),
|
|
665
|
+
rawRepositoryStatus(worktree, env),
|
|
666
|
+
]);
|
|
667
|
+
if (headResult.status === 'rejected') throw headResult.reason;
|
|
668
|
+
if (visibilityResult.status === 'rejected') throw visibilityResult.reason;
|
|
669
|
+
const head = headResult.value;
|
|
670
|
+
const indexVisibility = visibilityResult.value;
|
|
664
671
|
assertIndexVisibility(indexVisibility);
|
|
665
|
-
|
|
672
|
+
if (statusResult.status === 'rejected') throw statusResult.reason;
|
|
673
|
+
const status = statusResult.value;
|
|
666
674
|
const projection = await projectionFromStatus(worktree, status);
|
|
667
675
|
return { head, indexVisibility, status, projection };
|
|
668
676
|
}
|
|
@@ -359,10 +359,14 @@ export async function runPlaybookRun(options = {}) {
|
|
|
359
359
|
overlayPaths: args.withPaths,
|
|
360
360
|
structuralProjection: priorRecord.structuralProjection,
|
|
361
361
|
onNotice: (line) => configNotices.push(line),
|
|
362
|
+
env,
|
|
363
|
+
homeDir: home,
|
|
362
364
|
})
|
|
363
365
|
: await loadLaunchPlan({
|
|
364
366
|
userConfigPath,
|
|
365
367
|
overlayPaths: args.withPaths,
|
|
368
|
+
env,
|
|
369
|
+
homeDir: home,
|
|
366
370
|
loadModule,
|
|
367
371
|
prepareRegistryModule,
|
|
368
372
|
onNotice: (line) => configNotices.push(line),
|
|
@@ -3163,7 +3163,7 @@ async function createLease({
|
|
|
3163
3163
|
}
|
|
3164
3164
|
if (
|
|
3165
3165
|
snapshot.mode !== 'chat' ||
|
|
3166
|
-
snapshot.lastAction !== 'runtime' ||
|
|
3166
|
+
(snapshot.lastAction !== 'runtime' && snapshot.lastAction !== 'dismiss') ||
|
|
3167
3167
|
snapshot.lastSettlementStatus !== 'ok'
|
|
3168
3168
|
) {
|
|
3169
3169
|
throw new Error(
|
|
@@ -4221,7 +4221,7 @@ function validateCanonicalCaptainSessionRecord(
|
|
|
4221
4221
|
}
|
|
4222
4222
|
if (
|
|
4223
4223
|
snapshot.mode !== 'chat' ||
|
|
4224
|
-
snapshot.lastAction !== 'runtime' ||
|
|
4224
|
+
(snapshot.lastAction !== 'runtime' && snapshot.lastAction !== 'dismiss') ||
|
|
4225
4225
|
snapshot.lastSettlementStatus !== 'ok' ||
|
|
4226
4226
|
Object.hasOwn(record.retainedGenerations ?? {}, rootPlaybookId)
|
|
4227
4227
|
) {
|
|
@@ -3321,6 +3321,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3321
3321
|
* may return an executed receipt to its result phase.
|
|
3322
3322
|
*/
|
|
3323
3323
|
const settleUnresolvedEffectAbandonment = async (unresolvedLeaf) => {
|
|
3324
|
+
abandonmentSettlementUnsafe = true;
|
|
3324
3325
|
if (leafFrame() !== unresolvedLeaf) {
|
|
3325
3326
|
throw new Error('unresolved-effect abandonment requires the active leaf');
|
|
3326
3327
|
}
|
|
@@ -3347,6 +3348,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3347
3348
|
rootPlaybookId,
|
|
3348
3349
|
});
|
|
3349
3350
|
await runEffect(() => unresolvedEffectSettlement.complete(settlement));
|
|
3351
|
+
abandonmentSettlementUnsafe = false;
|
|
3350
3352
|
};
|
|
3351
3353
|
const callResultFor = (frame, result) => {
|
|
3352
3354
|
if (result.outcome === 'unresolved-effect') {
|
|
@@ -4693,6 +4695,12 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4693
4695
|
if (!root)
|
|
4694
4696
|
return false;
|
|
4695
4697
|
const label = frameLabel(root);
|
|
4698
|
+
if (activeTurn?.hostGiveUp === true &&
|
|
4699
|
+
freezeTurnUnresolvedEffects().length > 0) {
|
|
4700
|
+
await settleUnresolvedEffectAbandonment(leafFrame());
|
|
4701
|
+
facts.push(`Dismissed the ${label} engagement with unresolved repository effects reported.`);
|
|
4702
|
+
return false;
|
|
4703
|
+
}
|
|
4696
4704
|
// Dismissal leaves the procedure unfinished. Persist the latest safe
|
|
4697
4705
|
// generation captured for this turn before disposal erases the frames —
|
|
4698
4706
|
// unless the Boss gave up on it (CAPTAIN-44), in which case retaining it
|
|
@@ -5066,7 +5074,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5066
5074
|
selection.actionId === UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID) &&
|
|
5067
5075
|
fencedLeaf !== undefined;
|
|
5068
5076
|
if (retainedEffectReconciliation !== undefined &&
|
|
5069
|
-
!routesRetainedReconciliation
|
|
5077
|
+
!routesRetainedReconciliation &&
|
|
5078
|
+
!(selection.action === 'dismiss' && turn.hostGiveUp === true)) {
|
|
5070
5079
|
return rejectSelection(selection, 'retained work must reconcile its repository-effect evidence before an ordinary action can run');
|
|
5071
5080
|
}
|
|
5072
5081
|
if (selection.action === 'resume') {
|
|
@@ -5359,7 +5368,6 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5359
5368
|
await settleUnresolvedEffectAbandonment(leaf);
|
|
5360
5369
|
}
|
|
5361
5370
|
catch (error) {
|
|
5362
|
-
abandonmentSettlementUnsafe = true;
|
|
5363
5371
|
const normalized = normalizeErrorCompact(error) ?? {
|
|
5364
5372
|
name: 'Error',
|
|
5365
5373
|
message: String(error),
|
|
@@ -5473,8 +5481,13 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5473
5481
|
const giveUpReplyText = () => {
|
|
5474
5482
|
const report = activeTurn?.report;
|
|
5475
5483
|
const facts = report?.bossFacts ?? report?.facts ?? [];
|
|
5476
|
-
const
|
|
5477
|
-
const
|
|
5484
|
+
const stopped = report?.status === 'ok';
|
|
5485
|
+
const opening = stopped
|
|
5486
|
+
? 'Stopped that workflow. I will not resume it.'
|
|
5487
|
+
: 'I could not confirm that the workflow was stopped.';
|
|
5488
|
+
const closing = stopped
|
|
5489
|
+
? 'Send the command again to start fresh work.'
|
|
5490
|
+
: 'The stop did not settle successfully; recover the session before continuing.';
|
|
5478
5491
|
const composed = [
|
|
5479
5492
|
opening,
|
|
5480
5493
|
...(facts.length === 0
|
|
@@ -5229,6 +5229,7 @@ export function createPlaybookCaptainShell(
|
|
|
5229
5229
|
const settleUnresolvedEffectAbandonment = async (
|
|
5230
5230
|
unresolvedLeaf: EngagementFrame,
|
|
5231
5231
|
): Promise<void> => {
|
|
5232
|
+
abandonmentSettlementUnsafe = true;
|
|
5232
5233
|
if (leafFrame() !== unresolvedLeaf) {
|
|
5233
5234
|
throw new Error(
|
|
5234
5235
|
'unresolved-effect abandonment requires the active leaf',
|
|
@@ -5263,6 +5264,7 @@ export function createPlaybookCaptainShell(
|
|
|
5263
5264
|
rootPlaybookId,
|
|
5264
5265
|
});
|
|
5265
5266
|
await runEffect(() => unresolvedEffectSettlement.complete(settlement));
|
|
5267
|
+
abandonmentSettlementUnsafe = false;
|
|
5266
5268
|
};
|
|
5267
5269
|
|
|
5268
5270
|
const callResultFor = (
|
|
@@ -6913,6 +6915,14 @@ export function createPlaybookCaptainShell(
|
|
|
6913
6915
|
const root = rootFrame();
|
|
6914
6916
|
if (!root) return false;
|
|
6915
6917
|
const label = frameLabel(root);
|
|
6918
|
+
if (
|
|
6919
|
+
activeTurn?.hostGiveUp === true &&
|
|
6920
|
+
freezeTurnUnresolvedEffects().length > 0
|
|
6921
|
+
) {
|
|
6922
|
+
await settleUnresolvedEffectAbandonment(leafFrame()!);
|
|
6923
|
+
facts.push(`Dismissed the ${label} engagement with unresolved repository effects reported.`);
|
|
6924
|
+
return false;
|
|
6925
|
+
}
|
|
6916
6926
|
// Dismissal leaves the procedure unfinished. Persist the latest safe
|
|
6917
6927
|
// generation captured for this turn before disposal erases the frames —
|
|
6918
6928
|
// unless the Boss gave up on it (CAPTAIN-44), in which case retaining it
|
|
@@ -7368,7 +7378,8 @@ export function createPlaybookCaptainShell(
|
|
|
7368
7378
|
fencedLeaf !== undefined;
|
|
7369
7379
|
if (
|
|
7370
7380
|
retainedEffectReconciliation !== undefined &&
|
|
7371
|
-
!routesRetainedReconciliation
|
|
7381
|
+
!routesRetainedReconciliation &&
|
|
7382
|
+
!(selection.action === 'dismiss' && turn.hostGiveUp === true)
|
|
7372
7383
|
) {
|
|
7373
7384
|
return rejectSelection(
|
|
7374
7385
|
selection,
|
|
@@ -7726,7 +7737,6 @@ export function createPlaybookCaptainShell(
|
|
|
7726
7737
|
try {
|
|
7727
7738
|
await settleUnresolvedEffectAbandonment(leaf);
|
|
7728
7739
|
} catch (error) {
|
|
7729
|
-
abandonmentSettlementUnsafe = true;
|
|
7730
7740
|
const normalized = normalizeErrorCompact(error) ?? {
|
|
7731
7741
|
name: 'Error',
|
|
7732
7742
|
message: String(error),
|
|
@@ -7852,8 +7862,13 @@ export function createPlaybookCaptainShell(
|
|
|
7852
7862
|
const giveUpReplyText = (): string => {
|
|
7853
7863
|
const report = activeTurn?.report;
|
|
7854
7864
|
const facts = report?.bossFacts ?? report?.facts ?? [];
|
|
7855
|
-
const
|
|
7856
|
-
const
|
|
7865
|
+
const stopped = report?.status === 'ok';
|
|
7866
|
+
const opening = stopped
|
|
7867
|
+
? 'Stopped that workflow. I will not resume it.'
|
|
7868
|
+
: 'I could not confirm that the workflow was stopped.';
|
|
7869
|
+
const closing = stopped
|
|
7870
|
+
? 'Send the command again to start fresh work.'
|
|
7871
|
+
: 'The stop did not settle successfully; recover the session before continuing.';
|
|
7857
7872
|
const composed = [
|
|
7858
7873
|
opening,
|
|
7859
7874
|
...(facts.length === 0
|
|
@@ -1,19 +1,12 @@
|
|
|
1
|
-
import { type XStatePlaybookRuntimeFactory, type XStatePlaybookRuntimeConstruction } from '@sublang/playbook/xstate-runtime';
|
|
2
|
-
import { type DevInput
|
|
1
|
+
import { type PlaybookPlayerInput, type XStatePlaybookRuntimeFactory, type XStatePlaybookRuntimeConstruction } from '@sublang/playbook/xstate-runtime';
|
|
2
|
+
import { type DevInput } from './dev.fsm.js';
|
|
3
3
|
import type { PlaybookHostConstructionCapabilities } from '../code.playbook/playbook-captain.js';
|
|
4
4
|
import type { CaptainCallOptions, CaptainResult, JsonValue, NormalizedError, PlayerCallOptions, PlaybookCallRequest, PlaybookCallResult, PlaybookCallStart, PlaybookControlReceipt, PlaybookControlView, PlaybookPendingCall, PlaybookPorts, PlaybookRunResult, PlaybookRuntime, PlaybookRuntimeFactory, PlaybookRuntimeSnapshot, PlaybookSession, PlaybookState, PlaybookStateValue, PlaybookTraceEvent, PlaybookTraceType, PlayerResult, PlayerSessionStore } from '@sublang/playbook/runtime';
|
|
5
5
|
export type { CaptainCallOptions, CaptainResult, JsonValue, NormalizedError, PlayerCallOptions, PlaybookCallRequest, PlaybookCallResult, PlaybookCallStart, PlaybookControlReceipt, PlaybookControlView, PlaybookPendingCall, PlaybookPorts, PlaybookRunResult, PlaybookRuntime, PlaybookRuntimeFactory, PlaybookRuntimeSnapshot, PlaybookSession, PlaybookState, PlaybookStateValue, PlaybookTraceEvent, PlaybookTraceType, PlayerResult, PlayerSessionStore, };
|
|
6
6
|
export type DevPlaybookOptions = DevInput;
|
|
7
7
|
export type DevPlaybookHostCapabilities = PlaybookHostConstructionCapabilities & XStatePlaybookRuntimeConstruction<DevPlaybookOptions, object>['hostCapabilities'];
|
|
8
|
-
/**
|
|
9
|
-
* Preserve authored Markdown quote markers around every line of relayed
|
|
10
|
-
* runtime text. The generic composer preserves the marker itself; DEV's
|
|
11
|
-
* override additionally keeps a multiline value inside that quote and drops
|
|
12
|
-
* the optional relays that have no value yet.
|
|
13
|
-
*/
|
|
14
|
-
declare function composePlayerPrompt(input: PlayerInput, resuming?: boolean): string;
|
|
15
8
|
export declare const _internal: {
|
|
16
|
-
composePlayerPrompt:
|
|
9
|
+
composePlayerPrompt: (input: PlaybookPlayerInput, _identity: import("@sublang/playbook/xstate-runtime").XStatePromptIdentity, resuming?: boolean) => string;
|
|
17
10
|
VERBATIM_PAYLOAD_FIELDS: ReadonlySet<string>;
|
|
18
11
|
UNFINISHED_FINAL_STATE_IDS: ReadonlySet<string>;
|
|
19
12
|
};
|
|
@@ -73,11 +73,6 @@ function composePlayerPrompt(input, resuming = false) {
|
|
|
73
73
|
});
|
|
74
74
|
return composePlayerContinuation(input, body, resuming);
|
|
75
75
|
}
|
|
76
|
-
export const _internal = {
|
|
77
|
-
composePlayerPrompt,
|
|
78
|
-
VERBATIM_PAYLOAD_FIELDS,
|
|
79
|
-
UNFINISHED_FINAL_STATE_IDS,
|
|
80
|
-
};
|
|
81
76
|
const runtimeSpec = {
|
|
82
77
|
label: 'DEV',
|
|
83
78
|
// DR-022 / slc/link.md: the declaration carries the value current at link
|
|
@@ -141,5 +136,10 @@ const runtimeSpec = {
|
|
|
141
136
|
unfinishedFinalStateIds: UNFINISHED_FINAL_STATE_IDS,
|
|
142
137
|
transitionEventFields: ['developmentRequest', 'answer', 'questionId'],
|
|
143
138
|
};
|
|
139
|
+
export const _internal = {
|
|
140
|
+
composePlayerPrompt: runtimeSpec.composePlayerPrompt,
|
|
141
|
+
VERBATIM_PAYLOAD_FIELDS,
|
|
142
|
+
UNFINISHED_FINAL_STATE_IDS,
|
|
143
|
+
};
|
|
144
144
|
const createPlaybookRuntime = createXStatePlaybookRuntime(devMachine, runtimeSpec);
|
|
145
145
|
export default createPlaybookRuntime;
|
|
@@ -160,12 +160,6 @@ function composePlayerPrompt(input: PlayerInput, resuming = false): string {
|
|
|
160
160
|
return composePlayerContinuation(input, body, resuming);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
export const _internal = {
|
|
164
|
-
composePlayerPrompt,
|
|
165
|
-
VERBATIM_PAYLOAD_FIELDS,
|
|
166
|
-
UNFINISHED_FINAL_STATE_IDS,
|
|
167
|
-
};
|
|
168
|
-
|
|
169
163
|
const runtimeSpec = {
|
|
170
164
|
label: 'DEV',
|
|
171
165
|
// DR-022 / slc/link.md: the declaration carries the value current at link
|
|
@@ -224,7 +218,7 @@ const runtimeSpec = {
|
|
|
224
218
|
},
|
|
225
219
|
},
|
|
226
220
|
},
|
|
227
|
-
composePlayerPrompt: (input: PlaybookPlayerInput, _identity, resuming) =>
|
|
221
|
+
composePlayerPrompt: (input: PlaybookPlayerInput, _identity, resuming?: boolean) =>
|
|
228
222
|
composePlayerPrompt(input as PlayerInput, resuming),
|
|
229
223
|
verbatimPayloadFields: VERBATIM_PAYLOAD_FIELDS,
|
|
230
224
|
controlContextFields: [],
|
|
@@ -232,6 +226,12 @@ const runtimeSpec = {
|
|
|
232
226
|
transitionEventFields: ['developmentRequest', 'answer', 'questionId'],
|
|
233
227
|
} satisfies XStatePlaybookRuntimeSpecV3<DevPlaybookOptions>;
|
|
234
228
|
|
|
229
|
+
export const _internal = {
|
|
230
|
+
composePlayerPrompt: runtimeSpec.composePlayerPrompt,
|
|
231
|
+
VERBATIM_PAYLOAD_FIELDS,
|
|
232
|
+
UNFINISHED_FINAL_STATE_IDS,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
235
|
const createPlaybookRuntime: XStatePlaybookRuntimeFactory<
|
|
236
236
|
XStatePlaybookRuntimeConstruction<
|
|
237
237
|
DevPlaybookOptions,
|
package/reference/sdlc/pr.md
CHANGED
|
@@ -19,7 +19,9 @@ The caller supplies:
|
|
|
19
19
|
`pr` delivers a reviewed branch into the repository default branch through a GitHub pull request: it publishes the branch, waits for the pull request's checks, fixes red checks through playbook `code` no more than once, and merges.
|
|
20
20
|
It changes no files and owns no repository commit; the one fix it may request is owned by `code`.
|
|
21
21
|
The check waits, the fix publication, the merge, and the local update are mechanical steps: each runs one fixed command whose exit status alone decides its two outcomes, reads no conversation, and produces no prose.
|
|
22
|
-
`gh` infers the pull request from the checked-out branch, so
|
|
22
|
+
`gh` infers the pull request from the checked-out branch, which the run does not own: the nested `code` call suspends across Boss turns, so the checkout can change before the fix is published or the merge runs.
|
|
23
|
+
The two steps that act on the pull request itself therefore carry one runtime value — the pull request `pr` published — and refuse unless the checkout still infers exactly it.
|
|
24
|
+
That identity is reported text, so it binds as a single-quoted shell literal: the command compares it as data and never executes it as shell syntax.
|
|
23
25
|
|
|
24
26
|
When the caller gives its input, Captain shall relay the complete caller input in quotes (`>`) to Coder, along with the following instruction:
|
|
25
27
|
|
|
@@ -66,6 +68,7 @@ When the nested `code` call fails outside that authored result contract, `pr` sh
|
|
|
66
68
|
|
|
67
69
|
When `code` succeeds, Captain shall publish the fix to the pull request by running exactly the following command in the repository, with no other action and without reading its output:
|
|
68
70
|
|
|
71
|
+
> [ "$(gh pr view --json url --jq .url)" = '<pull-request-url>' ] || exit 1
|
|
69
72
|
> git push || exit 1
|
|
70
73
|
> n=0
|
|
71
74
|
> until [ "$(gh pr view --json headRefOid --jq .headRefOid 2>/dev/null)" = "$(git rev-parse HEAD)" ]; do
|
|
@@ -75,6 +78,7 @@ When `code` succeeds, Captain shall publish the fix to the pull request by runni
|
|
|
75
78
|
> done
|
|
76
79
|
|
|
77
80
|
The publication has exactly two outcomes decided by the exit status alone: fix published on status zero, once the pull request's head is the pushed commit, and fix not published otherwise.
|
|
81
|
+
The command pushes nothing until the checked-out branch infers the pull request `pr` published, so a checkout that changed while `code` ran publishes to no other branch.
|
|
78
82
|
Fix not published is an authored failure that leaves the pull request open.
|
|
79
83
|
|
|
80
84
|
When the fix is published, Captain shall wait for the pull request's checks again by running exactly the same command as the first wait in the repository, with no other action and without reading its output:
|
|
@@ -92,11 +96,20 @@ Checks still failing is an authored failure that leaves the pull request open; t
|
|
|
92
96
|
|
|
93
97
|
When the checks pass, before or after the one fix attempt, Captain shall merge the pull request by running exactly the following command in the repository, with no other action and without reading its output:
|
|
94
98
|
|
|
95
|
-
> gh pr
|
|
99
|
+
> pr=$(gh pr view --json url --jq .url) || exit 1
|
|
100
|
+
> [ "$pr" = '<pull-request-url>' ] || exit 1
|
|
101
|
+
> base=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name) || exit 1
|
|
102
|
+
> [ -n "$base" ] || exit 1
|
|
103
|
+
> [ "$(gh pr view "$pr" --json baseRefName --jq .baseRefName)" = "$base" ] || exit 1
|
|
104
|
+
> gh pr merge --merge --delete-branch --match-head-commit "$(git rev-parse HEAD)" || exit 1
|
|
105
|
+
> [ "$(gh pr view "$pr" --json state --jq .state)" = MERGED ] || exit 1
|
|
106
|
+
> [ "$(git branch --show-current)" = "$base" ]
|
|
96
107
|
|
|
97
|
-
The merge creates a merge commit on the repository default branch,
|
|
108
|
+
The merge creates a merge commit on the repository default branch, requests deletion of the remote and local branch, and checks out the local default branch; GitHub closes the linked issue on merge.
|
|
98
109
|
The merge has exactly two outcomes decided by the exit status alone: merged on status zero and merge refused otherwise.
|
|
99
|
-
|
|
110
|
+
The command requires the inferred pull request to be the one `pr` published and to target the repository default branch before the irreversible merge, and confirms that same pull request's merged state and the default-branch checkout after the merge command succeeds; a queued pull request is not a merged result.
|
|
111
|
+
Merge refused is an authored failure that leaves the pull request in the state GitHub reports: the checkout no longer infers the published pull request, the pull request targets another branch, GitHub refused the merge for a conflict, a branch protection, a forbidden merge method, or a head that moved, or the merge may have landed while its confirmation, the local switch to the default branch, or the branch deletion failed.
|
|
112
|
+
A refused merge does not establish that the pull request is unmerged, so `pr` reports it as an unconfirmed merge rather than as not merged, and no outcome claims a branch was deleted, because `gh` skips the remote deletion for a pull request from another repository or one already merged and exits zero anyway.
|
|
100
113
|
|
|
101
114
|
When the pull request is merged, Captain shall bring the local default branch to the merged head by running exactly the following command in the repository, with no other action and without reading its output:
|
|
102
115
|
|
|
@@ -113,11 +113,21 @@ export type PrPlaybookOutput = {
|
|
|
113
113
|
readonly childResult: CompletedChildResult;
|
|
114
114
|
} | {
|
|
115
115
|
readonly status: 'not-merged';
|
|
116
|
-
readonly reason: 'fix-not-published' | 'checks-failed'
|
|
116
|
+
readonly reason: 'fix-not-published' | 'checks-failed';
|
|
117
|
+
readonly pullRequest: string;
|
|
118
|
+
readonly pullRequestUrl: string;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The merge command exited nonzero. It ran, so whether GitHub merged the
|
|
122
|
+
* pull request is unknown — script output never enters context — and the
|
|
123
|
+
* result claims neither a merge nor its absence.
|
|
124
|
+
*/
|
|
125
|
+
| {
|
|
126
|
+
readonly status: 'merge-unconfirmed';
|
|
117
127
|
readonly pullRequest: string;
|
|
118
128
|
readonly pullRequestUrl: string;
|
|
119
129
|
};
|
|
120
|
-
export type PrCompletion = 'merged' | 'not-published' | 'fix-failed' | 'fix-not-published' | 'checks-failed' | 'merge-
|
|
130
|
+
export type PrCompletion = 'merged' | 'not-published' | 'fix-failed' | 'fix-not-published' | 'checks-failed' | 'merge-unconfirmed';
|
|
121
131
|
/** The machine reads no input: the caller input arrives on `START_PR`. */
|
|
122
132
|
export type PrInput = Readonly<Record<never, never>>;
|
|
123
133
|
export type PrContext = {
|
|
@@ -290,7 +300,11 @@ export declare const prMachine: import("xstate").StateMachine<PrContext, {
|
|
|
290
300
|
readonly childResult: CompletedChildResult;
|
|
291
301
|
} | {
|
|
292
302
|
readonly status: "not-merged";
|
|
293
|
-
readonly reason: "fix-not-published" | "checks-failed"
|
|
303
|
+
readonly reason: "fix-not-published" | "checks-failed";
|
|
304
|
+
readonly pullRequest: string;
|
|
305
|
+
readonly pullRequestUrl: string;
|
|
306
|
+
} | {
|
|
307
|
+
readonly status: "merge-unconfirmed";
|
|
294
308
|
readonly pullRequest: string;
|
|
295
309
|
readonly pullRequestUrl: string;
|
|
296
310
|
}, import("xstate").EventObject, import("xstate").MetaObject, {
|