@josfox/jos 4.0.8 → 5.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/LICENSE +208 -21
- package/PROMISES.md +52 -0
- package/README.md +294 -146
- package/SECURITY-MCP.md +117 -0
- package/SECURITY-SECRETS.md +108 -0
- package/STYLE.md +23 -0
- package/bin/jos +139 -67
- package/bin/jos.cmd +5 -0
- package/examples/01-hello.jos +44 -0
- package/examples/02-criteria.jos +54 -0
- package/examples/03-flow-gate.jos +83 -0
- package/examples/04-use-template/README.md +22 -0
- package/examples/04-use-template/lib/greeting.jos +56 -0
- package/examples/04-use-template/use.jos +64 -0
- package/examples/05-sealed/.jos/manifest.jos +53 -0
- package/examples/05-sealed/README.md +25 -0
- package/examples/06-requires.jos +42 -0
- package/examples/07-sandbox.jos +39 -0
- package/examples/08-ids-ledger-meter.md +26 -0
- package/examples/10-ollama.jos +47 -0
- package/examples/11-openai.jos +47 -0
- package/examples/12-gemini.jos +47 -0
- package/examples/13-anthropic.jos +47 -0
- package/examples/14-serve.jos +88 -0
- package/examples/README.md +105 -0
- package/examples/hello.jos +47 -0
- package/package.json +31 -9
- package/promises.json +894 -0
- package/src/args.js +47 -0
- package/src/commands/_frozen/add.README.md +1 -0
- package/src/commands/_frozen/get.README.md +1 -0
- package/src/commands/_frozen/llm.README.md +1 -0
- package/src/commands/_frozen/prompts.README.md +1 -0
- package/src/commands/_frozen/repo.README.md +1 -0
- package/src/commands/_frozen/serve-explorer.README.md +1 -0
- package/src/commands/ids.js +101 -0
- package/src/commands/init.js +113 -147
- package/src/commands/ledger.js +94 -0
- package/src/commands/meter.js +96 -0
- package/src/commands/migrate.js +214 -0
- package/src/commands/overview.js +118 -0
- package/src/commands/run.js +164 -214
- package/src/commands/runs.js +102 -0
- package/src/commands/secrets.js +695 -115
- package/src/commands/start.js +210 -0
- package/src/commands/validate.js +153 -165
- package/src/commands/verify.js +56 -0
- package/src/commands/version.js +23 -0
- package/src/intention.js +114 -0
- package/src/kernel.js +69 -0
- package/src/ledgers.js +57 -0
- package/src/mcp-guide.js +138 -0
- package/src/mcp.js +734 -0
- package/src/serve.js +38 -770
- package/src/ui.js +98 -0
- package/src/commands/add.js +0 -145
- package/src/commands/get.js +0 -245
- package/src/commands/repo.js +0 -139
- package/src/index.js +0 -0
package/src/commands/run.js
CHANGED
|
@@ -1,244 +1,194 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JOS RUN
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Format version v0.0.7 — Specification maturity v0.1.0 (Alpha)
|
|
2
|
+
* JOS RUN - Execute a .jos document through the jos-core kernel
|
|
3
|
+
* Accepts 2.0.0, 1.1.0, 1.0.0 and 0.0.7 documents (jos-core normalizes them).
|
|
4
|
+
* Exit codes: SPEC-2.0 section 7.1; 64 for command-line usage errors.
|
|
6
5
|
*/
|
|
7
|
-
|
|
8
|
-
const fs = require('fs');
|
|
6
|
+
'use strict';
|
|
9
7
|
const path = require('path');
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
8
|
+
const kernel = require('../kernel');
|
|
9
|
+
const ui = require('../ui');
|
|
10
|
+
const { RULE } = ui;
|
|
11
|
+
const { parse, problem } = require('../args');
|
|
12
|
+
const intention = require('../intention');
|
|
13
|
+
|
|
14
|
+
// The options of `jos run`; each one passed to jos-core as is (SPEC-2.0 §5: flows, `use`, gates, data flow).
|
|
15
|
+
const OPTIONS = {
|
|
16
|
+
'--json': false, '--dry-run': false,
|
|
17
|
+
'--flow': true, '--library': true, '--approve': 'many', '--input': true, '--with': true,
|
|
20
18
|
};
|
|
19
|
+
const USAGE = 'usage: jos run <file.jos | folder> [--json] [--dry-run] [--flow <name>] [--library <dir>] [--approve <step>]... [--input <json>] [--with <json>]';
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const runDir = path.join(home, 'runs', runId);
|
|
28
|
-
if (!fs.existsSync(runDir)) fs.mkdirSync(runDir, { recursive: true });
|
|
29
|
-
|
|
30
|
-
const event = {
|
|
31
|
-
timestamp: new Date().toISOString(),
|
|
32
|
-
type: `run:${eventType}`,
|
|
33
|
-
...data
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
fs.appendFileSync(path.join(runDir, 'events.jsonl'), JSON.stringify(event) + '\n');
|
|
37
|
-
}
|
|
21
|
+
const CODE_MEANING = {
|
|
22
|
+
0: 'done', 1: 'not done', 2: 'invalid document', 3: 'blocked', 4: 'budget exceeded',
|
|
23
|
+
5: 'unexecutable', 124: 'timeout', 137: 'output limit or kill',
|
|
24
|
+
};
|
|
38
25
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
26
|
+
const HELP = (C) => `
|
|
27
|
+
${C.purple}${RULE}${C.reset}
|
|
28
|
+
${C.bold}JOS RUN${C.reset} // Execute .jos documents with jos-core
|
|
29
|
+
${C.purple}${RULE}${C.reset}
|
|
43
30
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (!integrityRef) return { valid: true, warning: "open (no integrity_ref)" };
|
|
31
|
+
${C.bold}Usage:${C.reset}
|
|
32
|
+
jos run <file.jos | folder> [options]
|
|
33
|
+
jos run [options] -- <file.jos | folder> (-- ends the options)
|
|
48
34
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
35
|
+
A .jos document runs in its own folder, not in the current directory.
|
|
36
|
+
A folder D with a .jos/ directory (D or D/.jos) runs D/.jos/manifest.jos in D;
|
|
37
|
+
a sealed folder is verified by its directory hash (SPEC-2.0 9.3).
|
|
38
|
+
A 0.0.7 document sealed by security.integrity_ref is verified against its
|
|
39
|
+
manifest before anything starts, --dry-run too: a mismatch is code 3.
|
|
53
40
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
41
|
+
${C.bold}Options:${C.reset}
|
|
42
|
+
--json Print the result object (SPEC-2.0 7.4) as JSON
|
|
43
|
+
--dry-run All checks, no launch; prints the plan
|
|
44
|
+
--flow <name> Run this flow of orchestration.flows (default: main, or the only one)
|
|
45
|
+
--library <dir> Also resolve \`use\` references in this folder (after the .jos/ chain)
|
|
46
|
+
--approve <step> Approve the human gate of <step> (repeat for several; nested: outer/step,
|
|
47
|
+
second occurrence: step#2); without it the flow stops there, code 1
|
|
48
|
+
--input <json> The run's input: \`input\` and \`last\` in the flow, a step's stdin
|
|
49
|
+
--with <json> Slot values for a template run directly (SPEC-2.0 5.7)
|
|
50
|
+
|
|
51
|
+
${C.bold}Files written:${C.reset}
|
|
52
|
+
jos-core keeps a ledger of each run (not of --dry-run): <run>/ledger.jsonl in .jos/replays/
|
|
53
|
+
of the nearest .jos/ folder with a manifest.jos above the document (or ~/.jos, if it has
|
|
54
|
+
one and there is no other); otherwise in ~/.jos/runs/.
|
|
55
|
+
|
|
56
|
+
After a run: jos runs, jos ledger verify <run_id>, jos meter <run_id>.
|
|
57
|
+
|
|
58
|
+
${C.bold}Output:${C.reset}
|
|
59
|
+
Before it runs: the document's intention (objective, "Done when" criteria, avoid,
|
|
60
|
+
limit). After: each receipt with the verdict of each criterion, as jos-core
|
|
61
|
+
evaluated it, and "Intention met" or "Intention NOT met (code N: ...)".
|
|
62
|
+
|
|
63
|
+
${C.bold}Exit codes:${C.reset}
|
|
64
|
+
0 done, 1 not done, 2 invalid, 3 blocked, 4 budget, 5 unexecutable,
|
|
65
|
+
124 timeout, 137 output limit, 64 usage
|
|
66
|
+
|
|
67
|
+
${C.bold}Examples:${C.reset}
|
|
68
|
+
jos run hello.jos --dry-run
|
|
69
|
+
jos run ./my-project --json
|
|
70
|
+
jos run release.jos --approve build
|
|
71
|
+
jos run greet.jos --with '{"name":"Ana"}'
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
exports.execute = async (args) => {
|
|
75
|
+
const err = ui.printer(process.stderr);
|
|
76
|
+
const { C } = err;
|
|
77
|
+
const { opts, files, help, error } = parse(args, OPTIONS);
|
|
78
|
+
if (help) {
|
|
79
|
+
const out = ui.printer(process.stdout);
|
|
80
|
+
out.text(HELP(out.C));
|
|
81
|
+
return 0;
|
|
59
82
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
error: `Hash mismatch. Expected: ${manifest.artifact_sha256.substring(0, 16)}..., Got: ${actualHash.substring(0, 16)}...`
|
|
66
|
-
};
|
|
83
|
+
if (error) {
|
|
84
|
+
err.fail('jos run: ' + problem(error));
|
|
85
|
+
if (error.option === '--insecure') err.hint('(removed in 5.0.0: jos-core checks integrity itself)');
|
|
86
|
+
err.hint(USAGE);
|
|
87
|
+
return 64;
|
|
67
88
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
exports.execute = async (args, home) => {
|
|
73
|
-
const target = args[0];
|
|
74
|
-
const dryRun = args.includes('--dry-run');
|
|
75
|
-
const insecure = args.includes('--insecure');
|
|
76
|
-
|
|
77
|
-
if (args.includes('--help') || !target) {
|
|
78
|
-
console.log(`
|
|
79
|
-
${C.purple}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}
|
|
80
|
-
${C.bold}JOS RUN${C.reset} // Execute .jos artifacts
|
|
81
|
-
${C.purple}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}
|
|
82
|
-
|
|
83
|
-
${C.bold}Usage:${C.reset}
|
|
84
|
-
jos run <file.jos> [options]
|
|
85
|
-
|
|
86
|
-
${C.bold}Options:${C.reset}
|
|
87
|
-
--flow <name> Flow to execute (default: main)
|
|
88
|
-
--dry-run Simulate without executing
|
|
89
|
-
--insecure Skip integrity verification
|
|
90
|
-
|
|
91
|
-
${C.bold}Example:${C.reset}
|
|
92
|
-
jos run orchestration.jos --flow publish_all --dry-run
|
|
93
|
-
`);
|
|
94
|
-
return;
|
|
89
|
+
if (files.length !== 1) {
|
|
90
|
+
err.fail('jos run: takes exactly one file');
|
|
91
|
+
err.hint(USAGE);
|
|
92
|
+
return 64;
|
|
95
93
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
94
|
+
const json = opts['--json'] === true;
|
|
95
|
+
const dryRun = opts['--dry-run'] === true;
|
|
96
|
+
const runOpts = {};
|
|
97
|
+
if (opts['--flow'] !== undefined) runOpts.flow = opts['--flow'];
|
|
98
|
+
if (opts['--library'] !== undefined) runOpts.library = path.resolve(opts['--library']);
|
|
99
|
+
if (opts['--approve'] !== undefined) runOpts.approvals = opts['--approve'];
|
|
100
|
+
for (const [flag, key] of [['--input', 'input'], ['--with', 'with']]) {
|
|
101
|
+
if (opts[flag] === undefined) continue;
|
|
102
|
+
try { runOpts[key] = JSON.parse(opts[flag]); } catch (e) {
|
|
103
|
+
err.fail('jos run: ' + flag + ' takes JSON', '(' + e.message + ')');
|
|
104
|
+
err.hint("example: " + flag + " '{\"name\":\"Ana\"}'");
|
|
105
|
+
return 64;
|
|
104
106
|
}
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
109
|
+
let k;
|
|
110
|
+
let core;
|
|
111
|
+
try { k = kernel.locate(); core = kernel.need(k, ['loadFileBytes']); } catch (e) { return kernel.fail(e); }
|
|
112
|
+
// SPEC-2.0 §9.1: a folder D (or D/.jos) runs its D/.jos/manifest.jos, in D; older kernels without documentPath get the path as given
|
|
113
|
+
const given = path.resolve(files[0]);
|
|
114
|
+
const file = typeof core.documentPath === 'function' ? core.documentPath(given) : given;
|
|
115
|
+
const base = path.basename(file) === 'manifest.jos' && path.basename(path.dirname(file)) === '.jos' ? path.dirname(path.dirname(file)) : path.dirname(file);
|
|
116
|
+
|
|
117
|
+
// The file is read once: the kernel hashes the same octets it parses (a 0.0.7 document sealed by
|
|
118
|
+
// security.integrity_ref is verified against them, jos-core 2.0.0-rc.8), so they travel with the document.
|
|
119
|
+
let doc = null;
|
|
120
|
+
let source = null;
|
|
121
|
+
let loadErrors = null;
|
|
122
|
+
try { ({ doc, bytes: source } = core.loadFileBytes(file)); } catch (e) {
|
|
123
|
+
if (!(e instanceof core.JosError)) throw e;
|
|
124
|
+
loadErrors = e.errors;
|
|
121
125
|
}
|
|
122
126
|
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (!integrity.valid) {
|
|
132
|
-
console.log(`${C.cyan}🔐 Integrity:${C.reset} ${C.red}FAILED - ${integrity.error}${C.reset}`);
|
|
133
|
-
logWitness(home, 'integrity_failed', { error: integrity.error });
|
|
134
|
-
process.exit(1);
|
|
135
|
-
} else if (integrity.verified) {
|
|
136
|
-
console.log(`${C.cyan}🔐 Integrity:${C.reset} ${C.green}✓ Verified${C.reset} ${C.gray}(${integrity.hash.substring(0, 16)}...)${C.reset}`);
|
|
137
|
-
logWitness(home, 'integrity_verified', { hash: integrity.hash });
|
|
138
|
-
} else {
|
|
139
|
-
console.log(`${C.cyan}🔐 Integrity:${C.reset} ${C.yellow}⚠ ${integrity.warning}${C.reset}`);
|
|
140
|
-
}
|
|
141
|
-
} else {
|
|
142
|
-
console.log(`${C.cyan}🔐 Integrity:${C.reset} ${C.yellow}⚠ Skipped (--insecure)${C.reset}`);
|
|
127
|
+
// Text mode: the header and the intention of the document go out before it runs (a long step, a server, a gate
|
|
128
|
+
// waiting: the person sees what was asked first); the receipts and the verdict after.
|
|
129
|
+
if (!json) {
|
|
130
|
+
err.header(`${C.cyan}JOS RUN`, 'jos-core ' + k.version);
|
|
131
|
+
if (dryRun) err.warn(`${C.bold}DRY RUN MODE${C.reset}${C.yellow}: no commands will be executed`);
|
|
132
|
+
err.field('File', file === path.join(base, '.jos', 'manifest.jos') ? path.join(path.basename(base), '.jos', 'manifest.jos') : path.basename(file));
|
|
133
|
+
const v = doc === null ? null : core.validate(doc);
|
|
134
|
+
if (v !== null && v.valid) intention.intention(err, v.document);
|
|
143
135
|
}
|
|
144
136
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
137
|
+
let result;
|
|
138
|
+
if (loadErrors) {
|
|
139
|
+
result = { ok: false, code: 2, run_id: core.josid.mint('run'), artifact: null, input_version: null, capability: null, receipts: [], output: null, duration_ms: 0, errors: loadErrors };
|
|
140
|
+
if (dryRun) { result.dry_run = true; delete result.ok; delete result.receipts; delete result.output; result.plan = null; }
|
|
141
|
+
} else {
|
|
142
|
+
// dir: the document's directory, as the kernel's own CLI passes it; for D/.jos/manifest.jos it is D/.jos, which is how
|
|
143
|
+
// the kernel recognizes a directory run and verifies the directory hash (SPEC-2.0 §9.3)
|
|
144
|
+
// folder: a folder D (or D/.jos) names its manifest, which the kernel requires to be of kind directory before
|
|
145
|
+
// any step, with the code and text of `jos validate D` (SPEC-2.0 §9.1; readiness rc.7 pass 5 F-19)
|
|
146
|
+
const folder = file !== given ? { folder: true } : {};
|
|
147
|
+
result = await core.run(doc, Object.assign({ cwd: base, dir: path.dirname(file), source, env: process.env, dryRun }, folder, runOpts));
|
|
152
148
|
}
|
|
153
149
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
150
|
+
if (json) {
|
|
151
|
+
ui.json(process.stdout, result);
|
|
152
|
+
return result.code;
|
|
157
153
|
}
|
|
158
154
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
process.exit(1);
|
|
155
|
+
if (result.artifact || result.input_version || result.capability) err.line();
|
|
156
|
+
if (result.artifact) err.field('ID', result.artifact);
|
|
157
|
+
if (result.input_version) err.field('Input version', result.input_version);
|
|
158
|
+
if (result.capability) err.field('Capability', `${result.capability.cap} ${C.gray}(${result.capability.trust})${C.reset}`);
|
|
159
|
+
if (result.integrity_ref) err.field('Integrity', `${C.green}✓${C.reset} verified by ${result.integrity_ref.manifest} ${C.gray}(0.0.7 integrity_ref, sha256 ${result.integrity_ref.sha256.slice(0, 12)}…, ${result.integrity_ref.eol})${C.reset}`);
|
|
160
|
+
|
|
161
|
+
if (result.plan) {
|
|
162
|
+
const p = result.plan;
|
|
163
|
+
err.line();
|
|
164
|
+
err.line(`${C.cyan}▶ Plan:${C.reset} ${C.bold}${p.engine}${C.reset}`);
|
|
165
|
+
if (Array.isArray(p.run)) err.cmd(p.run.join(' '));
|
|
166
|
+
else if (p.run) err.cmd(p.run);
|
|
167
|
+
if (p.limits) err.hint('limits: ' + JSON.stringify(p.limits));
|
|
174
168
|
}
|
|
175
169
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
170
|
+
if (result.ledger) err.field('Ledger', result.ledger);
|
|
171
|
+
if (result.output) {
|
|
172
|
+
if (result.output.stdout || result.output.stderr) err.line();
|
|
173
|
+
ui.data(process.stdout, result.output.stdout);
|
|
174
|
+
ui.data(process.stderr, result.output.stderr);
|
|
180
175
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// Execute steps
|
|
184
|
-
let stepIndex = 0;
|
|
185
|
-
for (const stepName of flow.steps || []) {
|
|
186
|
-
stepIndex++;
|
|
187
|
-
const def = artifact.orchestration.definitions?.[stepName];
|
|
188
|
-
|
|
189
|
-
if (!def) {
|
|
190
|
-
console.log(`${C.red} [${stepIndex}] ✖ ${stepName}: Definition not found${C.reset}`);
|
|
191
|
-
logWitness(home, 'step_error', { step: stepName, error: 'definition not found' });
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
logWitness(home, 'step_start', { step: stepName, type: def.type, index: stepIndex });
|
|
196
|
-
|
|
197
|
-
const typeIcon = def.type === 'shell' ? '⚡' : def.type === 'jos' ? '📦' : '●';
|
|
198
|
-
|
|
199
|
-
if (dryRun) {
|
|
200
|
-
console.log(`${C.yellow} [${stepIndex}] ${typeIcon} ${stepName}${C.reset}`);
|
|
201
|
-
if (def.description) console.log(`${C.gray} ${def.description}${C.reset}`);
|
|
202
|
-
if (def.type === 'shell') {
|
|
203
|
-
const cmdPreview = def.command.length > 60 ? def.command.substring(0, 60) + '...' : def.command;
|
|
204
|
-
console.log(`${C.dim} $ ${cmdPreview}${C.reset}`);
|
|
205
|
-
} else if (def.type === 'jos') {
|
|
206
|
-
console.log(`${C.dim} → ${def.artifact}${C.reset}`);
|
|
207
|
-
}
|
|
208
|
-
logWitness(home, 'step_dry_run', { step: stepName });
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
176
|
+
if (!dryRun && result.receipts.length) intention.verdict(err, result);
|
|
211
177
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
stdio: 'inherit'
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
logWitness(home, 'step_complete', { step: stepName, success: true });
|
|
228
|
-
} catch (e) {
|
|
229
|
-
console.log(`${C.red} ✖ Failed: ${e.message}${C.reset}`);
|
|
230
|
-
logWitness(home, 'step_failed', { step: stepName, error: e.message });
|
|
231
|
-
logWitness(home, 'run_failed', { lastStep: stepName });
|
|
232
|
-
process.exit(1);
|
|
178
|
+
const detail = result.errors ? result.errors.join('; ') : (result.reason || result.unexecutable || '');
|
|
179
|
+
const meaning = CODE_MEANING[result.code] || 'error';
|
|
180
|
+
err.line();
|
|
181
|
+
if (result.code === 0) {
|
|
182
|
+
err.ok(`${C.bold}${dryRun ? 'Dry run complete' : 'Intention met'}`, `(code 0, ${result.run_id})`);
|
|
183
|
+
} else {
|
|
184
|
+
err.fail(`${C.bold}${dryRun ? 'Dry run refused' : 'Intention NOT met'} (code ${result.code}: ${meaning})`, `(${result.run_id})`);
|
|
185
|
+
if (detail) err.hint(detail);
|
|
186
|
+
if (result.waiting) {
|
|
187
|
+
const approvals = (runOpts.approvals || []).concat(result.waiting.step).map((a) => ' --approve ' + ui.shellArg(a)).join('');
|
|
188
|
+
const target = ui.shellArg(files[0]);
|
|
189
|
+
err.hint('To approve it: jos run ' + (files[0].startsWith('-') ? approvals.slice(1) + ' -- ' + target : target + approvals));
|
|
233
190
|
}
|
|
234
191
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
console.log(`\n${C.green}${C.bold}✓ ${dryRun ? 'Dry run complete' : 'Execution complete'}${C.reset}`);
|
|
238
|
-
if (artifact.intention?.success_criteria) {
|
|
239
|
-
console.log(`${C.gray} Success criteria: ${artifact.intention.success_criteria}${C.reset}`);
|
|
240
|
-
}
|
|
241
|
-
console.log('');
|
|
242
|
-
|
|
243
|
-
logWitness(home, 'success', { dryRun: dryRun, stepsExecuted: stepIndex });
|
|
192
|
+
err.line();
|
|
193
|
+
return result.code;
|
|
244
194
|
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JOS RUNS - lists the runs whose ledgers jos-core kept in ~/.jos/runs/ (runs of documents outside any .jos/ folder):
|
|
3
|
+
* id, document, exit code, start time and metered cost, newest first. Each ledger is verified by jos-core; one that
|
|
4
|
+
* does not verify is listed with the reason and nothing else. Read-only: it never creates ~/.jos.
|
|
5
|
+
* Exit codes: 0, 5 no kernel, 64 usage.
|
|
6
|
+
*/
|
|
7
|
+
'use strict';
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const kernel = require('../kernel');
|
|
11
|
+
const ledgers = require('../ledgers');
|
|
12
|
+
const ui = require('../ui');
|
|
13
|
+
const { RULE } = ui;
|
|
14
|
+
const { parse, problem } = require('../args');
|
|
15
|
+
|
|
16
|
+
const USAGE = 'usage: jos runs [--limit <n>] [--json]';
|
|
17
|
+
const DEFAULT_LIMIT = 20;
|
|
18
|
+
|
|
19
|
+
const HELP = (C) => `
|
|
20
|
+
${C.purple}${RULE}${C.reset}
|
|
21
|
+
${C.bold}JOS RUNS${C.reset} // The runs recorded in ~/.jos/runs
|
|
22
|
+
${C.purple}${RULE}${C.reset}
|
|
23
|
+
|
|
24
|
+
${C.bold}Usage:${C.reset}
|
|
25
|
+
jos runs [--limit <n>] [--json]
|
|
26
|
+
|
|
27
|
+
jos-core writes a ledger for every run (not --dry-run). Runs of documents inside a
|
|
28
|
+
folder with .jos/manifest.jos keep it in that folder's .jos/replays/; the others in
|
|
29
|
+
~/.jos/runs/, which this command lists, newest first. Every ledger is verified;
|
|
30
|
+
the cost is the metered cost_usd, shown only when an engine reported one.
|
|
31
|
+
|
|
32
|
+
${C.bold}Options:${C.reset}
|
|
33
|
+
--limit <n> Show at most n runs (default ${DEFAULT_LIMIT}; 0 shows all)
|
|
34
|
+
--json Print the list as JSON
|
|
35
|
+
|
|
36
|
+
${C.bold}See also:${C.reset}
|
|
37
|
+
jos ledger verify <run_id> jos meter <run_id>
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
const CODE_MEANING = {
|
|
41
|
+
0: 'done', 1: 'not done', 2: 'invalid', 3: 'blocked', 4: 'budget', 5: 'unexecutable', 124: 'timeout', 137: 'output limit',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
exports.execute = async (args, josHome) => {
|
|
45
|
+
const err = ui.printer(process.stderr);
|
|
46
|
+
const { opts, files, help, error } = parse(args, { '--json': false, '--limit': true });
|
|
47
|
+
if (help) {
|
|
48
|
+
const out = ui.printer(process.stdout);
|
|
49
|
+
out.text(HELP(out.C));
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
const limit = opts['--limit'] === undefined ? DEFAULT_LIMIT : Number(opts['--limit']);
|
|
53
|
+
if (error || files.length || !Number.isInteger(limit) || limit < 0) {
|
|
54
|
+
err.fail('jos runs: ' + (error ? problem(error) : files.length ? 'takes no arguments' : '--limit takes a whole number, 0 or more'));
|
|
55
|
+
err.hint(USAGE);
|
|
56
|
+
return 64;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let core;
|
|
60
|
+
try { core = kernel.need(kernel.locate(), ['ledger.read', 'ledger.verify', 'ledger.meter']); } catch (e) { return kernel.fail(e); }
|
|
61
|
+
const dir = path.join(josHome, 'runs');
|
|
62
|
+
let names = [];
|
|
63
|
+
try { names = fs.readdirSync(dir); } catch (e) { /* no runs yet */ }
|
|
64
|
+
const ids = names.filter((n) => ledgers.RUN_ID.test(n) && fs.existsSync(path.join(dir, n, 'ledger.jsonl'))).sort().reverse();
|
|
65
|
+
const shownIds = limit === 0 ? ids : ids.slice(0, limit);
|
|
66
|
+
const rows = shownIds.map((id) => {
|
|
67
|
+
const file = path.join(dir, id, 'ledger.jsonl');
|
|
68
|
+
const r = ledgers.read(core, file);
|
|
69
|
+
const s = r.errors ? { ledger_ok: false, reason: r.errors.join('; '), started: null, document: null, artifact: null, code: null, cost_usd: null } : ledgers.summary(core, r.lines);
|
|
70
|
+
return Object.assign({ run: id, file }, s);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (opts['--json']) {
|
|
74
|
+
ui.json(process.stdout, rows);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
const out = ui.printer(process.stdout);
|
|
78
|
+
const { C } = out;
|
|
79
|
+
out.header('JOS RUNS', dir);
|
|
80
|
+
if (!ids.length) {
|
|
81
|
+
out.line(' No runs recorded here yet.');
|
|
82
|
+
out.hint('Runs of documents outside a .jos/ folder land here. Try: jos start');
|
|
83
|
+
out.line();
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
for (const r of rows) {
|
|
87
|
+
if (!r.ledger_ok) {
|
|
88
|
+
out.warn(r.run, '(ledger does not verify: ' + r.reason + ')');
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const when = r.started === null ? '' : r.started.replace(/\.\d+Z$/, 'Z');
|
|
92
|
+
const meaning = r.code === null ? 'no code' : (CODE_MEANING[r.code] || 'error');
|
|
93
|
+
const mark = r.code === 0 ? `${C.green}✓${C.reset}` : `${C.red}✖${C.reset}`;
|
|
94
|
+
out.line(`${mark} ${C.bold}${r.run}${C.reset} ${when} code ${r.code === null ? '-' : r.code} ${C.gray}(${meaning})${C.reset}`);
|
|
95
|
+
out.hint((r.document === null ? '(no receipt)' : r.document) + (r.artifact === null ? '' : ' ' + r.artifact) + (r.cost_usd === null ? '' : ' ' + r.cost_usd + ' USD'));
|
|
96
|
+
}
|
|
97
|
+
if (shownIds.length < ids.length) out.hint('… and ' + (ids.length - shownIds.length) + ' more (--limit 0 shows all)');
|
|
98
|
+
out.line();
|
|
99
|
+
out.hint('Next: jos ledger verify <run_id> jos meter <run_id>');
|
|
100
|
+
out.line();
|
|
101
|
+
return 0;
|
|
102
|
+
};
|