@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.
Files changed (59) hide show
  1. package/LICENSE +208 -21
  2. package/PROMISES.md +52 -0
  3. package/README.md +294 -146
  4. package/SECURITY-MCP.md +117 -0
  5. package/SECURITY-SECRETS.md +108 -0
  6. package/STYLE.md +23 -0
  7. package/bin/jos +139 -67
  8. package/bin/jos.cmd +5 -0
  9. package/examples/01-hello.jos +44 -0
  10. package/examples/02-criteria.jos +54 -0
  11. package/examples/03-flow-gate.jos +83 -0
  12. package/examples/04-use-template/README.md +22 -0
  13. package/examples/04-use-template/lib/greeting.jos +56 -0
  14. package/examples/04-use-template/use.jos +64 -0
  15. package/examples/05-sealed/.jos/manifest.jos +53 -0
  16. package/examples/05-sealed/README.md +25 -0
  17. package/examples/06-requires.jos +42 -0
  18. package/examples/07-sandbox.jos +39 -0
  19. package/examples/08-ids-ledger-meter.md +26 -0
  20. package/examples/10-ollama.jos +47 -0
  21. package/examples/11-openai.jos +47 -0
  22. package/examples/12-gemini.jos +47 -0
  23. package/examples/13-anthropic.jos +47 -0
  24. package/examples/14-serve.jos +88 -0
  25. package/examples/README.md +105 -0
  26. package/examples/hello.jos +47 -0
  27. package/package.json +31 -9
  28. package/promises.json +894 -0
  29. package/src/args.js +47 -0
  30. package/src/commands/_frozen/add.README.md +1 -0
  31. package/src/commands/_frozen/get.README.md +1 -0
  32. package/src/commands/_frozen/llm.README.md +1 -0
  33. package/src/commands/_frozen/prompts.README.md +1 -0
  34. package/src/commands/_frozen/repo.README.md +1 -0
  35. package/src/commands/_frozen/serve-explorer.README.md +1 -0
  36. package/src/commands/ids.js +101 -0
  37. package/src/commands/init.js +113 -147
  38. package/src/commands/ledger.js +94 -0
  39. package/src/commands/meter.js +96 -0
  40. package/src/commands/migrate.js +214 -0
  41. package/src/commands/overview.js +118 -0
  42. package/src/commands/run.js +164 -214
  43. package/src/commands/runs.js +102 -0
  44. package/src/commands/secrets.js +695 -115
  45. package/src/commands/start.js +210 -0
  46. package/src/commands/validate.js +153 -165
  47. package/src/commands/verify.js +56 -0
  48. package/src/commands/version.js +23 -0
  49. package/src/intention.js +114 -0
  50. package/src/kernel.js +69 -0
  51. package/src/ledgers.js +57 -0
  52. package/src/mcp-guide.js +138 -0
  53. package/src/mcp.js +734 -0
  54. package/src/serve.js +38 -770
  55. package/src/ui.js +98 -0
  56. package/src/commands/add.js +0 -145
  57. package/src/commands/get.js +0 -245
  58. package/src/commands/repo.js +0 -139
  59. package/src/index.js +0 -0
