@sublang/playbook 3.0.0 → 3.1.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 -4
- package/docs/cli.md +21 -0
- package/package.json +2 -1
- package/reference/sdlc/code.playbook/bin/playbook.js +1 -0
- package/reference/sdlc/code.playbook/bin/provision.js +228 -0
- package/reference/sdlc/code.playbook/bin/run.js +49 -1
- package/slc/link.md +14 -0
- package/src/xstate-playbook-runtime.d.ts +17 -0
- package/src/xstate-playbook-runtime.js +45 -0
- package/src/xstate-playbook-runtime.ts +75 -4
package/README.md
CHANGED
|
@@ -70,14 +70,15 @@ playbook run @sublang/playbook/code/registry "add a test for parseArgs" --json
|
|
|
70
70
|
- **[docs/embedding.md](docs/embedding.md)** — the six-port runtime
|
|
71
71
|
contract for hosts other than `tmux-play`.
|
|
72
72
|
|
|
73
|
-
> **Current release:** 3.
|
|
73
|
+
> **Current release:** 3.1.0. The composed system — the compiled default
|
|
74
74
|
> Captain, CODE and DISCUSS, nested playbook calls, script actors and the
|
|
75
75
|
> GEARS optimize pass, the semver-stable six-port runtime contract, and
|
|
76
76
|
> non-interactive `playbook run` with parked-session resume — landed in
|
|
77
77
|
> 1.0.0. Since then, `playbook run` gained defaults in the user config,
|
|
78
|
-
>
|
|
79
|
-
>
|
|
80
|
-
>
|
|
78
|
+
> 3.0.0 replaced the top-level `profiles` map with inline agent settings
|
|
79
|
+
> (existing configs migrate themselves on the next launch), and 3.1.0
|
|
80
|
+
> added the linked-artifact/engine compatibility check. See the
|
|
81
|
+
> [CHANGELOG](CHANGELOG.md).
|
|
81
82
|
|
|
82
83
|
## How it compiles
|
|
83
84
|
|
package/docs/cli.md
CHANGED
|
@@ -67,6 +67,7 @@ intents the same way you would to `claude -p` or `codex exec`.
|
|
|
67
67
|
| `--option <key>=<value>` | a playbook option (CODE's `committer`) |
|
|
68
68
|
| `--cwd <dir>` | the agents' working directory |
|
|
69
69
|
| `--json` | one envelope: `outcome`, `sessionId`, output or questions |
|
|
70
|
+
| `--no-provision` | never create engine links beside a filesystem `<from>` |
|
|
70
71
|
|
|
71
72
|
`<agent>` is `<adapter>[:<model>][@<effort>]` — `codex:gpt-5.5@xhigh`,
|
|
72
73
|
or `claude@high` for the default model at high reasoning effort. The
|
|
@@ -79,6 +80,26 @@ Exit codes: `0` terminal, `1` bad argument or module, `2` failure, `3`
|
|
|
79
80
|
the playbook needs a Boss reply
|
|
80
81
|
([PBCLI-18](../specs/user/playbook-cli.md#pbcli-18)).
|
|
81
82
|
|
|
83
|
+
### Engine provisioning
|
|
84
|
+
|
|
85
|
+
A compiled playbook module imports `xstate` and
|
|
86
|
+
`@sublang/playbook/xstate-runtime` from its own directory. When a
|
|
87
|
+
filesystem `<from>` cannot resolve them — typically under a global
|
|
88
|
+
`npm install -g @sublang/playbook` with no project-local packages —
|
|
89
|
+
`playbook run` provisions them automatically before loading: it creates
|
|
90
|
+
`node_modules/xstate` and `node_modules/@sublang/playbook` beside the
|
|
91
|
+
module as symlinks to the running host's own packages and prints one
|
|
92
|
+
line naming what it linked
|
|
93
|
+
([PBCLI-36](../specs/user/playbook-cli.md#pbcli-36),
|
|
94
|
+
[DR-024](../specs/decisions/024-runtime-engine-provisioning.md)).
|
|
95
|
+
A directory where the imports already resolve is never touched — a
|
|
96
|
+
project-local install always wins — and `--no-provision` disables the
|
|
97
|
+
mechanism entirely.
|
|
98
|
+
|
|
99
|
+
If the module's directory is a git repository, add `node_modules/` to
|
|
100
|
+
its `.gitignore` so the provisioned links never land in commits made by
|
|
101
|
+
player agents working there.
|
|
102
|
+
|
|
82
103
|
### Resuming a parked run
|
|
83
104
|
|
|
84
105
|
When the playbook stops to ask something, the run is parked, not lost:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sublang/playbook",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.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",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"reference/sdlc/code.playbook/playbook.config.template.yaml",
|
|
68
68
|
"reference/sdlc/code.playbook/bin/playbook.js",
|
|
69
69
|
"reference/sdlc/code.playbook/bin/run.js",
|
|
70
|
+
"reference/sdlc/code.playbook/bin/provision.js",
|
|
70
71
|
"reference/sdlc/discuss.playbook/discuss.gears.md",
|
|
71
72
|
"reference/sdlc/discuss.playbook/discuss.fsm.ts",
|
|
72
73
|
"reference/sdlc/discuss.playbook/discuss.fsm.js",
|
|
@@ -67,6 +67,7 @@ export async function runPlaybookCli(options = {}) {
|
|
|
67
67
|
...(options.createAgent ? { createAgent: options.createAgent } : {}),
|
|
68
68
|
...(options.readStdin ? { readStdin: options.readStdin } : {}),
|
|
69
69
|
...(options.sessionsDir ? { sessionsDir: options.sessionsDir } : {}),
|
|
70
|
+
...(options.hostRoots ? { hostRoots: options.hostRoots } : {}),
|
|
70
71
|
});
|
|
71
72
|
}
|
|
72
73
|
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
|
+
|
|
4
|
+
// PBCLI-36/37 (DR-024): probe-first engine provisioning for filesystem
|
|
5
|
+
// registry modules. A compiled thin artifact imports `xstate` and
|
|
6
|
+
// `@sublang/playbook/xstate-runtime`, which Node resolves by walking up
|
|
7
|
+
// from the artifact's own directory; a globally installed host therefore
|
|
8
|
+
// fails at artifact load in a bare directory. Before importing such a
|
|
9
|
+
// module, `playbook run` probes both specifiers with the module's path as
|
|
10
|
+
// resolution parent and, only when a probe fails, symlinks the running
|
|
11
|
+
// host's own installed package roots beside the module. It never shells
|
|
12
|
+
// out to `npm link` and never installs from the registry.
|
|
13
|
+
|
|
14
|
+
import { existsSync, lstatSync, readFileSync, readlinkSync } from 'node:fs';
|
|
15
|
+
import { mkdir, symlink, unlink } from 'node:fs/promises';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
// PBCLI-37: probe specifier → the package name provisioning may link.
|
|
21
|
+
const ENGINE_LINKS = new Map([
|
|
22
|
+
['xstate', 'xstate'],
|
|
23
|
+
['@sublang/playbook/xstate-runtime', '@sublang/playbook'],
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const DEPENDENCY_FIELDS = [
|
|
27
|
+
'dependencies',
|
|
28
|
+
'devDependencies',
|
|
29
|
+
'peerDependencies',
|
|
30
|
+
'optionalDependencies',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// PBCLI-37: package names whose probes fail with the module as resolution
|
|
34
|
+
// parent. `createRequire` follows the same walk-up Node uses for the
|
|
35
|
+
// module's own imports, so an empty result means a project-local (or
|
|
36
|
+
// already provisioned) engine wins and provisioning must touch nothing.
|
|
37
|
+
function missingEngineLinks(modulePath) {
|
|
38
|
+
const req = createRequire(modulePath);
|
|
39
|
+
const missing = [];
|
|
40
|
+
for (const [specifier, name] of ENGINE_LINKS) {
|
|
41
|
+
try {
|
|
42
|
+
req.resolve(specifier);
|
|
43
|
+
} catch {
|
|
44
|
+
missing.push(name);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return missing;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// PBCLI-36: a manifest at or above the module declaring @sublang/playbook
|
|
51
|
+
// means a project chose a dependency and its install is broken or absent;
|
|
52
|
+
// shadow-provisioning would mask the real fix.
|
|
53
|
+
function declaringManifest(startDir) {
|
|
54
|
+
for (let dir = startDir; ; ) {
|
|
55
|
+
const manifestPath = join(dir, 'package.json');
|
|
56
|
+
if (existsSync(manifestPath)) {
|
|
57
|
+
try {
|
|
58
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
59
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
60
|
+
const block = manifest?.[field];
|
|
61
|
+
if (
|
|
62
|
+
block !== null &&
|
|
63
|
+
typeof block === 'object' &&
|
|
64
|
+
Object.prototype.hasOwnProperty.call(block, '@sublang/playbook')
|
|
65
|
+
) {
|
|
66
|
+
return manifestPath;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// An unreadable manifest cannot declare the dependency.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const parent = dirname(dir);
|
|
74
|
+
if (parent === dir) return undefined;
|
|
75
|
+
dir = parent;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function packageRootUpward(startDir, name) {
|
|
80
|
+
for (let dir = startDir; ; ) {
|
|
81
|
+
const manifestPath = join(dir, 'package.json');
|
|
82
|
+
if (existsSync(manifestPath)) {
|
|
83
|
+
try {
|
|
84
|
+
if (JSON.parse(readFileSync(manifestPath, 'utf8')).name === name) {
|
|
85
|
+
return dir;
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// Keep walking past an unreadable manifest.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const parent = dirname(dir);
|
|
92
|
+
if (parent === dir) return undefined;
|
|
93
|
+
dir = parent;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// PBCLI-37: the running host's own installed package roots, resolved from
|
|
98
|
+
// the host's module scope — this file lives inside @sublang/playbook, and
|
|
99
|
+
// xstate resolves from that root's own dependency tree.
|
|
100
|
+
function defaultHostRoots() {
|
|
101
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
102
|
+
const playbookRoot = packageRootUpward(here, '@sublang/playbook');
|
|
103
|
+
if (playbookRoot === undefined) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
'cannot locate the running @sublang/playbook package root for provisioning',
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const req = createRequire(join(playbookRoot, 'package.json'));
|
|
109
|
+
let xstateRoot;
|
|
110
|
+
try {
|
|
111
|
+
xstateRoot = packageRootUpward(dirname(req.resolve('xstate')), 'xstate');
|
|
112
|
+
} catch {
|
|
113
|
+
xstateRoot = undefined;
|
|
114
|
+
}
|
|
115
|
+
if (xstateRoot === undefined) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
"cannot locate the running host's own xstate package for provisioning",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return { xstate: xstateRoot, '@sublang/playbook': playbookRoot };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 'absent' | 'dangling' | 'live' (a symlink with an existing target) |
|
|
124
|
+
// 'occupied' (a real file or directory, never removed).
|
|
125
|
+
function linkState(linkPath) {
|
|
126
|
+
let stat;
|
|
127
|
+
try {
|
|
128
|
+
stat = lstatSync(linkPath);
|
|
129
|
+
} catch {
|
|
130
|
+
return 'absent';
|
|
131
|
+
}
|
|
132
|
+
if (!stat.isSymbolicLink()) return 'occupied';
|
|
133
|
+
return existsSync(linkPath) ? 'live' : 'dangling';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// PBCLI-36/37: probe, then provision the missing engine links beside a
|
|
137
|
+
// filesystem registry module. Returns {} when the run may proceed (either
|
|
138
|
+
// nothing was needed or links were created and logged) or { code: 1 }
|
|
139
|
+
// after writing one `playbook run: <message>` diagnostic to stderr.
|
|
140
|
+
export async function provisionEngine({
|
|
141
|
+
modulePath,
|
|
142
|
+
stderr,
|
|
143
|
+
enabled = true,
|
|
144
|
+
hostRoots,
|
|
145
|
+
}) {
|
|
146
|
+
const missing = missingEngineLinks(modulePath);
|
|
147
|
+
if (missing.length === 0) return {};
|
|
148
|
+
|
|
149
|
+
const moduleDir = dirname(modulePath);
|
|
150
|
+
if (!enabled) {
|
|
151
|
+
// PBCLI-36: --no-provision still owes the dangling-link diagnostic —
|
|
152
|
+
// a stale link we (or a prior host) created must not surface as a raw
|
|
153
|
+
// module-not-found error.
|
|
154
|
+
for (const name of missing) {
|
|
155
|
+
const linkPath = join(moduleDir, 'node_modules', name);
|
|
156
|
+
if (linkState(linkPath) === 'dangling') {
|
|
157
|
+
stderr.write(
|
|
158
|
+
`playbook run: ${linkPath} is a stale engine link to missing ` +
|
|
159
|
+
`${readlinkSync(linkPath)}; rerun without --no-provision to relink\n`,
|
|
160
|
+
);
|
|
161
|
+
return { code: 1 };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return {};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const manifestPath = declaringManifest(moduleDir);
|
|
168
|
+
if (manifestPath !== undefined) {
|
|
169
|
+
stderr.write(
|
|
170
|
+
`playbook run: ${manifestPath} declares @sublang/playbook; ` +
|
|
171
|
+
'provisioning would shadow the project install — run the ' +
|
|
172
|
+
"project's dependency install (e.g. npm install) instead\n",
|
|
173
|
+
);
|
|
174
|
+
return { code: 1 };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let roots;
|
|
178
|
+
try {
|
|
179
|
+
roots = hostRoots ?? defaultHostRoots();
|
|
180
|
+
} catch (error) {
|
|
181
|
+
stderr.write(
|
|
182
|
+
`playbook run: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
183
|
+
);
|
|
184
|
+
return { code: 1 };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// PBCLI-37: validate every destination before mutating any, so an
|
|
188
|
+
// occupied-path refusal leaves the module directory unchanged rather
|
|
189
|
+
// than half-provisioned.
|
|
190
|
+
const plans = [];
|
|
191
|
+
for (const name of missing) {
|
|
192
|
+
const linkPath = join(moduleDir, 'node_modules', name);
|
|
193
|
+
const state = linkState(linkPath);
|
|
194
|
+
if (state === 'occupied' || state === 'live') {
|
|
195
|
+
// A live-but-unresolvable link is as foreign as a real directory:
|
|
196
|
+
// neither is a link this host may replace.
|
|
197
|
+
stderr.write(
|
|
198
|
+
`playbook run: cannot provision ${linkPath}: the path is already ` +
|
|
199
|
+
`occupied${state === 'live' ? ' by a foreign symbolic link' : ''}\n`,
|
|
200
|
+
);
|
|
201
|
+
return { code: 1 };
|
|
202
|
+
}
|
|
203
|
+
plans.push({
|
|
204
|
+
linkPath,
|
|
205
|
+
target: roots[name],
|
|
206
|
+
dangling: state === 'dangling',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const created = [];
|
|
211
|
+
try {
|
|
212
|
+
for (const { linkPath, target, dangling } of plans) {
|
|
213
|
+
if (dangling) await unlink(linkPath);
|
|
214
|
+
await mkdir(dirname(linkPath), { recursive: true });
|
|
215
|
+
await symlink(target, linkPath, 'dir');
|
|
216
|
+
created.push(`${linkPath} -> ${target}`);
|
|
217
|
+
}
|
|
218
|
+
} catch (error) {
|
|
219
|
+
// PBCLI-37: a filesystem failure is a load fault, not a raw crash.
|
|
220
|
+
stderr.write(
|
|
221
|
+
'playbook run: cannot provision engine links: ' +
|
|
222
|
+
`${error instanceof Error ? error.message : String(error)}\n`,
|
|
223
|
+
);
|
|
224
|
+
return { code: 1 };
|
|
225
|
+
}
|
|
226
|
+
stderr.write(`playbook run: provisioned ${created.join(', ')}\n`);
|
|
227
|
+
return {};
|
|
228
|
+
}
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
} from 'node:fs/promises';
|
|
22
22
|
import { homedir } from 'node:os';
|
|
23
23
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
24
|
-
import { pathToFileURL } from 'node:url';
|
|
24
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
25
25
|
import {
|
|
26
26
|
Cligent,
|
|
27
27
|
isEffortSupported,
|
|
@@ -29,6 +29,7 @@ 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 { provisionEngine } from './provision.js';
|
|
32
33
|
|
|
33
34
|
// PBCLI-19: adapter shorthands the run host can construct.
|
|
34
35
|
const ADAPTER_LOADERS = {
|
|
@@ -76,6 +77,9 @@ export async function runPlaybookRun(options = {}) {
|
|
|
76
77
|
readStdin,
|
|
77
78
|
sessionsDir,
|
|
78
79
|
userConfigPath,
|
|
80
|
+
// PBCLI-37: injected host package roots let tests provision against
|
|
81
|
+
// synthetic trees, like the injected session store.
|
|
82
|
+
hostRoots: options.hostRoots,
|
|
79
83
|
};
|
|
80
84
|
|
|
81
85
|
let args;
|
|
@@ -101,6 +105,11 @@ async function runFirst(args, ctx) {
|
|
|
101
105
|
return { code: EXIT.arg };
|
|
102
106
|
}
|
|
103
107
|
|
|
108
|
+
// PBCLI-36/37 (DR-024): provision engine links for a filesystem module
|
|
109
|
+
// before importing it; a resolvable engine is never touched.
|
|
110
|
+
const provisioned = await maybeProvision(args.from, args, ctx);
|
|
111
|
+
if (provisioned.code !== undefined) return provisioned;
|
|
112
|
+
|
|
104
113
|
const loaded = await loadRegistryEntry(args.from, ctx);
|
|
105
114
|
if (loaded.code !== undefined) return loaded;
|
|
106
115
|
const { entry } = loaded;
|
|
@@ -246,6 +255,11 @@ async function runResume(args, ctx) {
|
|
|
246
255
|
return { code: EXIT.arg };
|
|
247
256
|
}
|
|
248
257
|
|
|
258
|
+
// PBCLI-37: a stored filesystem `from` (a file: URL) is probed and
|
|
259
|
+
// provisioned on resume exactly as on a first run.
|
|
260
|
+
const provisioned = await maybeProvision(record.from, args, ctx);
|
|
261
|
+
if (provisioned.code !== undefined) return provisioned;
|
|
262
|
+
|
|
249
263
|
const loaded = await loadRegistryEntry(record.from, ctx);
|
|
250
264
|
if (loaded.code !== undefined) return loaded;
|
|
251
265
|
const { entry } = loaded;
|
|
@@ -876,6 +890,7 @@ export function parseRunArgs(argv) {
|
|
|
876
890
|
cwd: undefined,
|
|
877
891
|
json: false,
|
|
878
892
|
verbose: false,
|
|
893
|
+
noProvision: false,
|
|
879
894
|
help: false,
|
|
880
895
|
};
|
|
881
896
|
const positionals = [];
|
|
@@ -884,6 +899,7 @@ export function parseRunArgs(argv) {
|
|
|
884
899
|
if (arg === '--help' || arg === '-h') args.help = true;
|
|
885
900
|
else if (arg === '--json') args.json = true;
|
|
886
901
|
else if (arg === '--verbose') args.verbose = true;
|
|
902
|
+
else if (arg === '--no-provision') args.noProvision = true;
|
|
887
903
|
else if (arg === '--last') args.last = true;
|
|
888
904
|
else if (arg === '--cwd') args.cwd = takeValue(argv, (i += 1), '--cwd');
|
|
889
905
|
else if (arg === '--captain')
|
|
@@ -961,6 +977,36 @@ function registryImportSpecifier(specifier, cwd) {
|
|
|
961
977
|
return specifier;
|
|
962
978
|
}
|
|
963
979
|
|
|
980
|
+
// PBCLI-37: the absolute file path of a filesystem `<from>` (path or
|
|
981
|
+
// file: URL), or undefined for a bare package specifier — those resolve
|
|
982
|
+
// from the host's own module tree and are neither probed nor provisioned.
|
|
983
|
+
function moduleFilePath(specifier, cwd) {
|
|
984
|
+
if (specifier.startsWith('file:')) return fileURLToPath(specifier);
|
|
985
|
+
if (
|
|
986
|
+
isAbsolute(specifier) ||
|
|
987
|
+
specifier.startsWith('./') ||
|
|
988
|
+
specifier.startsWith('../') ||
|
|
989
|
+
specifier.startsWith('.\\') ||
|
|
990
|
+
specifier.startsWith('..\\')
|
|
991
|
+
) {
|
|
992
|
+
return resolve(cwd, specifier);
|
|
993
|
+
}
|
|
994
|
+
return undefined;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// PBCLI-36/37: probe-and-provision for a filesystem registry module.
|
|
998
|
+
// Returns {} to proceed or { code } after a reported provisioning fault.
|
|
999
|
+
async function maybeProvision(specifier, args, ctx) {
|
|
1000
|
+
const modulePath = moduleFilePath(specifier, ctx.cwdDefault);
|
|
1001
|
+
if (modulePath === undefined) return {};
|
|
1002
|
+
return provisionEngine({
|
|
1003
|
+
modulePath,
|
|
1004
|
+
stderr: ctx.stderr,
|
|
1005
|
+
enabled: !args.noProvision,
|
|
1006
|
+
hostRoots: ctx.hostRoots,
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
|
|
964
1010
|
function isValidRegistryEntry(value) {
|
|
965
1011
|
return (
|
|
966
1012
|
typeof value === 'object' &&
|
|
@@ -1000,6 +1046,8 @@ function runHelpText() {
|
|
|
1000
1046
|
' output or questions) instead of plain text',
|
|
1001
1047
|
' --last resume the most recently parked session',
|
|
1002
1048
|
' --verbose forward telemetry topics to stderr',
|
|
1049
|
+
' --no-provision never create engine links beside a',
|
|
1050
|
+
' filesystem <from> module',
|
|
1003
1051
|
' -h, --help print this help',
|
|
1004
1052
|
'',
|
|
1005
1053
|
' <agent> is <adapter>[:<model>][@<effort>] over the shorthands',
|
package/slc/link.md
CHANGED
|
@@ -1335,6 +1335,20 @@ The emitted module:
|
|
|
1335
1335
|
type; supplying one is a construction error, so a linker that judges a
|
|
1336
1336
|
runtime-owned arm to have lost payload detail under erasure shall report
|
|
1337
1337
|
that gap rather than emit the entry.
|
|
1338
|
+
- Supplies `spec.compat` with the compatibility values current at link time:
|
|
1339
|
+
`{ artifactSchema, runtimeAbi }`, where `artifactSchema` is `1` — the
|
|
1340
|
+
schema number of the thin-module format this §Output defines — and
|
|
1341
|
+
`runtimeAbi` is the installed shared engine's `RUNTIME_ABI` self-report.
|
|
1342
|
+
The linker shall verify that the installed engine lists the emitted
|
|
1343
|
+
schema in `SUPPORTED_ARTIFACT_SCHEMAS` and treat its absence as a
|
|
1344
|
+
link-time error; it shall not stamp a different member (such as the
|
|
1345
|
+
highest) merely because that engine also supports a newer artifact
|
|
1346
|
+
format — the declaration names the format of the emitted module, not
|
|
1347
|
+
the capability of the emitting engine. The factory checks the
|
|
1348
|
+
declaration against the engine instance that actually loads the emitted
|
|
1349
|
+
module and fails construction on a mismatch, so an artifact linked under
|
|
1350
|
+
one engine cannot run silently skewed under another. Modules emitted
|
|
1351
|
+
before this contract carry no `compat` member and remain loadable.
|
|
1338
1352
|
- Default-exports the factory call as `createPlaybookRuntime`, typed
|
|
1339
1353
|
`PlaybookRuntimeFactory<PlaybookRuntimeOptions>`.
|
|
1340
1354
|
- Exposes, under an `_internal` export, the pure helpers verification
|
|
@@ -70,9 +70,26 @@ export declare const BOSS_REPLY_ERRORS: {
|
|
|
70
70
|
readonly missingQuestion: "needsBossReply outcome missing 'question' field";
|
|
71
71
|
readonly unregisteredState: (stateId: string) => string;
|
|
72
72
|
};
|
|
73
|
+
/** The runtime ABI this engine implements (DR-022). */
|
|
74
|
+
export declare const RUNTIME_ABI = 1;
|
|
75
|
+
/** The linked-artifact schema versions this engine accepts (DR-022). */
|
|
76
|
+
export declare const SUPPORTED_ARTIFACT_SCHEMAS: readonly number[];
|
|
77
|
+
/** A linked artifact's declared link-time compatibility values (DR-022). */
|
|
78
|
+
export interface XStatePlaybookRuntimeCompat {
|
|
79
|
+
/** The artifact schema version the linker emitted. */
|
|
80
|
+
artifactSchema: number;
|
|
81
|
+
/** The engine ABI the artifact was linked against. */
|
|
82
|
+
runtimeAbi: number;
|
|
83
|
+
}
|
|
73
84
|
export interface XStatePlaybookRuntimeSpec<TOptions> {
|
|
74
85
|
/** Diagnostic label used in internal invariant errors. Default 'playbook'. */
|
|
75
86
|
label?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Link-time compatibility declaration checked at construction against the
|
|
89
|
+
* loaded engine's self-report (DR-022). Absent: a legacy artifact emitted
|
|
90
|
+
* before the contract — constructed with no compatibility check.
|
|
91
|
+
*/
|
|
92
|
+
compat?: XStatePlaybookRuntimeCompat;
|
|
76
93
|
/** Validate and JSON-snapshot the caller's per-run options. */
|
|
77
94
|
snapshotOptions: (value: unknown) => TOptions;
|
|
78
95
|
/** Derive the FSM machine input from validated options. Default: identity. */
|
|
@@ -42,6 +42,48 @@ function isFsmResultFailure(error) {
|
|
|
42
42
|
fsmResultFailures.has(error));
|
|
43
43
|
}
|
|
44
44
|
// ---------------------------------------------------------------------------
|
|
45
|
+
// DR-022: the engine's compatibility self-report. A linked thin module
|
|
46
|
+
// records the values current at link time in `spec.compat`; the factory
|
|
47
|
+
// checks that declaration against this very module — the engine instance
|
|
48
|
+
// that will interpret the FSM, so the check can never consult a different
|
|
49
|
+
// engine copy than the one executing — and fails construction on a mismatch
|
|
50
|
+
// instead of misbehaving deep in a session. Raising RUNTIME_ABI or removing
|
|
51
|
+
// a member of SUPPORTED_ARTIFACT_SCHEMAS is a breaking change (RELEASE-15).
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
/** The runtime ABI this engine implements (DR-022). */
|
|
54
|
+
export const RUNTIME_ABI = 1;
|
|
55
|
+
/** The linked-artifact schema versions this engine accepts (DR-022). */
|
|
56
|
+
export const SUPPORTED_ARTIFACT_SCHEMAS = Object.freeze([
|
|
57
|
+
1,
|
|
58
|
+
]);
|
|
59
|
+
// PBRT-50: validate a declaration against the loaded engine, schema first,
|
|
60
|
+
// so one clear diagnostic covers a fully skewed artifact. Absent means a
|
|
61
|
+
// legacy artifact emitted before the DR-022 contract; those must keep
|
|
62
|
+
// loading unchanged (DR-019 §4), so there is nothing to check.
|
|
63
|
+
function assertRuntimeCompat(compat, label) {
|
|
64
|
+
if (compat === undefined)
|
|
65
|
+
return;
|
|
66
|
+
if (compat === null || typeof compat !== 'object') {
|
|
67
|
+
throw new TypeError(`${label} spec.compat must be an object`);
|
|
68
|
+
}
|
|
69
|
+
const { artifactSchema, runtimeAbi } = compat;
|
|
70
|
+
if (!Number.isSafeInteger(artifactSchema)) {
|
|
71
|
+
throw new TypeError(`${label} spec.compat.artifactSchema must be an integer`);
|
|
72
|
+
}
|
|
73
|
+
if (!Number.isSafeInteger(runtimeAbi)) {
|
|
74
|
+
throw new TypeError(`${label} spec.compat.runtimeAbi must be an integer`);
|
|
75
|
+
}
|
|
76
|
+
if (!SUPPORTED_ARTIFACT_SCHEMAS.includes(artifactSchema)) {
|
|
77
|
+
throw new TypeError(`${label} artifact declares schema ${artifactSchema}, but this ` +
|
|
78
|
+
`@sublang/playbook/xstate-runtime engine supports ` +
|
|
79
|
+
`[${SUPPORTED_ARTIFACT_SCHEMAS.join(', ')}]`);
|
|
80
|
+
}
|
|
81
|
+
if (runtimeAbi !== RUNTIME_ABI) {
|
|
82
|
+
throw new TypeError(`${label} artifact declares runtime ABI ${runtimeAbi}, but this ` +
|
|
83
|
+
`@sublang/playbook/xstate-runtime engine implements ${RUNTIME_ABI}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
45
87
|
// Tolerant judge-JSON recovery (slc/link.md §Boss-event mapping).
|
|
46
88
|
// ---------------------------------------------------------------------------
|
|
47
89
|
function isPlainObject(value) {
|
|
@@ -857,6 +899,9 @@ function makeDefaultClassifyBossText(machine, entryEvent, bossEvents) {
|
|
|
857
899
|
*/
|
|
858
900
|
export function createXStatePlaybookRuntime(machine, spec) {
|
|
859
901
|
const label = spec.label ?? 'playbook';
|
|
902
|
+
// DR-022 / PBRT-50: reject an incompatible artifact declaration before any
|
|
903
|
+
// machine interpretation, against this loaded engine's own self-report.
|
|
904
|
+
assertRuntimeCompat(spec.compat, label);
|
|
860
905
|
const declaredActors = collectInvokeSources(machine);
|
|
861
906
|
const resumableStateIds = spec.resumableStateIds ?? resumableStateIdsFromMachine(machine);
|
|
862
907
|
const resolvePlayerIdSpec = spec.resolvePlayerId;
|
|
@@ -177,18 +177,86 @@ function isFsmResultFailure(error: unknown): boolean {
|
|
|
177
177
|
);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// DR-022: the engine's compatibility self-report. A linked thin module
|
|
182
|
+
// records the values current at link time in `spec.compat`; the factory
|
|
183
|
+
// checks that declaration against this very module — the engine instance
|
|
184
|
+
// that will interpret the FSM, so the check can never consult a different
|
|
185
|
+
// engine copy than the one executing — and fails construction on a mismatch
|
|
186
|
+
// instead of misbehaving deep in a session. Raising RUNTIME_ABI or removing
|
|
187
|
+
// a member of SUPPORTED_ARTIFACT_SCHEMAS is a breaking change (RELEASE-15).
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
/** The runtime ABI this engine implements (DR-022). */
|
|
191
|
+
export const RUNTIME_ABI = 1;
|
|
192
|
+
|
|
193
|
+
/** The linked-artifact schema versions this engine accepts (DR-022). */
|
|
194
|
+
export const SUPPORTED_ARTIFACT_SCHEMAS: readonly number[] = Object.freeze([
|
|
195
|
+
1,
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
/** A linked artifact's declared link-time compatibility values (DR-022). */
|
|
199
|
+
export interface XStatePlaybookRuntimeCompat {
|
|
200
|
+
/** The artifact schema version the linker emitted. */
|
|
201
|
+
artifactSchema: number;
|
|
202
|
+
/** The engine ABI the artifact was linked against. */
|
|
203
|
+
runtimeAbi: number;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// PBRT-50: validate a declaration against the loaded engine, schema first,
|
|
207
|
+
// so one clear diagnostic covers a fully skewed artifact. Absent means a
|
|
208
|
+
// legacy artifact emitted before the DR-022 contract; those must keep
|
|
209
|
+
// loading unchanged (DR-019 §4), so there is nothing to check.
|
|
210
|
+
function assertRuntimeCompat(
|
|
211
|
+
compat: XStatePlaybookRuntimeCompat | undefined,
|
|
212
|
+
label: string,
|
|
213
|
+
): void {
|
|
214
|
+
if (compat === undefined) return;
|
|
215
|
+
if (compat === null || typeof compat !== 'object') {
|
|
216
|
+
throw new TypeError(`${label} spec.compat must be an object`);
|
|
217
|
+
}
|
|
218
|
+
const { artifactSchema, runtimeAbi } = compat;
|
|
219
|
+
if (!Number.isSafeInteger(artifactSchema)) {
|
|
220
|
+
throw new TypeError(
|
|
221
|
+
`${label} spec.compat.artifactSchema must be an integer`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (!Number.isSafeInteger(runtimeAbi)) {
|
|
225
|
+
throw new TypeError(`${label} spec.compat.runtimeAbi must be an integer`);
|
|
226
|
+
}
|
|
227
|
+
if (!SUPPORTED_ARTIFACT_SCHEMAS.includes(artifactSchema)) {
|
|
228
|
+
throw new TypeError(
|
|
229
|
+
`${label} artifact declares schema ${artifactSchema}, but this ` +
|
|
230
|
+
`@sublang/playbook/xstate-runtime engine supports ` +
|
|
231
|
+
`[${SUPPORTED_ARTIFACT_SCHEMAS.join(', ')}]`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
if (runtimeAbi !== RUNTIME_ABI) {
|
|
235
|
+
throw new TypeError(
|
|
236
|
+
`${label} artifact declares runtime ABI ${runtimeAbi}, but this ` +
|
|
237
|
+
`@sublang/playbook/xstate-runtime engine implements ${RUNTIME_ABI}`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
180
242
|
// ---------------------------------------------------------------------------
|
|
181
243
|
// The per-workflow spec. Every strategy member has a generic default derived
|
|
182
244
|
// from the FSM artifact's own data, so a linker-emitted thin module normally
|
|
183
|
-
// supplies only `snapshotOptions` and, where applicable, `
|
|
184
|
-
// Boss-event field metadata, placeholder exceptions, and
|
|
185
|
-
// fields. Hand-maintained artifacts may override any member
|
|
186
|
-
// existing observable behavior exactly.
|
|
245
|
+
// supplies only `snapshotOptions` and, where applicable, `compat`,
|
|
246
|
+
// `entryEvent`, erased Boss-event field metadata, placeholder exceptions, and
|
|
247
|
+
// transition-event fields. Hand-maintained artifacts may override any member
|
|
248
|
+
// to preserve their existing observable behavior exactly.
|
|
187
249
|
// ---------------------------------------------------------------------------
|
|
188
250
|
|
|
189
251
|
export interface XStatePlaybookRuntimeSpec<TOptions> {
|
|
190
252
|
/** Diagnostic label used in internal invariant errors. Default 'playbook'. */
|
|
191
253
|
label?: string;
|
|
254
|
+
/**
|
|
255
|
+
* Link-time compatibility declaration checked at construction against the
|
|
256
|
+
* loaded engine's self-report (DR-022). Absent: a legacy artifact emitted
|
|
257
|
+
* before the contract — constructed with no compatibility check.
|
|
258
|
+
*/
|
|
259
|
+
compat?: XStatePlaybookRuntimeCompat;
|
|
192
260
|
/** Validate and JSON-snapshot the caller's per-run options. */
|
|
193
261
|
snapshotOptions: (value: unknown) => TOptions;
|
|
194
262
|
/** Derive the FSM machine input from validated options. Default: identity. */
|
|
@@ -1337,6 +1405,9 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
1337
1405
|
spec: XStatePlaybookRuntimeSpec<TOptions>,
|
|
1338
1406
|
): PlaybookRuntimeFactory<TOptions> {
|
|
1339
1407
|
const label = spec.label ?? 'playbook';
|
|
1408
|
+
// DR-022 / PBRT-50: reject an incompatible artifact declaration before any
|
|
1409
|
+
// machine interpretation, against this loaded engine's own self-report.
|
|
1410
|
+
assertRuntimeCompat(spec.compat, label);
|
|
1340
1411
|
const declaredActors = collectInvokeSources(machine);
|
|
1341
1412
|
const resumableStateIds =
|
|
1342
1413
|
spec.resumableStateIds ?? resumableStateIdsFromMachine(machine);
|