@mamund/grail 0.0.0-stage → 0.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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -2
  3. package/affordanceModel.js +23 -0
  4. package/affordanceRegistry.js +14 -0
  5. package/bindings/binding.js +229 -0
  6. package/bindings/httpBinding.js +173 -0
  7. package/bindings/nodeBinding.js +25 -0
  8. package/bindings/stdioBinding.js +58 -0
  9. package/cli/grail-cli.js +565 -0
  10. package/client.js +77 -0
  11. package/config-loader/loadEnvironment.js +71 -0
  12. package/docs/ENVIRONMENT.md +381 -0
  13. package/docs/grail-trust-model.md +665 -0
  14. package/examples/cli-tour/README.md +346 -0
  15. package/examples/cli-tour/capabilities/farewell.js +3 -0
  16. package/examples/cli-tour/capabilities/hello.js +3 -0
  17. package/examples/cli-tour/config/goal.json +3 -0
  18. package/examples/cli-tour/config/inputs.json +3 -0
  19. package/examples/cli-tour/config/observations.json +23 -0
  20. package/examples/cli-tour/config/registry.json +48 -0
  21. package/examples/cli-tour/config/worldstate.json +4 -0
  22. package/examples/cli-tour/g-loop.sh +7 -0
  23. package/examples/cli-tour/inputs/jane.json +3 -0
  24. package/examples/cli-tour/inputs/mike.json +3 -0
  25. package/examples/cli-tour/inputs/ruth.json +3 -0
  26. package/grail.js +59 -0
  27. package/images/grail-release-banner.png +0 -0
  28. package/index.js +2 -0
  29. package/observationStore.js +110 -0
  30. package/package.json +57 -4
  31. package/schemas/goal.schema.json +14 -0
  32. package/schemas/inputs.schema.json +6 -0
  33. package/schemas/observations.schema.json +75 -0
  34. package/schemas/registry.schema.json +285 -0
  35. package/schemas/worldstate.schema.json +7 -0
  36. package/server.js +201 -0
  37. package/utils/loadJSON.js +33 -0
  38. package/utils/validateWithSchema.js +42 -0
  39. package/worldState.js +36 -0