@@ -1,244 +1,194 @@
1
1
  /**
2
- * JOS RUN Command - Execute .jos artifacts
3
- * Complies with JOSFOXAI MAGIC contract
4
- * Integrity Strategy: Detached Manifest (.sig.json)
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 { execSync, spawn } = require('child_process');
11
- const crypto = require('crypto');
12
-
13
- // AURORA colors
14
- const C = {
15
- reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
16
- purple: '\x1b[38;5;135m', cyan: '\x1b[38;5;51m',
17
- green: '\x1b[38;5;78m', red: '\x1b[38;5;196m',
18
- yellow: '\x1b[38;5;220m', white: '\x1b[38;5;255m',
19
- gray: '\x1b[38;5;245m', blue: '\x1b[38;5;39m'
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
- // Witness Logging
23
- function logWitness(home, eventType, data) {
24
- const runId = global.JOS_RUN_ID || new Date().toISOString().replace(/[:.]/g, '-');
25
- global.JOS_RUN_ID = runId;
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
- // Compute SHA-256 integrity
40
- function computeIntegrity(content) {
41
- return crypto.createHash('sha256').update(content).digest('hex');
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
- // Verify Integrity using Detached Manifest
45
- function verifyIntegrity(artifactPath, content, artifact) {
46
- const integrityRef = artifact.security?.integrity_ref;
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
- const manifestPath = path.resolve(path.dirname(artifactPath), integrityRef);
50
- if (!fs.existsSync(manifestPath)) {
51
- return { valid: false, error: `Manifest not found at ${manifestPath}` };
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
- let manifest;
55
- try {
56
- manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
57
- } catch (e) {
58
- return { valid: false, error: "Invalid manifest JSON" };
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
- const actualHash = computeIntegrity(content);
62
- if (actualHash !== manifest.artifact_sha256) {
63
- return {
64
- valid: false,
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
- return { valid: true, verified: true, hash: actualHash };
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
- let artifactPath = path.resolve(target);
98
- if (!fs.existsSync(artifactPath)) {
99
- if (fs.existsSync(path.join(home, 'artifacts', target))) {
100
- artifactPath = path.join(home, 'artifacts', target);
101
- } else {
102
- console.log(`${C.red}✖ Artifact not found:${C.reset} ${target}`);
103
- process.exit(1);
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
- const content = fs.readFileSync(artifactPath, 'utf8');
108
- const artifact = JSON.parse(content);
109
-
110
- // Init Witness
111
- logWitness(home, 'start', { artifact: artifactPath, dryRun: dryRun });
112
-
113
- // Header
114
- console.log(`\n${C.purple}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}`);
115
- console.log(`${C.cyan}${C.bold}JOS RUN${C.reset} // ${C.gray}JOSFOXAI MAGIC Runtime${C.reset}`);
116
- console.log(`${C.purple}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}\n`);
117
-
118
- // Mode indicator
119
- if (dryRun) {
120
- console.log(`${C.yellow}${C.bold}⚡ DRY RUN MODE${C.reset} ${C.gray}— No commands will be executed${C.reset}\n`);
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
- // Artifact info
124
- console.log(`${C.cyan}📦 Artifact:${C.reset} ${artifact.meta?.name || path.basename(artifactPath)}`);
125
- console.log(`${C.cyan}📄 File:${C.reset} ${path.basename(artifactPath)}`);
126
- console.log(`${C.cyan}🆔 ID:${C.reset} ${artifact.id_jos || 'N/A'}`);
127
-
128
- // Integrity verification
129
- if (!insecure) {
130
- const integrity = verifyIntegrity(artifactPath, content, artifact);
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
- // MAGIC validation
146
- const hasMagic = artifact.intention && artifact.guardrails && artifact.capabilities;
147
- console.log(`${C.cyan}✨ MAGIC:${C.reset} ${hasMagic ? C.green + '✓ Valid' : C.yellow + '⚠ Partial'}${C.reset}`);
148
-
149
- // Intention
150
- if (artifact.intention?.objective) {
151
- console.log(`${C.cyan}🎯 Intention:${C.reset} ${artifact.intention.objective}`);
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
- // Guardrails
155
- if (artifact.guardrails?.avoid?.length > 0) {
156
- console.log(`${C.cyan}🛡️ Guardrails:${C.reset} ${artifact.guardrails.avoid.join(', ')}`);
150
+ if (json) {
151
+ ui.json(process.stdout, result);
152
+ return result.code;
157
153
  }
158
154
 
159
- // Flow selection
160
- const flowName = args.includes('--flow') ? args[args.indexOf('--flow') + 1] : 'main';
161
- const flow = artifact.orchestration?.flows?.[flowName] ||
162
- artifact.orchestration?.pipelines?.[flowName];
163
-
164
- if (!flow) {
165
- console.log(`\n${C.red}✖ Flow '${flowName}' not found.${C.reset}`);
166
- const available = [
167
- ...Object.keys(artifact.orchestration?.flows || {}),
168
- ...Object.keys(artifact.orchestration?.pipelines || {})
169
- ];
170
- if (available.length > 0) {
171
- console.log(`${C.gray}Available flows: ${available.join(', ')}${C.reset}`);
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
- // Execution plan
177
- console.log(`\n${C.cyan}▶ Flow:${C.reset} ${C.bold}${flowName}${C.reset}`);
178
- if (flow.description) {
179
- console.log(`${C.gray} ${flow.description}${C.reset}`);
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
- console.log(`${C.gray} Steps: ${flow.steps?.join(' → ') || 'none'}${C.reset}\n`);
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
- // Real execution
213
- console.log(`${C.blue} [${stepIndex}] ${typeIcon} ${stepName}...${C.reset}`);
214
-
215
- try {
216
- if (def.type === 'shell') {
217
- execSync(def.command, { stdio: 'inherit', shell: '/bin/bash' });
218
- console.log(`${C.green} ✓ Complete${C.reset}`);
219
- } else if (def.type === 'jos') {
220
- console.log(`${C.purple} Running sub-artifact: ${def.artifact}${C.reset}`);
221
- const subPath = path.resolve(path.dirname(artifactPath), def.artifact);
222
- const binJos = process.argv[1];
223
- execSync(`${process.execPath} "${binJos}" run "${subPath}" ${insecure ? '--insecure' : ''} --flow main`, {
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
- // Success
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
+ };