@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.
- package/LICENSE +21 -0
- package/README.md +328 -2
- package/affordanceModel.js +23 -0
- package/affordanceRegistry.js +14 -0
- package/bindings/binding.js +229 -0
- package/bindings/httpBinding.js +173 -0
- package/bindings/nodeBinding.js +25 -0
- package/bindings/stdioBinding.js +58 -0
- package/cli/grail-cli.js +565 -0
- package/client.js +77 -0
- package/config-loader/loadEnvironment.js +71 -0
- package/docs/ENVIRONMENT.md +381 -0
- package/docs/grail-trust-model.md +665 -0
- package/examples/cli-tour/README.md +346 -0
- package/examples/cli-tour/capabilities/farewell.js +3 -0
- package/examples/cli-tour/capabilities/hello.js +3 -0
- package/examples/cli-tour/config/goal.json +3 -0
- package/examples/cli-tour/config/inputs.json +3 -0
- package/examples/cli-tour/config/observations.json +23 -0
- package/examples/cli-tour/config/registry.json +48 -0
- package/examples/cli-tour/config/worldstate.json +4 -0
- package/examples/cli-tour/g-loop.sh +7 -0
- package/examples/cli-tour/inputs/jane.json +3 -0
- package/examples/cli-tour/inputs/mike.json +3 -0
- package/examples/cli-tour/inputs/ruth.json +3 -0
- package/grail.js +59 -0
- package/images/grail-release-banner.png +0 -0
- package/index.js +2 -0
- package/observationStore.js +110 -0
- package/package.json +57 -4
- package/schemas/goal.schema.json +14 -0
- package/schemas/inputs.schema.json +6 -0
- package/schemas/observations.schema.json +75 -0
- package/schemas/registry.schema.json +285 -0
- package/schemas/worldstate.schema.json +7 -0
- package/server.js +201 -0
- package/utils/loadJSON.js +33 -0
- package/utils/validateWithSchema.js +42 -0
- package/worldState.js +36 -0
package/cli/grail-cli.js
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const packageJson = JSON.parse(
|
|
10
|
+
fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8')
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
function showHelp() {
|
|
14
|
+
console.log(`GRAIL
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
grail <command> [options]
|
|
18
|
+
|
|
19
|
+
Commands:
|
|
20
|
+
init Create a runnable GRAIL world
|
|
21
|
+
run Run a GRAIL environment
|
|
22
|
+
show Display validated configuration
|
|
23
|
+
validate Validate a GRAIL environment
|
|
24
|
+
|
|
25
|
+
Options:
|
|
26
|
+
--config <directory> Configuration directory (default: ./config)
|
|
27
|
+
--goal <effect> Override the configured goal (run only)
|
|
28
|
+
--inputs <json> Override inputs with a JSON object (run only)
|
|
29
|
+
--inputs-file <file> Override inputs from a JSON file (run only)
|
|
30
|
+
--output <mode> Result format: summary or json (run only)
|
|
31
|
+
--help, -h Show help
|
|
32
|
+
--version, -v Show version
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
grail init my-world
|
|
36
|
+
grail validate --config ./config
|
|
37
|
+
grail show registry
|
|
38
|
+
grail run --config ./config
|
|
39
|
+
grail run --config ./config --goal greetingCreated
|
|
40
|
+
grail run --config ./config --inputs '{"name":"Mike"}'
|
|
41
|
+
grail run --config ./config --inputs-file ./cases/mike.json
|
|
42
|
+
grail run --output summary
|
|
43
|
+
grail run --output json`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function showCommandHelp(command) {
|
|
47
|
+
|
|
48
|
+
if (command === 'init') {
|
|
49
|
+
console.log(`GRAIL init
|
|
50
|
+
|
|
51
|
+
Usage:
|
|
52
|
+
grail init <directory>
|
|
53
|
+
|
|
54
|
+
Creates a complete, runnable GRAIL world.
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
grail init my-world`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (command === 'run') {
|
|
62
|
+
console.log(`GRAIL run
|
|
63
|
+
|
|
64
|
+
Usage:
|
|
65
|
+
grail run [--config <directory>] [--goal <effect>] [--inputs <json> | --inputs-file <file>] [--output summary|json]
|
|
66
|
+
|
|
67
|
+
Runs the goal declared by the GRAIL environment, with optional invocation-specific goal and input overrides.
|
|
68
|
+
|
|
69
|
+
Options:
|
|
70
|
+
--config <directory> Configuration directory (default: ./config)
|
|
71
|
+
--goal <effect> Override the goal declared in goal.json for this run
|
|
72
|
+
--inputs <json> Replace inputs.json for this run with an inline JSON object
|
|
73
|
+
--inputs-file <file> Replace inputs.json for this run with a JSON file
|
|
74
|
+
--output <mode> Print a summary or complete JSON result (default: existing trace)
|
|
75
|
+
--help, -h Show help`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (command === 'show') {
|
|
80
|
+
console.log(`GRAIL show
|
|
81
|
+
|
|
82
|
+
Usage:
|
|
83
|
+
grail show [registry|worldstate|inputs|goal] [--config <directory>]
|
|
84
|
+
|
|
85
|
+
Displays validated configuration as formatted JSON without executing capabilities.
|
|
86
|
+
Without a target, displays all four configuration documents.
|
|
87
|
+
|
|
88
|
+
Options:
|
|
89
|
+
--config <directory> Configuration directory (default: ./config)
|
|
90
|
+
--help, -h Show help`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (command === 'validate') {
|
|
95
|
+
console.log(`GRAIL validate
|
|
96
|
+
|
|
97
|
+
Usage:
|
|
98
|
+
grail validate [--config <directory>]
|
|
99
|
+
|
|
100
|
+
Loads and validates the GRAIL environment without running it.
|
|
101
|
+
|
|
102
|
+
Options:
|
|
103
|
+
--config <directory> Configuration directory (default: ./config)
|
|
104
|
+
--help, -h Show help`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseConfig(args) {
|
|
109
|
+
let configDir = './config';
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
112
|
+
const arg = args[i];
|
|
113
|
+
|
|
114
|
+
if (arg === '--config') {
|
|
115
|
+
const value = args[i + 1];
|
|
116
|
+
if (!value || value.startsWith('-')) {
|
|
117
|
+
const error = new Error('--config requires a directory.');
|
|
118
|
+
error.hint = 'Run \"grail run --help\" or \"grail validate --help\" for usage.';
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
configDir = value;
|
|
122
|
+
i += 1;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (arg.startsWith('--config=')) {
|
|
127
|
+
const value = arg.slice('--config='.length);
|
|
128
|
+
if (!value) {
|
|
129
|
+
const error = new Error('--config requires a directory.');
|
|
130
|
+
error.hint = 'Run \"grail run --help\" or \"grail validate --help\" for usage.';
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
configDir = value;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const error = new Error(`Unknown option: ${arg}`);
|
|
138
|
+
error.hint = 'Run the command with --help for supported options.';
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return path.resolve(configDir);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function parseRunOptions(args) {
|
|
146
|
+
let configDir = './config';
|
|
147
|
+
let goal;
|
|
148
|
+
let inputs;
|
|
149
|
+
let inputsFile;
|
|
150
|
+
let output;
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
153
|
+
const arg = args[i];
|
|
154
|
+
|
|
155
|
+
if (arg === '--config') {
|
|
156
|
+
const value = args[i + 1];
|
|
157
|
+
if (!value || value.startsWith('-')) {
|
|
158
|
+
const error = new Error('--config requires a directory.');
|
|
159
|
+
error.hint = 'Run "grail run --help" for usage.';
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
configDir = value;
|
|
163
|
+
i += 1;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (arg.startsWith('--config=')) {
|
|
168
|
+
const value = arg.slice('--config='.length);
|
|
169
|
+
if (!value) {
|
|
170
|
+
const error = new Error('--config requires a directory.');
|
|
171
|
+
error.hint = 'Run "grail run --help" for usage.';
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
configDir = value;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (arg === '--goal') {
|
|
179
|
+
const value = args[i + 1];
|
|
180
|
+
if (!value || value.startsWith('-')) {
|
|
181
|
+
const error = new Error('--goal requires an effect.');
|
|
182
|
+
error.hint = 'Run "grail run --help" for usage.';
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
goal = value;
|
|
186
|
+
i += 1;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (arg.startsWith('--goal=')) {
|
|
191
|
+
const value = arg.slice('--goal='.length);
|
|
192
|
+
if (!value) {
|
|
193
|
+
const error = new Error('--goal requires an effect.');
|
|
194
|
+
error.hint = 'Run "grail run --help" for usage.';
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
goal = value;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (arg === '--inputs') {
|
|
202
|
+
const value = args[i + 1];
|
|
203
|
+
if (!value || value.startsWith('-')) {
|
|
204
|
+
const error = new Error('--inputs requires a JSON object.');
|
|
205
|
+
error.hint = 'Run "grail run --help" for usage.';
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
inputs = value;
|
|
209
|
+
i += 1;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (arg.startsWith('--inputs=')) {
|
|
214
|
+
const value = arg.slice('--inputs='.length);
|
|
215
|
+
if (!value) {
|
|
216
|
+
const error = new Error('--inputs requires a JSON object.');
|
|
217
|
+
error.hint = 'Run "grail run --help" for usage.';
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
inputs = value;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (arg === '--inputs-file') {
|
|
225
|
+
const value = args[i + 1];
|
|
226
|
+
if (!value || value.startsWith('-')) {
|
|
227
|
+
const error = new Error('--inputs-file requires a file.');
|
|
228
|
+
error.hint = 'Run "grail run --help" for usage.';
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
inputsFile = value;
|
|
232
|
+
i += 1;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (arg.startsWith('--inputs-file=')) {
|
|
237
|
+
const value = arg.slice('--inputs-file='.length);
|
|
238
|
+
if (!value) {
|
|
239
|
+
const error = new Error('--inputs-file requires a file.');
|
|
240
|
+
error.hint = 'Run "grail run --help" for usage.';
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
inputsFile = value;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (arg === '--output' || arg.startsWith('--output=')) {
|
|
248
|
+
const value = arg === '--output' ? args[++i] : arg.slice('--output='.length);
|
|
249
|
+
if (!value || value.startsWith('-')) {
|
|
250
|
+
throw new Error('--output requires summary or json.');
|
|
251
|
+
}
|
|
252
|
+
if (!['summary', 'json'].includes(value)) {
|
|
253
|
+
throw new Error(`Invalid --output value: ${value}. Expected summary or json.`);
|
|
254
|
+
}
|
|
255
|
+
output = value;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const error = new Error(`Unknown option: ${arg}`);
|
|
260
|
+
error.hint = 'Run "grail run --help" for supported options.';
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (inputs !== undefined && inputsFile !== undefined) {
|
|
265
|
+
const error = new Error('--inputs and --inputs-file cannot be used together.');
|
|
266
|
+
error.hint = 'Run "grail run --help" for usage.';
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { configDir: path.resolve(configDir), goal, inputs, inputsFile, output };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function parseInputsJson(value, source) {
|
|
274
|
+
let parsed;
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
parsed = JSON.parse(value);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
throw new Error(`Invalid JSON in ${source}: ${error.message}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (parsed === null || Array.isArray(parsed) || typeof parsed !== 'object') {
|
|
283
|
+
throw new Error(`${source} must contain a JSON object.`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return parsed;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function loadInputsFile(file) {
|
|
290
|
+
const filePath = path.resolve(file);
|
|
291
|
+
|
|
292
|
+
if (!fs.existsSync(filePath)) {
|
|
293
|
+
throw new Error(`Inputs file not found: ${filePath}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!fs.statSync(filePath).isFile()) {
|
|
297
|
+
throw new Error(`Inputs file is not a file: ${filePath}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return parseInputsJson(fs.readFileSync(filePath, 'utf8'), `inputs file ${filePath}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function writeJson(filePath, value) {
|
|
304
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function initCommand(args) {
|
|
308
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
309
|
+
showCommandHelp('init');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (args.length !== 1 || args[0].startsWith('-')) {
|
|
314
|
+
throw new Error('Usage: grail init <directory>');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const targetDir = path.resolve(args[0]);
|
|
318
|
+
|
|
319
|
+
if (fs.existsSync(targetDir)) {
|
|
320
|
+
throw new Error(`Target already exists: ${targetDir}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const configDir = path.join(targetDir, 'config');
|
|
324
|
+
const capabilitiesDir = path.join(targetDir, 'capabilities');
|
|
325
|
+
|
|
326
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
327
|
+
fs.mkdirSync(capabilitiesDir, { recursive: true });
|
|
328
|
+
|
|
329
|
+
writeJson(path.join(configDir, 'registry.json'), {
|
|
330
|
+
createGreeting: {
|
|
331
|
+
id: 'aff-create-greeting',
|
|
332
|
+
action: 'createGreeting',
|
|
333
|
+
type: 'task',
|
|
334
|
+
preconditions: [],
|
|
335
|
+
inputs: {
|
|
336
|
+
name: '$inputs.name'
|
|
337
|
+
},
|
|
338
|
+
effects: ['greetingCreated'],
|
|
339
|
+
binding: {
|
|
340
|
+
protocol: 'node',
|
|
341
|
+
module: './capabilities/hello.mjs',
|
|
342
|
+
function: 'createGreeting',
|
|
343
|
+
outputs: {
|
|
344
|
+
message: {
|
|
345
|
+
from: 'result',
|
|
346
|
+
path: 'message'
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
writeJson(path.join(configDir, 'worldstate.json'), {
|
|
354
|
+
greetingCreated: false
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
writeJson(path.join(configDir, 'inputs.json'), {
|
|
358
|
+
name: 'World'
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
writeJson(path.join(configDir, 'goal.json'), {
|
|
362
|
+
goal: 'greetingCreated'
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
fs.writeFileSync(
|
|
366
|
+
path.join(capabilitiesDir, 'hello.mjs'),
|
|
367
|
+
`export async function createGreeting({ name }) {\n return {\n message: \`Hello, \${name}!\`\n };\n}\n`,
|
|
368
|
+
'utf8'
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
console.log(`Created GRAIL world: ${args[0]}\n\n config/registry.json\n config/worldstate.json\n config/inputs.json\n config/goal.json\n capabilities/hello.mjs\n\nNext:\n\n cd ${args[0]}\n grail validate\n grail run`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function formatSummary(result) {
|
|
375
|
+
const lines = [
|
|
376
|
+
`Goal: ${result.goal}`,
|
|
377
|
+
`Result: ${result.reached ? 'SUCCESS' : 'FAIL'}`,
|
|
378
|
+
`Capability invocations: ${result.observations.length}`
|
|
379
|
+
];
|
|
380
|
+
for (const observation of result.observations) {
|
|
381
|
+
lines.push('', `${observation.invocation?.affordance ?? 'Unknown affordance'}: ${observation.result}`);
|
|
382
|
+
for (const [key, value] of Object.entries(observation.outputs ?? {})) {
|
|
383
|
+
lines.push(` ${key}: ${typeof value === 'string' ? value : JSON.stringify(value)}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return lines.join('\n');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function runCommand(args) {
|
|
390
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
391
|
+
showCommandHelp('run');
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const { Grail, loadEnvironment } = await import('../index.js');
|
|
396
|
+
const {
|
|
397
|
+
configDir,
|
|
398
|
+
goal: goalOverride,
|
|
399
|
+
inputs: inputsOverride,
|
|
400
|
+
inputsFile,
|
|
401
|
+
output
|
|
402
|
+
} = parseRunOptions(args);
|
|
403
|
+
const environment = loadEnvironment(configDir);
|
|
404
|
+
const goal = goalOverride ?? environment.goal;
|
|
405
|
+
const inputs = inputsOverride !== undefined
|
|
406
|
+
? parseInputsJson(inputsOverride, '--inputs')
|
|
407
|
+
: inputsFile !== undefined
|
|
408
|
+
? loadInputsFile(inputsFile)
|
|
409
|
+
: environment.inputs;
|
|
410
|
+
|
|
411
|
+
const grail = new Grail({
|
|
412
|
+
registry: environment.registry,
|
|
413
|
+
worldstate: environment.worldstate,
|
|
414
|
+
inputs,
|
|
415
|
+
baseDir: environment.baseDir,
|
|
416
|
+
observationPath: path.join(configDir, 'observations.json')
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
// Preserve the historical trace unless an explicit result format was requested.
|
|
421
|
+
// Restore logging even when pursuit throws; structured stdout must remain clean.
|
|
422
|
+
const originalLog = console.log;
|
|
423
|
+
let result;
|
|
424
|
+
try {
|
|
425
|
+
if (output) console.log = () => {};
|
|
426
|
+
result = await grail.pursue(goal);
|
|
427
|
+
} finally {
|
|
428
|
+
console.log = originalLog;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (output === 'json') {
|
|
432
|
+
console.log(JSON.stringify(result, null, 2));
|
|
433
|
+
} else if (output === 'summary') {
|
|
434
|
+
console.log(formatSummary(result));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (!result.reached) {
|
|
438
|
+
const failedObservation = [...result.observations]
|
|
439
|
+
.reverse()
|
|
440
|
+
.find(observation => observation.result === 'FAIL');
|
|
441
|
+
|
|
442
|
+
if (failedObservation) {
|
|
443
|
+
const detail =
|
|
444
|
+
failedObservation.response?.error ||
|
|
445
|
+
failedObservation.response?.stderr ||
|
|
446
|
+
'a capability invocation failed';
|
|
447
|
+
|
|
448
|
+
console.error(
|
|
449
|
+
`GRAIL: Execution failed while pursuing goal \"${goal}\": ${detail}`
|
|
450
|
+
);
|
|
451
|
+
} else {
|
|
452
|
+
console.error(
|
|
453
|
+
`GRAIL: Pursuit failed: goal \"${goal}\" cannot be resolved with the available capabilities.`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
process.exitCode = 1;
|
|
458
|
+
}
|
|
459
|
+
} catch (error) {
|
|
460
|
+
error.exitCode = 1;
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function showCommand(args) {
|
|
466
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
467
|
+
showCommandHelp('show');
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const targets = new Set(['registry', 'worldstate', 'inputs', 'goal']);
|
|
472
|
+
let target;
|
|
473
|
+
const configArgs = [];
|
|
474
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
475
|
+
const arg = args[i];
|
|
476
|
+
if (arg === '--config') {
|
|
477
|
+
configArgs.push(arg);
|
|
478
|
+
if (i + 1 < args.length) configArgs.push(args[++i]);
|
|
479
|
+
} else if (arg.startsWith('--config=')) {
|
|
480
|
+
configArgs.push(arg);
|
|
481
|
+
} else if (!arg.startsWith('-') && target === undefined) {
|
|
482
|
+
target = arg;
|
|
483
|
+
} else {
|
|
484
|
+
const error = new Error(`Unknown option or extra argument: ${arg}`);
|
|
485
|
+
error.hint = 'Run "grail show --help" for usage.';
|
|
486
|
+
throw error;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (target !== undefined && !targets.has(target)) {
|
|
491
|
+
const error = new Error(`Unknown show target: ${target}`);
|
|
492
|
+
error.hint = 'Choose registry, worldstate, inputs, or goal.';
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const { loadEnvironment } = await import('../index.js');
|
|
497
|
+
const environment = loadEnvironment(parseConfig(configArgs));
|
|
498
|
+
const documents = {
|
|
499
|
+
registry: environment.registry,
|
|
500
|
+
worldstate: environment.worldstate,
|
|
501
|
+
inputs: environment.inputs,
|
|
502
|
+
goal: { goal: environment.goal }
|
|
503
|
+
};
|
|
504
|
+
console.log(JSON.stringify(target ? documents[target] : documents, null, 2));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function validateCommand(args) {
|
|
508
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
509
|
+
showCommandHelp('validate');
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const { loadEnvironment } = await import('../index.js');
|
|
514
|
+
const configDir = parseConfig(args);
|
|
515
|
+
loadEnvironment(configDir);
|
|
516
|
+
console.log(`VALID: ${configDir}`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async function main() {
|
|
520
|
+
const args = process.argv.slice(2);
|
|
521
|
+
|
|
522
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
523
|
+
showHelp();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (args[0] === '--version' || args[0] === '-v') {
|
|
528
|
+
console.log(packageJson.version);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const [command, ...commandArgs] = args;
|
|
533
|
+
|
|
534
|
+
if (command === 'init') {
|
|
535
|
+
initCommand(commandArgs);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (command === 'run') {
|
|
540
|
+
await runCommand(commandArgs);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (command === 'show') {
|
|
545
|
+
await showCommand(commandArgs);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (command === 'validate') {
|
|
550
|
+
await validateCommand(commandArgs);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const error = new Error(`Unknown command: ${command}`);
|
|
555
|
+
error.hint = 'Run \"grail --help\" for available commands.';
|
|
556
|
+
throw error;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
main().catch(error => {
|
|
560
|
+
console.error(`GRAIL: ${error.message}`);
|
|
561
|
+
if (error.hint) {
|
|
562
|
+
console.error(error.hint);
|
|
563
|
+
}
|
|
564
|
+
process.exitCode = error.exitCode ?? 2;
|
|
565
|
+
});
|
package/client.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// client.js
|
|
2
|
+
export class Client {
|
|
3
|
+
constructor(server, inputs) {
|
|
4
|
+
this.server = server;
|
|
5
|
+
this.inputs = inputs;
|
|
6
|
+
this.stack = [];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async pursue(goal) {
|
|
10
|
+
// Each pursuit has its own traversal stack. Worldstate and observations persist.
|
|
11
|
+
this.stack = [];
|
|
12
|
+
console.log(`\n[CLIENT] Starting pursuit: ${goal}`);
|
|
13
|
+
|
|
14
|
+
while (true) {
|
|
15
|
+
|
|
16
|
+
// If there is no current work, resolve the goal.
|
|
17
|
+
if (this.stack.length === 0) {
|
|
18
|
+
const response = this.server.resolveGoal(goal);
|
|
19
|
+
|
|
20
|
+
if (response.result === "SUCCESS") {
|
|
21
|
+
console.log(`[CLIENT] Goal reached: ${goal}`);
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (response.offeredAffordances.length > 0) {
|
|
26
|
+
const next = response.offeredAffordances[0];
|
|
27
|
+
|
|
28
|
+
console.log(
|
|
29
|
+
`[CLIENT] Pushing goal affordance: ${next.action}`
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
this.stack.push(next);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(
|
|
37
|
+
`[CLIENT] Cannot proceed: goal is unresolvable - ${goal}`
|
|
38
|
+
);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const current = this.stack[this.stack.length - 1];
|
|
43
|
+
const response = await this.server.attempt(current, this.inputs);
|
|
44
|
+
|
|
45
|
+
if (response.result === "SUCCESS") {
|
|
46
|
+
console.log(`[CLIENT] Affordance succeeded: ${current.action}`);
|
|
47
|
+
this.stack.pop();
|
|
48
|
+
|
|
49
|
+
} else if (
|
|
50
|
+
response.result === "BLOCKED" &&
|
|
51
|
+
response.offeredAffordances.length > 0
|
|
52
|
+
) {
|
|
53
|
+
const next = response.offeredAffordances[0];
|
|
54
|
+
|
|
55
|
+
console.log(`[CLIENT] Pushing next affordance: ${next.action}`);
|
|
56
|
+
this.stack.push(next);
|
|
57
|
+
|
|
58
|
+
} else if (response.result === "FAIL") {
|
|
59
|
+
console.log(`[CLIENT] Affordance failed: ${current.action}`);
|
|
60
|
+
this.stack.pop();
|
|
61
|
+
|
|
62
|
+
} else if (
|
|
63
|
+
response.result === "BLOCKED" &&
|
|
64
|
+
response.condition
|
|
65
|
+
) {
|
|
66
|
+
console.log(
|
|
67
|
+
`[CLIENT] Cannot proceed: condition is unresolvable - ${response.condition}`
|
|
68
|
+
);
|
|
69
|
+
break;
|
|
70
|
+
|
|
71
|
+
} else {
|
|
72
|
+
console.log(`[CLIENT] Cannot proceed: ${response.result}`);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadAndValidateJSON } from '../utils/loadJSON.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Load and validate a GRAIL environment from a configuration directory.
|
|
7
|
+
*
|
|
8
|
+
* Filesystem loading belongs outside the runtime itself. The returned object
|
|
9
|
+
* can be passed directly to Grail; the goal is returned alongside the runtime
|
|
10
|
+
* configuration for the caller to pursue explicitly.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} configDir - Directory containing the GRAIL JSON files.
|
|
13
|
+
* @returns {{registry: object, worldstate: object, inputs: object, goal: string, baseDir: string}}
|
|
14
|
+
*/
|
|
15
|
+
export function loadEnvironment(configDir) {
|
|
16
|
+
if (!configDir) {
|
|
17
|
+
throw new Error('Configuration error: no configuration directory was provided.');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const resolvedConfigDir = path.resolve(configDir);
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(resolvedConfigDir)) {
|
|
23
|
+
throw new Error(`Configuration error: directory not found: ${resolvedConfigDir}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!fs.statSync(resolvedConfigDir).isDirectory()) {
|
|
27
|
+
throw new Error(`Configuration error: not a directory: ${resolvedConfigDir}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const loadConfig = (filename, schemaName) => {
|
|
31
|
+
const filePath = path.join(resolvedConfigDir, filename);
|
|
32
|
+
|
|
33
|
+
if (!fs.existsSync(filePath)) {
|
|
34
|
+
throw new Error(`Configuration error: ${filename} not found in ${resolvedConfigDir}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
return loadAndValidateJSON(filePath, schemaName, { silent: true });
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error instanceof SyntaxError) {
|
|
41
|
+
throw new Error(`Configuration error: invalid JSON in ${filename}: ${error.message}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (error.validationErrors) {
|
|
45
|
+
const details = error.validationErrors
|
|
46
|
+
.map(validationError => {
|
|
47
|
+
const location = validationError.instancePath || '/';
|
|
48
|
+
return `${location} ${validationError.message}`;
|
|
49
|
+
})
|
|
50
|
+
.join('; ');
|
|
51
|
+
|
|
52
|
+
throw new Error(`Configuration error: ${filename} failed schema validation: ${details}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const inputs = loadConfig('inputs.json', 'inputs.schema.json');
|
|
60
|
+
const registry = loadConfig('registry.json', 'registry.schema.json');
|
|
61
|
+
const worldstate = loadConfig('worldstate.json', 'worldstate.schema.json');
|
|
62
|
+
const goalObj = loadConfig('goal.json', 'goal.schema.json');
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
registry,
|
|
66
|
+
worldstate,
|
|
67
|
+
inputs,
|
|
68
|
+
goal: goalObj.goal,
|
|
69
|
+
baseDir: path.dirname(resolvedConfigDir)
|
|
70
|
+
};
|
|
71
|
+
}
|