@sublang/playbook 3.1.0 → 4.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 +26 -4
- package/docs/cli.md +62 -2
- package/package.json +5 -4
- package/reference/sdlc/captain.playbook/captain.playbook.js +21 -13
- package/reference/sdlc/captain.playbook/captain.playbook.ts +20 -12
- package/reference/sdlc/code.playbook/bin/adapter-sdk.js +247 -0
- package/reference/sdlc/code.playbook/bin/playbook.js +54 -9
- package/reference/sdlc/code.playbook/bin/run.js +97 -0
- package/reference/sdlc/code.playbook/playbook-captain.js +23 -2
- package/reference/sdlc/code.playbook/playbook-captain.ts +29 -3
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +7 -0
- package/src/xstate-playbook-runtime.d.ts +10 -0
- package/src/xstate-playbook-runtime.js +7 -2
- package/src/xstate-playbook-runtime.ts +12 -3
package/README.md
CHANGED
|
@@ -34,10 +34,30 @@ or `ANTHROPIC_API_KEY`, and signed-in
|
|
|
34
34
|
[Codex CLI](https://github.com/openai/codex) or `OPENAI_API_KEY`.
|
|
35
35
|
|
|
36
36
|
```sh
|
|
37
|
-
npm install -g @sublang/playbook
|
|
37
|
+
npm install -g @sublang/playbook @anthropic-ai/claude-agent-sdk @openai/codex-sdk
|
|
38
38
|
playbook
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
The agent SDKs are optional, so you install only the vendors your
|
|
42
|
+
config names — install one and the package stays around 14 MB rather
|
|
43
|
+
than pulling every stack. Name each SDK as its own top-level install
|
|
44
|
+
root, exactly as above: an SDK nested inside another package's subtree
|
|
45
|
+
is not reachable from the adapter that loads it. Which versions work
|
|
46
|
+
is cligent's knowledge and ships with it: if a runtime is missing — or
|
|
47
|
+
installed below the version cligent supports — `playbook` says so
|
|
48
|
+
before it launches anything, naming the installed and required
|
|
49
|
+
versions and printing cligent's pinned install command
|
|
50
|
+
([DR-026](specs/decisions/026-optional-adapter-sdks.md),
|
|
51
|
+
[DR-027](specs/decisions/027-runtime-compatibility-from-cligent.md)).
|
|
52
|
+
|
|
53
|
+
Upgrading from 3.1.0 or earlier? Use the same full line — npm removes
|
|
54
|
+
the SDK copies those releases bundled when it upgrades the package, so
|
|
55
|
+
upgrading `@sublang/playbook` alone leaves no agent SDK installed.
|
|
56
|
+
Running via `npx` instead? Name each SDK as a sibling package of the
|
|
57
|
+
same invocation (`npx -y -p @sublang/playbook -p <sdk> playbook`) — no
|
|
58
|
+
install command reaches npx's ephemeral tree; see
|
|
59
|
+
[docs/cli.md](docs/cli.md).
|
|
60
|
+
|
|
41
61
|
The first launch seeds a commented config at
|
|
42
62
|
`${XDG_CONFIG_HOME:-$HOME/.config}/playbook/playbook.config.yaml`,
|
|
43
63
|
composes a `tmux-play` config, checks the declared adapters, and opens
|
|
@@ -70,14 +90,16 @@ playbook run @sublang/playbook/code/registry "add a test for parseArgs" --json
|
|
|
70
90
|
- **[docs/embedding.md](docs/embedding.md)** — the six-port runtime
|
|
71
91
|
contract for hosts other than `tmux-play`.
|
|
72
92
|
|
|
73
|
-
> **Current release:**
|
|
93
|
+
> **Current release:** 4.0.0. The composed system — the compiled default
|
|
74
94
|
> Captain, CODE and DISCUSS, nested playbook calls, script actors and the
|
|
75
95
|
> GEARS optimize pass, the semver-stable six-port runtime contract, and
|
|
76
96
|
> non-interactive `playbook run` with parked-session resume — landed in
|
|
77
97
|
> 1.0.0. Since then, `playbook run` gained defaults in the user config,
|
|
78
98
|
> 3.0.0 replaced the top-level `profiles` map with inline agent settings
|
|
79
|
-
> (existing configs migrate themselves on the next launch),
|
|
80
|
-
> added the linked-artifact/engine compatibility check.
|
|
99
|
+
> (existing configs migrate themselves on the next launch), 3.1.0
|
|
100
|
+
> added the linked-artifact/engine compatibility check, and 4.0.0 made
|
|
101
|
+
> the agent SDKs optional — an install carries only the vendors you name
|
|
102
|
+
> — with which versions work now owned and published by cligent. See the
|
|
81
103
|
> [CHANGELOG](CHANGELOG.md).
|
|
82
104
|
|
|
83
105
|
## How it compiles
|
package/docs/cli.md
CHANGED
|
@@ -7,6 +7,51 @@
|
|
|
7
7
|
one-shot non-interactive `run`. Agent settings for both come from the
|
|
8
8
|
[config](configuration.md).
|
|
9
9
|
|
|
10
|
+
## Installing agent SDKs
|
|
11
|
+
|
|
12
|
+
Each adapter is backed by a vendor runtime that installing
|
|
13
|
+
`@sublang/playbook` never downloads for you, so no install carries an
|
|
14
|
+
agent stack you did not ask for. Which versions each adapter supports
|
|
15
|
+
is [cligent](https://github.com/sublang-ai/cligent)'s knowledge and
|
|
16
|
+
ships with it
|
|
17
|
+
([DR-027](../specs/decisions/027-runtime-compatibility-from-cligent.md));
|
|
18
|
+
the commands below install the latest, which cligent accepts from its
|
|
19
|
+
supported floor up. Install the SDKs your config names, each as its
|
|
20
|
+
own top-level install root:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install -g @sublang/playbook @anthropic-ai/claude-agent-sdk # claude
|
|
24
|
+
npm install -g @sublang/playbook @openai/codex-sdk # codex
|
|
25
|
+
npm install -g @sublang/playbook @opencode-ai/sdk opencode-ai # opencode (SDK + CLI)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The `gemini` adapter needs no SDK install — its transport ships inside
|
|
29
|
+
cligent — only the `gemini` CLI on `PATH`, at a version cligent
|
|
30
|
+
supports; the preflight gates it like the SDKs.
|
|
31
|
+
|
|
32
|
+
**Upgrading from ≤ 3.1.0:** run the same full line. The old releases
|
|
33
|
+
bundled the SDKs inside `@sublang/playbook`'s own tree, and npm
|
|
34
|
+
removes that bundled copy when it upgrades to a version that no
|
|
35
|
+
longer declares them — an in-place `npm install -g @sublang/playbook`
|
|
36
|
+
alone leaves no SDK behind.
|
|
37
|
+
|
|
38
|
+
The "own top-level root" part matters. The adapter that imports the SDK
|
|
39
|
+
lives at `@sublang/playbook/node_modules/@sublang/cligent/`, and Node
|
|
40
|
+
finds a bare specifier by walking *up* from there — which reaches the
|
|
41
|
+
install prefix's own `node_modules`, but never into a sibling package's
|
|
42
|
+
subtree. An SDK that landed inside some other package is invisible to
|
|
43
|
+
the adapter even though it is on disk
|
|
44
|
+
([DR-026](../specs/decisions/026-optional-adapter-sdks.md)).
|
|
45
|
+
|
|
46
|
+
Both surfaces check this before doing any work: a declared adapter
|
|
47
|
+
whose runtime is not loadable — or is installed below the version
|
|
48
|
+
cligent supports — blocks the launch and names the adapter. An absent
|
|
49
|
+
runtime is reported as not installed; a stale one with its installed
|
|
50
|
+
and required versions, never as absent. Either way the remedy printed
|
|
51
|
+
is cligent's pinned install, `npm install -g <package>@<version>`, so
|
|
52
|
+
following it cannot install a version the gate refuses again
|
|
53
|
+
([PBCLI-40](../specs/user/playbook-cli.md#pbcli-40)).
|
|
54
|
+
|
|
10
55
|
## Interactive
|
|
11
56
|
|
|
12
57
|
```sh
|
|
@@ -15,7 +60,22 @@ playbook --list # ids, slash commands, and intents; no launch
|
|
|
15
60
|
playbook --help # config path, auth pointers, agent-swap recipe
|
|
16
61
|
```
|
|
17
62
|
|
|
18
|
-
Without a global install, `npx
|
|
63
|
+
Without a global install, `npx` runs the same bin — but name each
|
|
64
|
+
agent SDK as a sibling package of the same invocation:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
npx -y -p @sublang/playbook -p @anthropic-ai/claude-agent-sdk playbook
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A bare `npx @sublang/playbook` cannot be repaired by any install
|
|
71
|
+
command: npx materializes the run in an ephemeral cache tree whose
|
|
72
|
+
ancestor walk touches no global prefix, so an SDK installed with
|
|
73
|
+
`npm install -g` is invisible to it. The preflight detects this case
|
|
74
|
+
and prints the multi-package re-run instead of an install line, naming
|
|
75
|
+
every SDK your config needs at cligent's pinned version — including any
|
|
76
|
+
already present, since each distinct package set is a distinct tree —
|
|
77
|
+
and replaying your original arguments, so the printed command works in
|
|
78
|
+
one hop.
|
|
19
79
|
|
|
20
80
|
The command resolves its config (seeding it on first run), composes a
|
|
21
81
|
`tmux-play` config, checks adapter readiness, and launches. It exits
|
|
@@ -85,7 +145,7 @@ the playbook needs a Boss reply
|
|
|
85
145
|
A compiled playbook module imports `xstate` and
|
|
86
146
|
`@sublang/playbook/xstate-runtime` from its own directory. When a
|
|
87
147
|
filesystem `<from>` cannot resolve them — typically under a global
|
|
88
|
-
|
|
148
|
+
install with no project-local packages —
|
|
89
149
|
`playbook run` provisions them automatically before loading: it creates
|
|
90
150
|
`node_modules/xstate` and `node_modules/@sublang/playbook` beside the
|
|
91
151
|
module as symlinks to the running host's own packages and prints one
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sublang/playbook",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Composable XState v5 playbook runtime with compiled Captain, CODE, and DISCUSS workflows driven by GEARS specs.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"reference/sdlc/code.playbook/bin/playbook.js",
|
|
69
69
|
"reference/sdlc/code.playbook/bin/run.js",
|
|
70
70
|
"reference/sdlc/code.playbook/bin/provision.js",
|
|
71
|
+
"reference/sdlc/code.playbook/bin/adapter-sdk.js",
|
|
71
72
|
"reference/sdlc/discuss.playbook/discuss.gears.md",
|
|
72
73
|
"reference/sdlc/discuss.playbook/discuss.fsm.ts",
|
|
73
74
|
"reference/sdlc/discuss.playbook/discuss.fsm.js",
|
|
@@ -129,15 +130,15 @@
|
|
|
129
130
|
"provenance": true
|
|
130
131
|
},
|
|
131
132
|
"dependencies": {
|
|
132
|
-
"@
|
|
133
|
-
"@openai/codex-sdk": "^0.139.0",
|
|
134
|
-
"@sublang/cligent": "^0.16.0",
|
|
133
|
+
"@sublang/cligent": "^0.18.0",
|
|
135
134
|
"@sublang/spex": "^0.3.0",
|
|
136
135
|
"p-queue": "^9.3.1",
|
|
137
136
|
"xstate": "^5.19.4",
|
|
138
137
|
"yaml": "^2.9.0"
|
|
139
138
|
},
|
|
140
139
|
"devDependencies": {
|
|
140
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.221",
|
|
141
|
+
"@openai/codex-sdk": "^0.146.0",
|
|
141
142
|
"@types/node": "^22.0.0",
|
|
142
143
|
"typescript": "^5.8.0",
|
|
143
144
|
"vitest": "^3.0.0"
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import PQueue from 'p-queue';
|
|
10
10
|
import { createActor, fromPromise } from 'xstate';
|
|
11
11
|
import { captainMachine, } from './captain.fsm.js';
|
|
12
|
-
import { assertJsonSafe, combineAbortSignals, createNestedPlaybookBridge, normalizeError, normalizePlaybookSnapshot, snapshotJsonValue, snapshotPlaybookSession, validateCaptainResult, waitForPlaybookQuiescence, } from '../../../src/xstate-runtime.js';
|
|
12
|
+
import { assertJsonSafe, combineAbortSignals, createNestedPlaybookBridge, defaultBuildCaptainJudgePrompt, normalizeError, normalizePlaybookSnapshot, snapshotJsonValue, snapshotPlaybookSession, validateCaptainResult, waitForPlaybookQuiescence, } from '../../../src/xstate-runtime.js';
|
|
13
13
|
const CAPTAIN_OPTIONS = {
|
|
14
14
|
visibility: 'visible',
|
|
15
15
|
resume: false,
|
|
@@ -207,19 +207,16 @@ function requiredOutputFields(description) {
|
|
|
207
207
|
return fields;
|
|
208
208
|
}
|
|
209
209
|
function makeJudgePrompt(input, visibleText) {
|
|
210
|
+
return defaultBuildCaptainJudgePrompt(input, visibleText);
|
|
211
|
+
}
|
|
212
|
+
// CAPPLAY-18: a structurally malformed adjudication reply gets exactly one
|
|
213
|
+
// corrective re-ask carrying the rejection reason and the restated shape.
|
|
214
|
+
function makeJudgeRetryPrompt(judgePrompt, rejection) {
|
|
210
215
|
return [
|
|
211
|
-
|
|
212
|
-
`State id: ${input.stateId}`,
|
|
213
|
-
`Source item: ${input.sourceItem}`,
|
|
214
|
-
'',
|
|
215
|
-
'Visible Captain output:',
|
|
216
|
-
visibleText,
|
|
216
|
+
judgePrompt,
|
|
217
217
|
'',
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
'',
|
|
221
|
-
'Return one JSON object with exactly one declared guard.',
|
|
222
|
-
'For direct Captain question or response guards, do not include question or response; the runtime injects the visible text.',
|
|
218
|
+
`Your previous control reply was rejected: ${normalizeError(rejection).message}.`,
|
|
219
|
+
'Reply again with exactly one JSON object naming one declared `guard` key and only its required structural fields, with no prose.',
|
|
223
220
|
].join('\n');
|
|
224
221
|
}
|
|
225
222
|
function adjudicateCaptainOutput(input, visibleText, judgeText) {
|
|
@@ -714,7 +711,18 @@ class CaptainPlaybookRuntime {
|
|
|
714
711
|
}
|
|
715
712
|
const judgePrompt = makeJudgePrompt(input, result.finalText);
|
|
716
713
|
const judgeText = await this.callJudge('captain-output-adjudication', judgePrompt, signal, input.stateId);
|
|
717
|
-
|
|
714
|
+
try {
|
|
715
|
+
return adjudicateCaptainOutput(input, result.finalText, judgeText);
|
|
716
|
+
}
|
|
717
|
+
catch (rejection) {
|
|
718
|
+
// One corrective re-ask on a malformed control reply (CAPPLAY-18);
|
|
719
|
+
// a judge transport failure above never reaches this catch.
|
|
720
|
+
if (signal.aborted)
|
|
721
|
+
throw signal.reason;
|
|
722
|
+
const retryPrompt = makeJudgeRetryPrompt(judgePrompt, rejection);
|
|
723
|
+
const retryText = await this.callJudge('captain-output-adjudication', retryPrompt, signal, input.stateId);
|
|
724
|
+
return adjudicateCaptainOutput(input, result.finalText, retryText);
|
|
725
|
+
}
|
|
718
726
|
}
|
|
719
727
|
catch (error) {
|
|
720
728
|
if (!signal.aborted)
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
assertJsonSafe,
|
|
37
37
|
combineAbortSignals,
|
|
38
38
|
createNestedPlaybookBridge,
|
|
39
|
+
defaultBuildCaptainJudgePrompt,
|
|
39
40
|
normalizeError,
|
|
40
41
|
normalizePlaybookSnapshot,
|
|
41
42
|
snapshotJsonValue,
|
|
@@ -291,19 +292,17 @@ function requiredOutputFields(description: string): readonly string[] {
|
|
|
291
292
|
}
|
|
292
293
|
|
|
293
294
|
function makeJudgePrompt(input: CaptainInput, visibleText: string): string {
|
|
295
|
+
return defaultBuildCaptainJudgePrompt(input, visibleText);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// CAPPLAY-18: a structurally malformed adjudication reply gets exactly one
|
|
299
|
+
// corrective re-ask carrying the rejection reason and the restated shape.
|
|
300
|
+
function makeJudgeRetryPrompt(judgePrompt: string, rejection: unknown): string {
|
|
294
301
|
return [
|
|
295
|
-
|
|
296
|
-
`State id: ${input.stateId}`,
|
|
297
|
-
`Source item: ${input.sourceItem}`,
|
|
298
|
-
'',
|
|
299
|
-
'Visible Captain output:',
|
|
300
|
-
visibleText,
|
|
301
|
-
'',
|
|
302
|
-
'Result keys and descriptions:',
|
|
303
|
-
...Object.entries(input.result).map(([key, description]) => `- ${key}: ${description}`),
|
|
302
|
+
judgePrompt,
|
|
304
303
|
'',
|
|
305
|
-
|
|
306
|
-
'
|
|
304
|
+
`Your previous control reply was rejected: ${normalizeError(rejection).message}.`,
|
|
305
|
+
'Reply again with exactly one JSON object naming one declared `guard` key and only its required structural fields, with no prose.',
|
|
307
306
|
].join('\n');
|
|
308
307
|
}
|
|
309
308
|
|
|
@@ -780,7 +779,16 @@ class CaptainPlaybookRuntime implements PlaybookRuntime {
|
|
|
780
779
|
}
|
|
781
780
|
const judgePrompt = makeJudgePrompt(input, result.finalText);
|
|
782
781
|
const judgeText = await this.callJudge('captain-output-adjudication', judgePrompt, signal, input.stateId);
|
|
783
|
-
|
|
782
|
+
try {
|
|
783
|
+
return adjudicateCaptainOutput(input, result.finalText, judgeText);
|
|
784
|
+
} catch (rejection) {
|
|
785
|
+
// One corrective re-ask on a malformed control reply (CAPPLAY-18);
|
|
786
|
+
// a judge transport failure above never reaches this catch.
|
|
787
|
+
if (signal.aborted) throw signal.reason;
|
|
788
|
+
const retryPrompt = makeJudgeRetryPrompt(judgePrompt, rejection);
|
|
789
|
+
const retryText = await this.callJudge('captain-output-adjudication', retryPrompt, signal, input.stateId);
|
|
790
|
+
return adjudicateCaptainOutput(input, result.finalText, retryText);
|
|
791
|
+
}
|
|
784
792
|
} catch (error) {
|
|
785
793
|
if (!signal.aborted) this.latchControlError(error);
|
|
786
794
|
throw error;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
|
+
|
|
4
|
+
// PBCLI-39/40 (DR-027): the agent runtimes are cligent's to know. This
|
|
5
|
+
// module keeps only cligent module-path knowledge — which subpath exports
|
|
6
|
+
// which adapter class — and derives every runtime identity, supported
|
|
7
|
+
// floor, and repair from cligent's shipped descriptor, so a cligent
|
|
8
|
+
// upgrade alone moves the compatibility policy. A runtime's absence or
|
|
9
|
+
// staleness has to be a named gate failure rather than a mid-turn adapter
|
|
10
|
+
// error, which is what this probe provides for both the interactive
|
|
11
|
+
// launcher and `run`.
|
|
12
|
+
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { sep } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
import { classifyRuntime } from '@sublang/cligent';
|
|
18
|
+
import { AGENT_RUNTIME_TARGETS } from '@sublang/cligent/runtime-targets';
|
|
19
|
+
|
|
20
|
+
// PBCLI-39: adapter shorthand -> the cligent module that constructs it.
|
|
21
|
+
// This is API-shape knowledge, not version knowledge; versions, floors,
|
|
22
|
+
// and repairs come from the descriptor. Adapters absent from cligent's
|
|
23
|
+
// descriptor are excluded from the gate and stay covered by PBCLI-12's
|
|
24
|
+
// unknown-adapter warning.
|
|
25
|
+
export const ADAPTER_MODULES = {
|
|
26
|
+
claude: {
|
|
27
|
+
module: '@sublang/cligent/adapters/claude-code',
|
|
28
|
+
export: 'ClaudeCodeAdapter',
|
|
29
|
+
},
|
|
30
|
+
codex: {
|
|
31
|
+
module: '@sublang/cligent/adapters/codex',
|
|
32
|
+
export: 'CodexAdapter',
|
|
33
|
+
},
|
|
34
|
+
// DR-027 ends gemini's exemption: its missing-SDK rationale was true but
|
|
35
|
+
// incomplete — the CLI can be absent or below cligent's floor, and the
|
|
36
|
+
// descriptor names both.
|
|
37
|
+
gemini: {
|
|
38
|
+
module: '@sublang/cligent/adapters/gemini',
|
|
39
|
+
export: 'GeminiAdapter',
|
|
40
|
+
},
|
|
41
|
+
kimi: {
|
|
42
|
+
module: '@sublang/cligent/adapters/kimi',
|
|
43
|
+
export: 'KimiAdapter',
|
|
44
|
+
},
|
|
45
|
+
opencode: {
|
|
46
|
+
module: '@sublang/cligent/adapters/opencode',
|
|
47
|
+
export: 'OpenCodeAdapter',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// PBCLI-39: probe through cligent's own adapter rather than by resolution.
|
|
52
|
+
// `isAvailable()` performs the same load the adapter performs at run time,
|
|
53
|
+
// from cligent's installed module scope — and since cligent enforces its
|
|
54
|
+
// version floors inside that same loader, a passing probe cannot disagree
|
|
55
|
+
// with a failing run, for absence and for staleness alike.
|
|
56
|
+
export async function probeAdapterSdk(adapter) {
|
|
57
|
+
const entry = ADAPTER_MODULES[adapter];
|
|
58
|
+
if (entry === undefined) return true;
|
|
59
|
+
try {
|
|
60
|
+
const AdapterClass = (await import(entry.module))[entry.export];
|
|
61
|
+
return await new AdapterClass().isAvailable();
|
|
62
|
+
} catch {
|
|
63
|
+
// An adapter module that cannot be imported at all is unavailable,
|
|
64
|
+
// not an internal error — the remedy is the same install line.
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The descriptor rows for one adapter shorthand, in declaration order.
|
|
70
|
+
function runtimeTargetsFor(adapter) {
|
|
71
|
+
return adapter in ADAPTER_MODULES
|
|
72
|
+
? (AGENT_RUNTIME_TARGETS[adapter] ?? [])
|
|
73
|
+
: [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// PBCLI-39: probe each distinct gated adapter at most once, and classify
|
|
77
|
+
// an unavailable adapter's runtimes through cligent's structured verdict.
|
|
78
|
+
// Only the runtimes at fault are reported: an `opencode` whose CLI is
|
|
79
|
+
// present and in range names the SDK alone, because the two halves have
|
|
80
|
+
// different repairs and naming a healthy one sends the user to install
|
|
81
|
+
// what is already there.
|
|
82
|
+
export async function checkAdapterSdks(
|
|
83
|
+
adapters,
|
|
84
|
+
probe = probeAdapterSdk,
|
|
85
|
+
classify = classifyRuntime,
|
|
86
|
+
) {
|
|
87
|
+
const known = [...new Set(adapters)].filter(
|
|
88
|
+
(a) => runtimeTargetsFor(a).length > 0,
|
|
89
|
+
);
|
|
90
|
+
const results = await Promise.all(known.map((a) => probe(a)));
|
|
91
|
+
const unusableAdapters = [];
|
|
92
|
+
known.forEach((adapter, i) => {
|
|
93
|
+
if (results[i]) return;
|
|
94
|
+
const verdicts = runtimeTargetsFor(adapter).map((target) =>
|
|
95
|
+
classify(target, false),
|
|
96
|
+
);
|
|
97
|
+
const unsupported = verdicts.filter((v) => v.state === 'unsupported');
|
|
98
|
+
const missing = verdicts.filter(
|
|
99
|
+
(v) => v.state === 'missing' && v.installed === undefined,
|
|
100
|
+
);
|
|
101
|
+
// When neither explains the failure (e.g. the cligent module itself
|
|
102
|
+
// failed to import), fall back to every runtime so the gate still
|
|
103
|
+
// blocks with a usable remedy.
|
|
104
|
+
const culprits =
|
|
105
|
+
unsupported.length > 0 || missing.length > 0
|
|
106
|
+
? [...unsupported, ...missing]
|
|
107
|
+
: verdicts;
|
|
108
|
+
unusableAdapters.push({ adapter, verdicts: culprits });
|
|
109
|
+
});
|
|
110
|
+
return { unusableAdapters };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// PBCLI-40: a run under `npx` / `npm exec` lives in npm's ephemeral cache
|
|
114
|
+
// tree. No `npm install` invocation reaches that tree — a global SDK install
|
|
115
|
+
// is not on its directory-ancestor walk — so the only honest remedy is to
|
|
116
|
+
// re-run with each SDK named as a sibling package of the same exec.
|
|
117
|
+
export function detectEphemeralNpxInstall(moduleUrl = import.meta.url) {
|
|
118
|
+
return fileURLToPath(moduleUrl).split(sep).includes('_npx');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// PBCLI-39: the pinned repair specifiers of every descriptor-backed peer
|
|
122
|
+
// SDK in a lineup, deduplicated in descriptor order. The ephemeral re-run
|
|
123
|
+
// must be built from this full set: a fresh exec tree starts empty, so a
|
|
124
|
+
// re-run named after only the currently missing SDKs drops the ones this
|
|
125
|
+
// tree does have and alternates between vendors forever. Pinned specs also
|
|
126
|
+
// mean the re-run installs versions the gate accepts.
|
|
127
|
+
export function mappedSdksFor(adapters) {
|
|
128
|
+
const distinct = new Set(adapters);
|
|
129
|
+
const specs = [];
|
|
130
|
+
for (const [adapter, targets] of Object.entries(AGENT_RUNTIME_TARGETS)) {
|
|
131
|
+
if (!distinct.has(adapter) || !(adapter in ADAPTER_MODULES)) continue;
|
|
132
|
+
for (const target of targets) {
|
|
133
|
+
if (target.kind === 'peer' && !specs.includes(target.repairSpec)) {
|
|
134
|
+
specs.push(target.repairSpec);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return specs;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Minimal POSIX quoting so a preserved argument survives copy-paste; matches
|
|
142
|
+
// cligent's shared shellQuote. tmux-play is POSIX-only, so no cmd.exe form.
|
|
143
|
+
export function shellQuote(value) {
|
|
144
|
+
if (/^[a-zA-Z0-9_./:=@-]+$/.test(value)) {
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// The running package's own spec, so the re-run reinstalls exactly this
|
|
151
|
+
// version rather than whatever dist-tag `npx` would resolve today.
|
|
152
|
+
function selfPackageSpec() {
|
|
153
|
+
try {
|
|
154
|
+
const manifest = JSON.parse(
|
|
155
|
+
readFileSync(new URL('../../../../package.json', import.meta.url), 'utf8'),
|
|
156
|
+
);
|
|
157
|
+
if (typeof manifest.name === 'string' && typeof manifest.version === 'string') {
|
|
158
|
+
return `${manifest.name}@${manifest.version}`;
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// Fall through to the unpinned name.
|
|
162
|
+
}
|
|
163
|
+
return '@sublang/playbook';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// One clause describing a runtime verdict. `unsupported` carries versions,
|
|
167
|
+
// because "not installed" for a runtime that is installed sends the user
|
|
168
|
+
// hunting for something already present (PBCLI-40).
|
|
169
|
+
function describeVerdict(verdict) {
|
|
170
|
+
const named = verdict.target.bundles ?? verdict.target.package;
|
|
171
|
+
if (verdict.state === 'unsupported') {
|
|
172
|
+
return `${named} ${verdict.installed} installed, >=${verdict.target.supportedFrom} required`;
|
|
173
|
+
}
|
|
174
|
+
return `${named} not installed`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// PBCLI-40: name every unusable adapter with its per-runtime verdicts and,
|
|
178
|
+
// for each runtime at fault, the exact pinned command that supplies it.
|
|
179
|
+
// External CLIs are found through PATH, which an exec tree inherits, so
|
|
180
|
+
// their global install lines hold in both cases.
|
|
181
|
+
//
|
|
182
|
+
// options.requiredSdks: the full mapped-spec set of the lineup (see
|
|
183
|
+
// mappedSdksFor) — the ephemeral re-run is built from it, not from the
|
|
184
|
+
// missing subset. options.invocation: the original CLI arguments, preserved
|
|
185
|
+
// on the re-run so the printed command is executable as printed.
|
|
186
|
+
export function adapterSdkFailureLines(unusableAdapters, options = {}) {
|
|
187
|
+
if (unusableAdapters.length === 0) return [];
|
|
188
|
+
const ephemeralNpx = options.ephemeralNpx ?? detectEphemeralNpxInstall();
|
|
189
|
+
const lines = [
|
|
190
|
+
`Adapter runtimes not usable: ${unusableAdapters
|
|
191
|
+
.map(
|
|
192
|
+
({ adapter, verdicts }) =>
|
|
193
|
+
`${adapter} (${verdicts.map(describeVerdict).join('; ')})`,
|
|
194
|
+
)
|
|
195
|
+
.join(', ')}`,
|
|
196
|
+
];
|
|
197
|
+
// External CLIs are found through PATH, which persists across exec trees
|
|
198
|
+
// and installed prefixes alike, so their pinned global installs are keyed
|
|
199
|
+
// to the runtimes actually at fault and hold in both branches. A CLI's
|
|
200
|
+
// one-time steps (e.g. a login) follow its install.
|
|
201
|
+
const cliInstalls = unusableAdapters.flatMap(({ verdicts }) =>
|
|
202
|
+
verdicts
|
|
203
|
+
.filter((v) => v.target.kind === 'cli')
|
|
204
|
+
.flatMap((v) => [`npm install -g ${v.repair.spec}`, ...v.repair.steps]),
|
|
205
|
+
);
|
|
206
|
+
const peerInstalls = unusableAdapters.flatMap(({ verdicts }) =>
|
|
207
|
+
verdicts
|
|
208
|
+
.filter((v) => v.target.kind === 'peer')
|
|
209
|
+
.map((v) => `npm install -g ${v.repair.spec}`),
|
|
210
|
+
);
|
|
211
|
+
if (ephemeralNpx) {
|
|
212
|
+
const sdks =
|
|
213
|
+
options.requiredSdks ??
|
|
214
|
+
unusableAdapters.flatMap(({ verdicts }) =>
|
|
215
|
+
verdicts
|
|
216
|
+
.filter((v) => v.target.kind === 'peer')
|
|
217
|
+
.map((v) => v.repair.spec),
|
|
218
|
+
);
|
|
219
|
+
const args = (options.invocation ?? [])
|
|
220
|
+
.map((arg) => ` ${shellQuote(arg)}`)
|
|
221
|
+
.join('');
|
|
222
|
+
lines.push(
|
|
223
|
+
' This npx / npm exec run is ephemeral: no npm install reaches its tree.',
|
|
224
|
+
);
|
|
225
|
+
if (cliInstalls.length > 0) {
|
|
226
|
+
// Prerequisites first: the re-run probes the CLI again, so following
|
|
227
|
+
// the output top-to-bottom must install it before re-running.
|
|
228
|
+
lines.push(
|
|
229
|
+
' First install the required CLI (it persists on PATH):',
|
|
230
|
+
...cliInstalls.map((command) => ` ${command}`),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
lines.push(
|
|
234
|
+
` ${cliInstalls.length > 0 ? 'Then re-run' : 'Re-run'} with every SDK your config needs named alongside the package:`,
|
|
235
|
+
` npx -y -p ${selfPackageSpec()}${sdks
|
|
236
|
+
.map((sdk) => ` -p ${sdk}`)
|
|
237
|
+
.join('')} playbook${args}`,
|
|
238
|
+
);
|
|
239
|
+
} else {
|
|
240
|
+
lines.push(
|
|
241
|
+
...peerInstalls.map((command) => ` ${command}`),
|
|
242
|
+
...cliInstalls.map((command) => ` ${command}`),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
lines.push('');
|
|
246
|
+
return lines;
|
|
247
|
+
}
|
|
@@ -22,6 +22,12 @@ import {
|
|
|
22
22
|
parseDocument as parseYamlDocument,
|
|
23
23
|
stringify as stringifyYaml,
|
|
24
24
|
} from 'yaml';
|
|
25
|
+
import {
|
|
26
|
+
adapterSdkFailureLines,
|
|
27
|
+
checkAdapterSdks,
|
|
28
|
+
mappedSdksFor,
|
|
29
|
+
probeAdapterSdk,
|
|
30
|
+
} from './adapter-sdk.js';
|
|
25
31
|
|
|
26
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
27
33
|
const templatePath = resolve(here, '..', 'playbook.config.template.yaml');
|
|
@@ -68,6 +74,16 @@ export async function runPlaybookCli(options = {}) {
|
|
|
68
74
|
...(options.readStdin ? { readStdin: options.readStdin } : {}),
|
|
69
75
|
...(options.sessionsDir ? { sessionsDir: options.sessionsDir } : {}),
|
|
70
76
|
...(options.hostRoots ? { hostRoots: options.hostRoots } : {}),
|
|
77
|
+
// PBCLI-39: the run path gates on SDK availability too.
|
|
78
|
+
...(options.probeAdapterSdk
|
|
79
|
+
? { probeAdapterSdk: options.probeAdapterSdk }
|
|
80
|
+
: {}),
|
|
81
|
+
...(options.classifyRuntime
|
|
82
|
+
? { classifyRuntime: options.classifyRuntime }
|
|
83
|
+
: {}),
|
|
84
|
+
...(options.ephemeralNpx !== undefined
|
|
85
|
+
? { ephemeralNpx: options.ephemeralNpx }
|
|
86
|
+
: {}),
|
|
71
87
|
});
|
|
72
88
|
}
|
|
73
89
|
|
|
@@ -144,19 +160,37 @@ export async function runPlaybookCli(options = {}) {
|
|
|
144
160
|
}
|
|
145
161
|
|
|
146
162
|
// PBCLI-12: readiness reads the adapters of the composed config.
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
163
|
+
const declaredAdapters = adaptersFromComposedConfig(composed.config);
|
|
164
|
+
const readiness = checkReadiness(declaredAdapters, env, home);
|
|
165
|
+
// PBCLI-39/40: SDK availability is an independent check with its own
|
|
166
|
+
// remedy — a credential and an SDK can be missing at once, and reporting
|
|
167
|
+
// only the first would send the user round the loop twice.
|
|
168
|
+
const { unusableAdapters } = await checkAdapterSdks(
|
|
169
|
+
declaredAdapters,
|
|
170
|
+
options.probeAdapterSdk ?? probeAdapterSdk,
|
|
171
|
+
...(options.classifyRuntime ? [options.classifyRuntime] : []),
|
|
151
172
|
);
|
|
152
173
|
for (const adapter of readiness.unknownAdapters) {
|
|
153
174
|
stderr.write(
|
|
154
175
|
`playbook: warning: no readiness check for adapter "${adapter}"\n`,
|
|
155
176
|
);
|
|
156
177
|
}
|
|
157
|
-
if (readiness.failingAdapters.length > 0) {
|
|
178
|
+
if (readiness.failingAdapters.length > 0 || unusableAdapters.length > 0) {
|
|
158
179
|
stderr.write(
|
|
159
|
-
helpText({
|
|
180
|
+
helpText({
|
|
181
|
+
userConfigPath,
|
|
182
|
+
failingAdapters: readiness.failingAdapters,
|
|
183
|
+
// PBCLI-40: the ephemeral re-run must carry the lineup's full mapped
|
|
184
|
+
// SDK set and the user's own arguments, so it completes in one hop
|
|
185
|
+
// and is executable exactly as printed.
|
|
186
|
+
sdkFailureLines: adapterSdkFailureLines(unusableAdapters, {
|
|
187
|
+
requiredSdks: mappedSdksFor(declaredAdapters),
|
|
188
|
+
invocation: argv,
|
|
189
|
+
...(options.ephemeralNpx !== undefined
|
|
190
|
+
? { ephemeralNpx: options.ephemeralNpx }
|
|
191
|
+
: {}),
|
|
192
|
+
}),
|
|
193
|
+
}),
|
|
160
194
|
);
|
|
161
195
|
return { code: READINESS_FAILURE_EXIT_CODE };
|
|
162
196
|
}
|
|
@@ -678,12 +712,19 @@ function hasExplicitConfig(argv) {
|
|
|
678
712
|
return argv.some((arg) => arg === '--config' || arg.startsWith('--config='));
|
|
679
713
|
}
|
|
680
714
|
|
|
681
|
-
function helpText({
|
|
715
|
+
function helpText({
|
|
716
|
+
userConfigPath,
|
|
717
|
+
failingAdapters = [],
|
|
718
|
+
sdkFailureLines = [],
|
|
719
|
+
}) {
|
|
682
720
|
const failures =
|
|
683
721
|
failingAdapters.length > 0
|
|
684
722
|
? [`Adapters not ready: ${failingAdapters.join(', ')}`, '']
|
|
685
723
|
: [];
|
|
686
724
|
return [
|
|
725
|
+
// PBCLI-40: the SDK remedy leads, because an unusable adapter cannot be
|
|
726
|
+
// fixed by the credential advice further down.
|
|
727
|
+
...sdkFailureLines,
|
|
687
728
|
...failures,
|
|
688
729
|
'Usage:',
|
|
689
730
|
' playbook [--list] [--with <path>]... [--config <path>] [tmux-play options]',
|
|
@@ -699,8 +740,12 @@ function helpText({ userConfigPath, failingAdapters = [] }) {
|
|
|
699
740
|
' default config file is never modified.',
|
|
700
741
|
'',
|
|
701
742
|
'Adapter setup:',
|
|
702
|
-
' claude:
|
|
703
|
-
'
|
|
743
|
+
' claude: npm install -g @anthropic-ai/claude-agent-sdk, then run',
|
|
744
|
+
' Claude Code once or set ANTHROPIC_API_KEY.',
|
|
745
|
+
' codex: npm install -g @openai/codex-sdk, then run Codex CLI once',
|
|
746
|
+
' or set OPENAI_API_KEY.',
|
|
747
|
+
' Each SDK is an optional peer dependency, so you install only the',
|
|
748
|
+
' vendors your config actually names.',
|
|
704
749
|
'',
|
|
705
750
|
'Agent swap recipe:',
|
|
706
751
|
' - set each agent inline: the top-level captain and every',
|
|
@@ -29,6 +29,12 @@ import {
|
|
|
29
29
|
} from '@sublang/cligent';
|
|
30
30
|
import { parse as parseYaml } from 'yaml';
|
|
31
31
|
import { hiddenControlEnvelope } from '../../../../src/xstate-runtime.js';
|
|
32
|
+
import {
|
|
33
|
+
adapterSdkFailureLines,
|
|
34
|
+
checkAdapterSdks,
|
|
35
|
+
mappedSdksFor,
|
|
36
|
+
probeAdapterSdk,
|
|
37
|
+
} from './adapter-sdk.js';
|
|
32
38
|
import { provisionEngine } from './provision.js';
|
|
33
39
|
|
|
34
40
|
// PBCLI-19: adapter shorthands the run host can construct.
|
|
@@ -77,6 +83,15 @@ export async function runPlaybookRun(options = {}) {
|
|
|
77
83
|
readStdin,
|
|
78
84
|
sessionsDir,
|
|
79
85
|
userConfigPath,
|
|
86
|
+
// PBCLI-39: the adapter SDK probe and the runtime classifier, injectable
|
|
87
|
+
// like createAgent so tests can drive an unavailable or below-floor
|
|
88
|
+
// runtime without uninstalling or downgrading one.
|
|
89
|
+
probeAdapterSdk: options.probeAdapterSdk ?? probeAdapterSdk,
|
|
90
|
+
classifyRuntime: options.classifyRuntime,
|
|
91
|
+
// PBCLI-40: the original invocation, preserved on the ephemeral re-run;
|
|
92
|
+
// this module receives argv with the leading `run` already consumed.
|
|
93
|
+
rawArgv: ['run', ...argv],
|
|
94
|
+
ephemeralNpx: options.ephemeralNpx,
|
|
80
95
|
// PBCLI-37: injected host package roots let tests provision against
|
|
81
96
|
// synthetic trees, like the injected session store.
|
|
82
97
|
hostRoots: options.hostRoots,
|
|
@@ -159,6 +174,18 @@ async function runFirst(args, ctx) {
|
|
|
159
174
|
return { code: EXIT.arg };
|
|
160
175
|
}
|
|
161
176
|
|
|
177
|
+
// PBCLI-39/40: an optional-peer SDK that is not installed fails here,
|
|
178
|
+
// before the runtime exists and before any agent call — never mid-turn.
|
|
179
|
+
const sdkError = await adapterSdksDiagnostic(
|
|
180
|
+
[...roleSpecs.values(), captainSpec],
|
|
181
|
+
ctx,
|
|
182
|
+
stdinReplayArgs(args, task),
|
|
183
|
+
);
|
|
184
|
+
if (sdkError !== undefined) {
|
|
185
|
+
stderr.write(sdkError);
|
|
186
|
+
return { code: EXIT.arg };
|
|
187
|
+
}
|
|
188
|
+
|
|
162
189
|
let runtime;
|
|
163
190
|
try {
|
|
164
191
|
runtime = entry.createRuntime({
|
|
@@ -290,6 +317,18 @@ async function runResume(args, ctx) {
|
|
|
290
317
|
return { code: EXIT.arg };
|
|
291
318
|
}
|
|
292
319
|
|
|
320
|
+
// PBCLI-39: a resume rebuilds the stored lineup, so it needs the same
|
|
321
|
+
// SDKs — an install that lost one must not resume into a mid-turn error.
|
|
322
|
+
const sdkError = await adapterSdksDiagnostic(
|
|
323
|
+
[...roleSpecs.values(), record.captain],
|
|
324
|
+
ctx,
|
|
325
|
+
stdinReplayArgs(args, reply),
|
|
326
|
+
);
|
|
327
|
+
if (sdkError !== undefined) {
|
|
328
|
+
stderr.write(sdkError);
|
|
329
|
+
return { code: EXIT.arg };
|
|
330
|
+
}
|
|
331
|
+
|
|
293
332
|
let runtime;
|
|
294
333
|
try {
|
|
295
334
|
runtime = entry.createRuntime({
|
|
@@ -639,6 +678,50 @@ function specsDiagnostic(specs) {
|
|
|
639
678
|
return undefined;
|
|
640
679
|
}
|
|
641
680
|
|
|
681
|
+
// PBCLI-40: the replay tail for input the command consumed from stdin —
|
|
682
|
+
// the pipe that carried it will not exist when the printed command runs.
|
|
683
|
+
// The value rides behind a `--` end-of-options terminator, because quoting
|
|
684
|
+
// alone cannot keep a flag-shaped value (`--json`, `--last`, a `-`-leading
|
|
685
|
+
// bullet) from being read as an option; where the original invocation
|
|
686
|
+
// already activated a terminator of its own, that one is reused — a second
|
|
687
|
+
// `--` after the first would itself be positional data on the replay,
|
|
688
|
+
// turning a `--json` task into `-- --json`.
|
|
689
|
+
function stdinReplayArgs(args, resolved) {
|
|
690
|
+
if (args.task !== undefined) return [];
|
|
691
|
+
return args.terminated ? [resolved] : ['--', resolved];
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// PBCLI-39/40: returns the ready-to-write stderr block naming every bound
|
|
695
|
+
// adapter whose optional-peer SDK is not installed, or undefined when every
|
|
696
|
+
// one of them loads. Runs only after specsDiagnostic has accepted the
|
|
697
|
+
// adapter names, so every spec here carries a known adapter.
|
|
698
|
+
async function adapterSdksDiagnostic(specs, ctx, stdinArgs = []) {
|
|
699
|
+
const adapters = specs.map((spec) => spec.adapter);
|
|
700
|
+
const { unusableAdapters } = await checkAdapterSdks(
|
|
701
|
+
adapters,
|
|
702
|
+
ctx.probeAdapterSdk,
|
|
703
|
+
...(ctx.classifyRuntime ? [ctx.classifyRuntime] : []),
|
|
704
|
+
);
|
|
705
|
+
if (unusableAdapters.length === 0) return undefined;
|
|
706
|
+
const [header, ...commands] = adapterSdkFailureLines(unusableAdapters, {
|
|
707
|
+
// PBCLI-40: the ephemeral re-run carries the lineup's full mapped SDK
|
|
708
|
+
// set and the original arguments, so it completes in one hop and runs
|
|
709
|
+
// exactly as printed. stdinArgs arrive terminator-ready from
|
|
710
|
+
// stdinReplayArgs — appended verbatim here, because whether a `--` is
|
|
711
|
+
// needed depends on the original invocation's own parse state.
|
|
712
|
+
requiredSdks: mappedSdksFor(adapters),
|
|
713
|
+
invocation: [...ctx.rawArgv, ...stdinArgs],
|
|
714
|
+
...(ctx.ephemeralNpx !== undefined
|
|
715
|
+
? { ephemeralNpx: ctx.ephemeralNpx }
|
|
716
|
+
: {}),
|
|
717
|
+
}).filter((line) => line !== '');
|
|
718
|
+
// Only the header takes the command prefix; the install lines stay
|
|
719
|
+
// copy-pasteable.
|
|
720
|
+
return [`playbook run: ${header}`, ...commands]
|
|
721
|
+
.map((line) => `${line}\n`)
|
|
722
|
+
.join('');
|
|
723
|
+
}
|
|
724
|
+
|
|
642
725
|
function isAgentSpec(spec) {
|
|
643
726
|
return (
|
|
644
727
|
typeof spec === 'object' &&
|
|
@@ -892,10 +975,23 @@ export function parseRunArgs(argv) {
|
|
|
892
975
|
verbose: false,
|
|
893
976
|
noProvision: false,
|
|
894
977
|
help: false,
|
|
978
|
+
terminated: false,
|
|
895
979
|
};
|
|
896
980
|
const positionals = [];
|
|
897
981
|
for (let i = 0; i < argv.length; i += 1) {
|
|
898
982
|
const arg = argv[i];
|
|
983
|
+
// PBCLI-40: end-of-options — everything after `--` is positional, so a
|
|
984
|
+
// flag-shaped task or reply (a stdin-derived `--json`, a `- bullet`
|
|
985
|
+
// line) survives the ephemeral re-run round trip instead of being
|
|
986
|
+
// reinterpreted as an option. `terminated` records that this branch
|
|
987
|
+
// fired — only a `--` the walk itself treats as the terminator counts,
|
|
988
|
+
// never one consumed as an option's value (`--cwd --`) — so the re-run
|
|
989
|
+
// builder can reuse an active terminator instead of doubling it.
|
|
990
|
+
if (arg === '--') {
|
|
991
|
+
args.terminated = true;
|
|
992
|
+
positionals.push(...argv.slice(i + 1));
|
|
993
|
+
break;
|
|
994
|
+
}
|
|
899
995
|
if (arg === '--help' || arg === '-h') args.help = true;
|
|
900
996
|
else if (arg === '--json') args.json = true;
|
|
901
997
|
else if (arg === '--verbose') args.verbose = true;
|
|
@@ -1036,6 +1132,7 @@ function runHelpText() {
|
|
|
1036
1132
|
' <from> registry module specifier (package subpath, path, or file: URL)',
|
|
1037
1133
|
' [task] Boss intent; read from stdin when omitted',
|
|
1038
1134
|
' [reply] Boss reply to a parked session; read from stdin when omitted',
|
|
1135
|
+
' -- end of options; use before a task or reply that starts with -',
|
|
1039
1136
|
'',
|
|
1040
1137
|
'Options:',
|
|
1041
1138
|
' --player <role>=<agent> bind a required role (repeatable)',
|
|
@@ -919,9 +919,30 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
919
919
|
if (visibilityControlError !== undefined)
|
|
920
920
|
throw visibilityControlError;
|
|
921
921
|
}
|
|
922
|
+
// CAPTAIN-34/35: a parentless internal Captain frame holds no recoverable
|
|
923
|
+
// work, so any rejected boundary call disposes the stack instead of
|
|
924
|
+
// stranding a frame that would refuse every later registered command. A
|
|
925
|
+
// parentless external root keeps its frame for Boss recovery.
|
|
926
|
+
async function failParentlessBoundary(frame, error) {
|
|
927
|
+
if (!frame.internal || !frames.includes(frame))
|
|
928
|
+
throw error;
|
|
929
|
+
if (!disposing) {
|
|
930
|
+
try {
|
|
931
|
+
await disposeStack('failure');
|
|
932
|
+
}
|
|
933
|
+
catch {
|
|
934
|
+
// The boundary failure wins; disposal detail stays on telemetry.
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
const commands = [...enablementById.values()]
|
|
938
|
+
.map((enablement) => `/${enablement.command} <task>`)
|
|
939
|
+
.join(' or ');
|
|
940
|
+
throw new Error('Captain could not finish that turn and the engagement was reset. ' +
|
|
941
|
+
`Send the request again${commands ? `, or start a playbook directly with ${commands}` : ''}.`, { cause: error });
|
|
942
|
+
}
|
|
922
943
|
async function returnBoundaryFailure(frame, error, context) {
|
|
923
944
|
if (!frame.parent)
|
|
924
|
-
|
|
945
|
+
await failParentlessBoundary(frame, error);
|
|
925
946
|
await resumeParent(frame, {
|
|
926
947
|
status: context.signal.aborted ? 'aborted' : 'error',
|
|
927
948
|
playbookId: frame.entry.id,
|
|
@@ -1126,7 +1147,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1126
1147
|
completed = true;
|
|
1127
1148
|
}
|
|
1128
1149
|
else {
|
|
1129
|
-
|
|
1150
|
+
await failParentlessBoundary(frame, error);
|
|
1130
1151
|
}
|
|
1131
1152
|
}
|
|
1132
1153
|
finally {
|
|
@@ -103,7 +103,7 @@ class VisibilityControlError extends Error {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
type DisposalReason = 'dismiss' | 'final' | 'dispose';
|
|
106
|
+
type DisposalReason = 'dismiss' | 'final' | 'dispose' | 'failure';
|
|
107
107
|
|
|
108
108
|
interface ControlLedger {
|
|
109
109
|
activePlaybookId?: string;
|
|
@@ -1275,12 +1275,38 @@ export function createPlaybookCaptainShell(
|
|
|
1275
1275
|
if (visibilityControlError !== undefined) throw visibilityControlError;
|
|
1276
1276
|
}
|
|
1277
1277
|
|
|
1278
|
+
// CAPTAIN-34/35: a parentless internal Captain frame holds no recoverable
|
|
1279
|
+
// work, so any rejected boundary call disposes the stack instead of
|
|
1280
|
+
// stranding a frame that would refuse every later registered command. A
|
|
1281
|
+
// parentless external root keeps its frame for Boss recovery.
|
|
1282
|
+
async function failParentlessBoundary(
|
|
1283
|
+
frame: EngagementFrame,
|
|
1284
|
+
error: unknown,
|
|
1285
|
+
): Promise<never> {
|
|
1286
|
+
if (!frame.internal || !frames.includes(frame)) throw error;
|
|
1287
|
+
if (!disposing) {
|
|
1288
|
+
try {
|
|
1289
|
+
await disposeStack('failure');
|
|
1290
|
+
} catch {
|
|
1291
|
+
// The boundary failure wins; disposal detail stays on telemetry.
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
const commands = [...enablementById.values()]
|
|
1295
|
+
.map((enablement) => `/${enablement.command} <task>`)
|
|
1296
|
+
.join(' or ');
|
|
1297
|
+
throw new Error(
|
|
1298
|
+
'Captain could not finish that turn and the engagement was reset. ' +
|
|
1299
|
+
`Send the request again${commands ? `, or start a playbook directly with ${commands}` : ''}.`,
|
|
1300
|
+
{ cause: error },
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1278
1304
|
async function returnBoundaryFailure(
|
|
1279
1305
|
frame: EngagementFrame,
|
|
1280
1306
|
error: unknown,
|
|
1281
1307
|
context: CaptainContext,
|
|
1282
1308
|
): Promise<void> {
|
|
1283
|
-
if (!frame.parent)
|
|
1309
|
+
if (!frame.parent) await failParentlessBoundary(frame, error);
|
|
1284
1310
|
await resumeParent(
|
|
1285
1311
|
frame,
|
|
1286
1312
|
{
|
|
@@ -1524,7 +1550,7 @@ export function createPlaybookCaptainShell(
|
|
|
1524
1550
|
await returnBoundaryFailure(frame, error, context);
|
|
1525
1551
|
completed = true;
|
|
1526
1552
|
} else {
|
|
1527
|
-
|
|
1553
|
+
await failParentlessBoundary(frame, error);
|
|
1528
1554
|
}
|
|
1529
1555
|
} finally {
|
|
1530
1556
|
activeTurnSummary = undefined;
|
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
# settings inline: an adapter shorthand (claude, codex) or a block with
|
|
11
11
|
# adapter/model/effort/permissions. Retuning one agent never changes
|
|
12
12
|
# another.
|
|
13
|
+
|
|
14
|
+
# Each adapter needs its vendor SDK installed as its own top-level
|
|
15
|
+
# install root — they are optional peer dependencies, so you pay only
|
|
16
|
+
# for the vendors named below:
|
|
17
|
+
# claude -> npm install -g @anthropic-ai/claude-agent-sdk
|
|
18
|
+
# codex -> npm install -g @openai/codex-sdk
|
|
19
|
+
# Drop an adapter from this file and you can skip its SDK entirely.
|
|
13
20
|
# Every seeded agent runs in cligent's protected auto mode
|
|
14
21
|
# (permissions.mode: auto): claude maps it to permissionMode auto, codex to
|
|
15
22
|
# on-request + auto_review. Codex roles also grant writablePaths: ['.git']
|
|
@@ -203,6 +203,16 @@ export interface PlayerBridgeSpec {
|
|
|
203
203
|
resumableStateIds: ReadonlySet<string>;
|
|
204
204
|
}
|
|
205
205
|
export declare function createPlayerBridge(spec: PlayerBridgeSpec, ports: PlaybookPorts, getActiveSignal?: () => AbortSignal | undefined, boundary?: RuntimeBoundaryCalls, onControlPlaneError?: (error: unknown) => void): PromiseActorLogic<PlaybookActorOutput, PlaybookPlayerInput>;
|
|
206
|
+
/**
|
|
207
|
+
* Default direct-Captain adjudicator prompt (DR-025). The single statement of
|
|
208
|
+
* the `{ guard, …structuralPayloadFields }` reply contract, shared with the
|
|
209
|
+
* compiled default Captain artifact so the wording cannot drift.
|
|
210
|
+
*/
|
|
211
|
+
export declare function defaultBuildCaptainJudgePrompt(input: {
|
|
212
|
+
readonly stateId: string;
|
|
213
|
+
readonly sourceItem: string;
|
|
214
|
+
readonly result: Readonly<Record<string, string>>;
|
|
215
|
+
}, finalText: string): string;
|
|
206
216
|
/** Targets of the FSM's `awaitBossReply` BOSS_REPLY transitions. */
|
|
207
217
|
export declare function resumableStateIdsFromMachine(machine: AnyStateMachine): ReadonlySet<string>;
|
|
208
218
|
/**
|
|
@@ -454,7 +454,12 @@ export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onCon
|
|
|
454
454
|
// `response` and rejects a judge reply that supplies either presentation
|
|
455
455
|
// field as an undeclared extra key.
|
|
456
456
|
// ---------------------------------------------------------------------------
|
|
457
|
-
|
|
457
|
+
/**
|
|
458
|
+
* Default direct-Captain adjudicator prompt (DR-025). The single statement of
|
|
459
|
+
* the `{ guard, …structuralPayloadFields }` reply contract, shared with the
|
|
460
|
+
* compiled default Captain artifact so the wording cannot drift.
|
|
461
|
+
*/
|
|
462
|
+
export function defaultBuildCaptainJudgePrompt(input, finalText) {
|
|
458
463
|
const lines = [];
|
|
459
464
|
lines.push('Adjudicate the direct Captain output for this FSM state.');
|
|
460
465
|
lines.push(`State id: ${input.stateId}`);
|
|
@@ -1382,7 +1387,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1382
1387
|
if (result.status !== 'ok' || !result.finalText) {
|
|
1383
1388
|
throw new Error('captainActor: boundary returned an unvalidated Captain result');
|
|
1384
1389
|
}
|
|
1385
|
-
const judgePrompt =
|
|
1390
|
+
const judgePrompt = defaultBuildCaptainJudgePrompt(input, result.finalText);
|
|
1386
1391
|
const raw = await boundary.callJudge('captain-output-adjudication', input.stateId, judgePrompt, active);
|
|
1387
1392
|
const output = adjudicateCaptainOutput(extractFields, input, result.finalText, raw);
|
|
1388
1393
|
validateBossReplyOutput(input, output, resumableStateIds);
|
|
@@ -799,8 +799,17 @@ export function createPlayerBridge(
|
|
|
799
799
|
// field as an undeclared extra key.
|
|
800
800
|
// ---------------------------------------------------------------------------
|
|
801
801
|
|
|
802
|
-
|
|
803
|
-
|
|
802
|
+
/**
|
|
803
|
+
* Default direct-Captain adjudicator prompt (DR-025). The single statement of
|
|
804
|
+
* the `{ guard, …structuralPayloadFields }` reply contract, shared with the
|
|
805
|
+
* compiled default Captain artifact so the wording cannot drift.
|
|
806
|
+
*/
|
|
807
|
+
export function defaultBuildCaptainJudgePrompt(
|
|
808
|
+
input: {
|
|
809
|
+
readonly stateId: string;
|
|
810
|
+
readonly sourceItem: string;
|
|
811
|
+
readonly result: Readonly<Record<string, string>>;
|
|
812
|
+
},
|
|
804
813
|
finalText: string,
|
|
805
814
|
): string {
|
|
806
815
|
const lines: string[] = [];
|
|
@@ -2042,7 +2051,7 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
2042
2051
|
'captainActor: boundary returned an unvalidated Captain result',
|
|
2043
2052
|
);
|
|
2044
2053
|
}
|
|
2045
|
-
const judgePrompt =
|
|
2054
|
+
const judgePrompt = defaultBuildCaptainJudgePrompt(
|
|
2046
2055
|
input,
|
|
2047
2056
|
result.finalText,
|
|
2048
2057
|
);
|