@@ -0,0 +1,346 @@
1
+ # A Tour of GRAIL at the Command Line
2
+
3
+ GRAIL is a runtime for pursuing a goal within a bounded environment of available capabilities.
4
+
5
+ The easiest way to understand GRAIL is to use it. This tour starts with a small working GRAIL world and changes one thing at a time. You will change inputs, invoke the world from different locations, pursue a different goal, ask for a goal the world cannot resolve, and finally use the shell to repeat pursuits.
6
+
7
+ The important thing to watch is what does not change: the world continues to define the available possibilities while each invocation supplies the context for a particular pursuit. Along the way, `grail show` lets you inspect the configured world, and `grail run --output` lets you inspect the result of a pursuit.
8
+
9
+ ## Before you begin
10
+
11
+ This directory is a complete GRAIL world prepared for the tour:
12
+
13
+ ```text
14
+ cli-tour/
15
+ ├── capabilities/
16
+ │ ├── farewell.js
17
+ │ └── hello.js
18
+ ├── config/
19
+ │ ├── goal.json
20
+ │ ├── inputs.json
21
+ │ ├── registry.json
22
+ │ └── worldstate.json
23
+ └── inputs/
24
+ ├── jane.json
25
+ ├── mike.json
26
+ └── ruth.json
27
+ ```
28
+
29
+ The examples assume that the `grail` command is installed or linked and that your shell is initially in this `cli-tour` directory.
30
+
31
+ ## 1. Validate and run the world
32
+
33
+ Start by validating the environment:
34
+
35
+ ```bash
36
+ grail validate
37
+ ```
38
+
39
+ GRAIL uses `./config` as the default configuration directory, so no `--config` option is needed when `config` is a subdirectory of the current directory.
40
+
41
+ Before running, inspect the world:
42
+
43
+ ```bash
44
+ grail show
45
+ ```
46
+
47
+ This displays the four configured documents. You can also inspect one at a time:
48
+
49
+ ```bash
50
+ grail show goal
51
+ grail show registry
52
+ ```
53
+
54
+ `show` displays the configured environment; it does not execute capabilities or display the results of earlier pursuits.
55
+
56
+ Now run it:
57
+
58
+ ```bash
59
+ grail run
60
+ ```
61
+
62
+ The configured goal is `greetingCreated`. The registry contains an affordance that can establish that effect, so GRAIL can select it, invoke the bound `hello.js` capability, and reach the goal.
63
+
64
+ You have just pursued a GRAIL goal.
65
+
66
+ To see a concise account of the pursuit, run it again with:
67
+
68
+ ```bash
69
+ grail run --output summary
70
+ ```
71
+
72
+ The summary reports the goal, outcome, capability invocations, and any captured outputs. Unlike the default run, it focuses on the result rather than the running trace.
73
+
74
+ ## 2. Change the input
75
+
76
+ The default input is stored in `config/inputs.json`:
77
+
78
+ ```json
79
+ {
80
+ "name": "World"
81
+ }
82
+ ```
83
+
84
+ You do not need to edit that file to use a different value. Try:
85
+
86
+ ```bash
87
+ grail run --inputs '{"name":"Mike"}' --output summary
88
+ ```
89
+
90
+ Then try another:
91
+
92
+ ```bash
93
+ grail run --inputs '{"name":"Jane"}' --output summary
94
+ ```
95
+
96
+ The same world is being used each time. Only the invocation input changes. The summaries let you compare the captured greeting outputs directly.
97
+
98
+ For a `run` invocation, an `--inputs` value replaces the value loaded from `inputs.json`. It does not modify `inputs.json` itself.
99
+
100
+ ## 3. Supply inputs from a file
101
+
102
+ The `inputs/` directory contains several input documents. For example, `inputs/ruth.json` contains:
103
+
104
+ ```json
105
+ {
106
+ "name": "Ruth"
107
+ }
108
+ ```
109
+
110
+ Use it with:
111
+
112
+ ```bash
113
+ grail run --inputs-file ./inputs/ruth.json
114
+ ```
115
+
116
+ An `--inputs-file` path is resolved relative to the caller's current working directory. This is different from capability binding paths, which belong to the GRAIL world.
117
+
118
+ `--inputs` and `--inputs-file` are alternative ways to supply invocation inputs and cannot be used together.
119
+
120
+ ## 4. Leave the world
121
+
122
+ So far the shell has been inside `cli-tour`. Move to its parent directory:
123
+
124
+ ```bash
125
+ cd ..
126
+ ```
127
+
128
+ Now run the same world explicitly:
129
+
130
+ ```bash
131
+ grail run --config ./cli-tour/config
132
+ ```
133
+
134
+ The world still works.
135
+
136
+ You can also inspect its registry from here:
137
+
138
+ ```bash
139
+ grail show registry --config ./cli-tour/config
140
+ ```
141
+
142
+ The selected configuration directory tells GRAIL where the world lives. Relative capability binding paths are resolved from the **world root**, which is the parent of the configuration directory, rather than from the shell's current working directory.
143
+
144
+ That means the caller and the world do not need to occupy the same place.
145
+
146
+ For the remaining examples, stay in this parent directory and use `--config ./cli-tour/config`.
147
+
148
+ ## 5. Explore the possibilities in the world
149
+
150
+ This tour world contains two capabilities:
151
+
152
+ ```text
153
+ hello.js -> greetingCreated
154
+ farewell.js -> farewellCreated
155
+ ```
156
+
157
+ The default `config/goal.json` contains:
158
+
159
+ ```json
160
+ {
161
+ "goal": "greetingCreated"
162
+ }
163
+ ```
164
+
165
+ Confirm the configured goal and available affordances without opening the files:
166
+
167
+ ```bash
168
+ grail show goal --config ./cli-tour/config
169
+ grail show registry --config ./cli-tour/config
170
+ ```
171
+
172
+ But the registry describes both possibilities. It is not a workflow saying that greeting must happen before farewell or that both must happen. It describes capabilities that are available in this world and the effects they can establish.
173
+
174
+ ## 6. Change the goal
175
+
176
+ Ask the same world to pursue its other available effect:
177
+
178
+ ```bash
179
+ grail run \
180
+ --config ./cli-tour/config \
181
+ --goal farewellCreated \
182
+ --inputs '{"name":"Mike"}' \
183
+ --output summary
184
+ ```
185
+
186
+ GRAIL now pursues `farewellCreated` instead of the goal stored in `goal.json`.
187
+
188
+ For a `run` invocation, `--goal` takes precedence over `goal.json`. The file itself is not changed. If you run `grail show goal --config ./cli-tour/config` again, you will still see `greetingCreated`.
189
+
190
+ You can therefore make different requests of the same world:
191
+
192
+ ```bash
193
+ grail run \
194
+ --config ./cli-tour/config \
195
+ --goal greetingCreated \
196
+ --inputs '{"name":"Mike"}'
197
+
198
+ grail run \
199
+ --config ./cli-tour/config \
200
+ --goal farewellCreated \
201
+ --inputs '{"name":"Mike"}'
202
+ ```
203
+
204
+ The world defines the possibilities. The invocation identifies which possibility GRAIL should pursue.
205
+
206
+ ## 7. Ask for something the world cannot do
207
+
208
+ Now request a goal for which the world has no producer:
209
+
210
+ ```bash
211
+ grail run \
212
+ --config ./cli-tour/config \
213
+ --goal makeCoffee
214
+ ```
215
+
216
+ GRAIL should report that `makeCoffee` cannot be resolved with the available capabilities.
217
+
218
+ Check the process exit code immediately afterward:
219
+
220
+ ```bash
221
+ echo $?
222
+ ```
223
+
224
+ The result should be:
225
+
226
+ ```text
227
+ 1
228
+ ```
229
+
230
+ The command was valid and GRAIL was able to examine the world. The pursuit failed because the requested goal could not be reached using the capabilities available in that world.
231
+
232
+ This is different from an invalid command or invalid configuration, which exits with code `2`.
233
+
234
+ You can inspect the complete structured pursuit result as JSON:
235
+
236
+ ```bash
237
+ grail run \
238
+ --config ./cli-tour/config \
239
+ --goal makeCoffee \
240
+ --output json
241
+ ```
242
+
243
+ The result includes the requested goal, whether it was reached, the resulting worldstate, and observations. In JSON mode, routine trace messages do not mix into stdout. This unsuccessful pursuit still exits with code `1`.
244
+
245
+ To inspect only the observations, you can pipe a successful pursuit through `jq` (if installed):
246
+
247
+ ```bash
248
+ grail run \
249
+ --config ./cli-tour/config \
250
+ --output json | jq '.observations'
251
+ ```
252
+
253
+ The `json` mode includes observations, so a separate observations output mode is unnecessary.
254
+
255
+ ## 8. Let the shell provide the loop
256
+
257
+ The `inputs/` directory contains three cases:
258
+
259
+ ```text
260
+ inputs/
261
+ ├── jane.json
262
+ ├── mike.json
263
+ └── ruth.json
264
+ ```
265
+
266
+ Use the shell to invoke GRAIL once for each input document:
267
+
268
+ ```bash
269
+ for input in ./cli-tour/inputs/*.json
270
+ do
271
+ grail run \
272
+ --config ./cli-tour/config \
273
+ --inputs-file "$input"
274
+ done
275
+ ```
276
+
277
+ The shell owns the loop. Each iteration starts a separate GRAIL pursuit with a different invocation context.
278
+
279
+ For a compact view of each result, add `--output summary`:
280
+
281
+ ```bash
282
+ for input in ./cli-tour/inputs/*.json
283
+ do
284
+ grail run \
285
+ --config ./cli-tour/config \
286
+ --inputs-file "$input" \
287
+ --output summary
288
+ done
289
+ ```
290
+
291
+ GRAIL itself does not need looping semantics to participate in a repetitive or larger process.
292
+
293
+ The exit-code contract also makes it possible for the caller to decide what to do when a pursuit fails:
294
+
295
+ ```bash
296
+ for input in ./cli-tour/inputs/*.json
297
+ do
298
+ grail run \
299
+ --config ./cli-tour/config \
300
+ --inputs-file "$input" || break
301
+ done
302
+ ```
303
+
304
+ Here the shell stops the loop if GRAIL returns a nonzero exit code.
305
+
306
+ ## What changed?
307
+
308
+ During this tour, you changed:
309
+
310
+ - the inputs supplied to a pursuit;
311
+ - the location from which GRAIL was invoked;
312
+ - the goal GRAIL was asked to pursue; and
313
+ - the number of times the caller invoked GRAIL.
314
+
315
+ You also inspected the configured world with `show` and the results of individual pursuits with `--output`.
316
+
317
+ You did not create a new workflow for each variation. The same world continued to describe its conditions, affordances, effects, and bound capabilities.
318
+
319
+ A useful way to think about the separation is:
320
+
321
+ ```text
322
+ World
323
+ defines what is possible
324
+
325
+ Invocation
326
+ supplies the goal and inputs
327
+
328
+ GRAIL
329
+ pursues the goal within the world
330
+
331
+ Capabilities
332
+ perform the domain work
333
+
334
+ Caller
335
+ decides when and how often to invoke GRAIL
336
+ ```
337
+
338
+ The distinction is worth keeping in mind: **`show` describes the configured world; invocation options choose the goal and inputs; `--output` reports the pursuit result.**
339
+
340
+ This is the central idea behind working with GRAIL from the command line.
341
+
342
+ > Define the environment, not the path.
343
+
344
+ ## Where to go next
345
+
346
+ This tour focused on using an existing GRAIL world rather than authoring one. The next step is to inspect `config/registry.json`, `config/worldstate.json`, and the two files in `capabilities/` and see how the world's possibilities are declared and bound to executable behavior.
@@ -0,0 +1,3 @@
1
+ export async function createFarewell({ name }) {
2
+ return { message: `Goodbye, ${name}!` };
3
+ }
@@ -0,0 +1,3 @@
1
+ export async function createGreeting({ name }) {
2
+ return { message: `Hello, ${name}!` };
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "goal": "greetingCreated"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "name": "World"
3
+ }
@@ -0,0 +1,23 @@
1
+ [
2
+ {
3
+ "invocation": {
4
+ "id": "inv-001",
5
+ "affordance": "createGreeting",
6
+ "timestamp": "2026-10-08T19:03:06.523Z",
7
+ "module": "./capabilities/hello.js",
8
+ "function": "createGreeting",
9
+ "inputs": {
10
+ "name": "Ruth"
11
+ }
12
+ },
13
+ "response": {
14
+ "result": {
15
+ "message": "Hello, Ruth!"
16
+ }
17
+ },
18
+ "outputs": {
19
+ "message": "Hello, Ruth!"
20
+ },
21
+ "result": "SUCCESS"
22
+ }
23
+ ]
@@ -0,0 +1,48 @@
1
+ {
2
+ "createGreeting": {
3
+ "id": "aff-create-greeting",
4
+ "action": "createGreeting",
5
+ "type": "task",
6
+ "preconditions": [],
7
+ "inputs": {
8
+ "name": "$inputs.name"
9
+ },
10
+ "effects": [
11
+ "greetingCreated"
12
+ ],
13
+ "binding": {
14
+ "protocol": "node",
15
+ "module": "./capabilities/hello.js",
16
+ "function": "createGreeting",
17
+ "outputs": {
18
+ "message": {
19
+ "from": "result",
20
+ "path": "message"
21
+ }
22
+ }
23
+ }
24
+ },
25
+ "createFarewell": {
26
+ "id": "aff-create-farewell",
27
+ "action": "createFarewell",
28
+ "type": "task",
29
+ "preconditions": [],
30
+ "inputs": {
31
+ "name": "$inputs.name"
32
+ },
33
+ "effects": [
34
+ "farewellCreated"
35
+ ],
36
+ "binding": {
37
+ "protocol": "node",
38
+ "module": "./capabilities/farewell.js",
39
+ "function": "createFarewell",
40
+ "outputs": {
41
+ "message": {
42
+ "from": "result",
43
+ "path": "message"
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "greetingCreated": false,
3
+ "farewellCreated": false
4
+ }
@@ -0,0 +1,7 @@
1
+ for input in ./inputs/*.json
2
+ do
3
+ grail run \
4
+ --config ./config \
5
+ --inputs-file "$input" \
6
+ --output summary
7
+ done
@@ -0,0 +1,3 @@
1
+ {
2
+ "name": "Jane"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "name": "Mike"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "name": "Ruth"
3
+ }
package/grail.js ADDED
@@ -0,0 +1,59 @@
1
+ import { loadAffordanceRegistry } from './affordanceRegistry.js';
2
+ import { WorldState } from './worldState.js';
3
+ import { ObservationStore } from './observationStore.js';
4
+ import { Server } from './server.js';
5
+ import { Client } from './client.js';
6
+
7
+ /**
8
+ * Public facade for the GRAIL runtime.
9
+ *
10
+ * This class deliberately composes the existing runtime components rather
11
+ * than replacing them. Configuration loading and validation remain concerns
12
+ * of the calling application.
13
+ */
14
+ export class Grail {
15
+ constructor({ registry, worldstate, inputs = {}, observationPath, baseDir = process.cwd() }) {
16
+ if (!registry) {
17
+ throw new Error('Grail requires a registry.');
18
+ }
19
+
20
+ if (!worldstate) {
21
+ throw new Error('Grail requires worldstate.');
22
+ }
23
+
24
+ this.inputs = inputs;
25
+ this.affordanceRegistry = loadAffordanceRegistry(registry);
26
+ this.initialWorldstate = { ...worldstate };
27
+ this.observationPath = observationPath;
28
+ this.baseDir = baseDir;
29
+ this.resetPursuit();
30
+ }
31
+
32
+ resetPursuit() {
33
+ this.worldState = new WorldState(this.affordanceRegistry, this.initialWorldstate);
34
+ this.observationStore = new ObservationStore(this.observationPath);
35
+ this.server = new Server(
36
+ this.worldState,
37
+ this.affordanceRegistry,
38
+ this.observationStore,
39
+ this.baseDir
40
+ );
41
+ this.client = new Client(this.server, this.inputs);
42
+ }
43
+
44
+ async pursue(goal) {
45
+ if (!goal) {
46
+ throw new Error('Grail.pursue requires a goal.');
47
+ }
48
+
49
+ this.resetPursuit();
50
+ await this.client.pursue(goal);
51
+
52
+ return {
53
+ goal,
54
+ reached: this.worldState.isPreconditionMet(goal),
55
+ worldstate: { ...this.worldState.state },
56
+ observations: [...this.observationStore.observations]
57
+ };
58
+ }
59
+ }
Binary file
package/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { Grail } from './grail.js';
2
+ export { loadEnvironment } from './config-loader/loadEnvironment.js';
@@ -0,0 +1,110 @@
1
+ import fs from "node:fs";
2
+
3
+ export class ObservationStore {
4
+ constructor(filePath) {
5
+ this.filePath = filePath;
6
+ this.observations = [];
7
+ this.invocationCounter = 0;
8
+ this.persist();
9
+ }
10
+
11
+ nextInvocationId() {
12
+ this.invocationCounter += 1;
13
+ return `inv-${String(this.invocationCounter).padStart(3, "0")}`;
14
+ }
15
+
16
+ append(observation) {
17
+ this.observations.push(observation);
18
+ this.persist();
19
+ }
20
+
21
+ getByAffordance(affordance) {
22
+ return this.observations.filter(
23
+ observation => observation.invocation.affordance === affordance
24
+ );
25
+ }
26
+
27
+ resolve(source) {
28
+ const prefix = "$outputs.";
29
+
30
+ if (!source.startsWith(prefix)) {
31
+ return { resolved: false };
32
+ }
33
+
34
+ const reference = source.slice(prefix.length);
35
+ const parts = reference.split(".");
36
+
37
+ // Scenario-level:
38
+ // $outputs.latest.<outputName>
39
+ if (parts.length === 2) {
40
+ const [selector, outputName] = parts;
41
+
42
+ if (selector !== "latest" || !outputName) {
43
+ return { resolved: false };
44
+ }
45
+
46
+ for (let i = this.observations.length - 1; i >= 0; i--) {
47
+ const observation = this.observations[i];
48
+
49
+ if (
50
+ observation.outputs &&
51
+ Object.prototype.hasOwnProperty.call(
52
+ observation.outputs,
53
+ outputName
54
+ )
55
+ ) {
56
+ return {
57
+ resolved: true,
58
+ value: observation.outputs[outputName]
59
+ };
60
+ }
61
+ }
62
+
63
+ return { resolved: false };
64
+ }
65
+
66
+ if (parts.length !== 3) {
67
+ return { resolved: false };
68
+ }
69
+
70
+ const [affordance, selector, outputName] = parts;
71
+
72
+ if (!affordance || selector !== "latest" || !outputName) {
73
+ return { resolved: false };
74
+ }
75
+
76
+ const matching = this.observations.filter(
77
+ observation => observation.invocation.affordance === affordance
78
+ );
79
+
80
+ if (matching.length === 0) {
81
+ return { resolved: false };
82
+ }
83
+
84
+ const observation = matching[matching.length - 1];
85
+
86
+ if (
87
+ !observation.outputs ||
88
+ !Object.prototype.hasOwnProperty.call(observation.outputs, outputName)
89
+ ) {
90
+ return { resolved: false };
91
+ }
92
+
93
+ return {
94
+ resolved: true,
95
+ value: observation.outputs[outputName]
96
+ };
97
+ }
98
+
99
+ persist() {
100
+ if (!this.filePath) {
101
+ return;
102
+ }
103
+
104
+ fs.writeFileSync(
105
+ this.filePath,
106
+ `${JSON.stringify(this.observations, null, 2)}\n`,
107
+ "utf8"
108
+ );
109
+ }
110
+ }