agents-handoff 0.0.0-stage → 2.0.2

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 (54) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/LICENSE +21 -0
  3. package/README.md +110 -2
  4. package/SKILL.md +147 -0
  5. package/capability-registry.json +27 -0
  6. package/docs/ARCHITECTURE.md +164 -0
  7. package/docs/CHANGELOG.md +151 -0
  8. package/docs/CLI.md +196 -0
  9. package/docs/COMPATIBILITY.md +124 -0
  10. package/docs/CONTRIBUTING.md +134 -0
  11. package/docs/FORMAT.md +157 -0
  12. package/docs/INSTALL.md +179 -0
  13. package/docs/INTEGRATION.md +188 -0
  14. package/docs/LEVEL4.md +202 -0
  15. package/docs/LEVEL5.md +96 -0
  16. package/docs/PERMISSIONS.md +145 -0
  17. package/docs/PROVENANCE.md +83 -0
  18. package/docs/SECURITY.md +93 -0
  19. package/docs/SESSIONS.md +66 -0
  20. package/docs/TROUBLESHOOTING.md +158 -0
  21. package/docs/UNINSTALL.md +122 -0
  22. package/docs/UPGRADE.md +139 -0
  23. package/docs/_config.yml +16 -0
  24. package/docs/_data/nav.yml +36 -0
  25. package/docs/_layouts/default.html +31 -0
  26. package/docs/assets/style.css +88 -0
  27. package/docs/index.md +83 -0
  28. package/handoff.config.example.json +35 -0
  29. package/handoff.config.schema.json +117 -0
  30. package/install/CHANGELOG.md +48 -0
  31. package/install/README.md +76 -0
  32. package/install/install.mjs +856 -0
  33. package/install/package.json +39 -0
  34. package/package.json +66 -4
  35. package/permission-policy.json +33 -0
  36. package/refs/ADAPTERS.md +33 -0
  37. package/refs/bootstrap.md +59 -0
  38. package/refs/brief-checklist.md +79 -0
  39. package/refs/handbook.md +58 -0
  40. package/refs/protocol.md +117 -0
  41. package/refs/roles.md +75 -0
  42. package/refs/validator.md +73 -0
  43. package/schemas/handoff.schema.json +275 -0
  44. package/skill.json +147 -0
  45. package/templates/HANDOFF.llm.schema.json +144 -0
  46. package/templates/HANDOFF.template.md +40 -0
  47. package/tests/acceptance/acceptance.yaml +209 -0
  48. package/tests/fixtures/minimal-transcript.jsonl +2 -0
  49. package/tools/agent-handoff.mjs +410 -0
  50. package/tools/capability-registry.mjs +120 -0
  51. package/tools/handoff.mjs +398 -0
  52. package/tools/handoff.test.mjs +465 -0
  53. package/tools/lib/handoff-root.mjs +161 -0
  54. package/tools/runtime-engine.mjs +330 -0
@@ -0,0 +1,330 @@
1
+ #!/usr/bin/env node
2
+ // runtime-engine.mjs v1.1.0 — runtime MVP Tasks 5+6: permission engine, bounded
3
+ // execution engine skeleton, and checkpoint/resume. Zero-dep (node:* only),
4
+ // filesystem-backed policy (permission-policy.json implements
5
+ // docs/PERMISSION-BOUNDARY.md).
6
+ //
7
+ // ENFORCEMENT ORDER (the contract): every operation is EVALUATED first; execution
8
+ // happens ONLY on an ALLOWED verdict. DENIED and NEEDS_AUTH operations never run —
9
+ // the record shows verdict + enforced_before_execution=true.
10
+ //
11
+ // Commands:
12
+ // node tools/runtime-engine.mjs evaluate --risk R0..R4 --target <path> [--json]
13
+ // node tools/runtime-engine.mjs run --risk R0|R1 --op read --target <path>
14
+ // node tools/runtime-engine.mjs run --risk R1 --op spawn --args "<args for node>"
15
+ // node tools/runtime-engine.mjs policy
16
+ // node tools/runtime-engine.mjs job --session S --steps N [--fail-at K] [--json]
17
+ // node tools/runtime-engine.mjs resume --session S --steps N [--json]
18
+ // node tools/runtime-engine.mjs status --session S [--json]
19
+ // Exit codes: 0 ALLOWED/executed/completed; 2 usage/config error; 3 DENIED;
20
+ // 4 NEEDS_AUTH or missing checkpoint; 5 corrupt checkpoint (integrity failure);
21
+ // 137 simulated abrupt kill (job --fail-at); otherwise the child's captured exit
22
+ // code (bounded execution).
23
+ // Checkpoints (Task 6): after EACH bounded step of a job a durable checkpoint is
24
+ // written to .agent-handoff/checkpoints/<session>.json with a sha256 integrity
25
+ // seal. Resume reconstructs state from disk ONLY — zero shared memory. A missing
26
+ // checkpoint fails honestly (exit 4, never guessed); a tampered checkpoint fails
27
+ // the integrity seal (exit 5). Abrupt termination is simulated by process.exit(137)
28
+ // before the --fail-at step executes — deterministic, hermetic kill equivalent.
29
+ import fs from 'node:fs';
30
+ import path from 'node:path';
31
+ import crypto from 'node:crypto';
32
+ import { spawnSync } from 'node:child_process';
33
+
34
+ const REPO = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')), '..');
35
+ const DEFAULT_POLICY = path.join(REPO, 'permission-policy.json');
36
+ // State root: overridable so oracles run hermetically and the repo accumulates
37
+ // nothing. Env: AGENT_HANDOFF_STATE_DIR=<dir>
38
+ const STATE_DIR = process.env.AGENT_HANDOFF_STATE_DIR
39
+ ? path.resolve(process.env.AGENT_HANDOFF_STATE_DIR)
40
+ : path.join(REPO, '.agent-handoff');
41
+ const EXEC_DIR = path.join(STATE_DIR, 'executions');
42
+ const CKPT_DIR = path.join(STATE_DIR, 'checkpoints');
43
+ const JOBS_DIR = path.join(STATE_DIR, 'jobs');
44
+ const sha = s => crypto.createHash('sha256').update(s).digest('hex');
45
+ const argOf = (n, f) => { const i = process.argv.indexOf(n); return i >= 0 && i + 1 < process.argv.length ? process.argv[i + 1] : (f ? f() : null); };
46
+ const hasFlag = n => process.argv.includes(n);
47
+ const out = (o) => { console.log(hasFlag('--json') ? JSON.stringify(o, null, 2) : JSON.stringify(o)); };
48
+
49
+ function loadPolicy(p) {
50
+ if (!fs.existsSync(p)) return { error: 'policy not found: ' + p };
51
+ try {
52
+ const pol = JSON.parse(fs.readFileSync(p, 'utf8'));
53
+ if (!pol.grants || !pol.risk_to_level) return { error: 'policy missing grants/risk_to_level: ' + p };
54
+ return { pol };
55
+ } catch (e) { return { error: 'policy invalid JSON: ' + p + ' (' + e.message + ')' }; }
56
+ }
57
+
58
+ const rel = p => path.isAbsolute(p) ? path.normalize(p) : path.normalize(path.join(REPO, p));
59
+ function contains(root, target) {
60
+ const r = path.relative(path.normalize(root), path.normalize(target));
61
+ return r === '' || (!r.startsWith('..') && !path.isAbsolute(r));
62
+ }
63
+ // Classification precedence, most specific first:
64
+ // 1. denied_roots — an explicit denial always wins
65
+ // 2. the tool's OWN STATE_DIR — explicit, wherever AGENT_HANDOFF_STATE_DIR points
66
+ // 3. approved_workspaces — the operator's explicit allow-list ('.', repo-upstream, ...)
67
+ // 4. personal_data_roots — a BROAD default root (C:\Users, /home/<user>)
68
+ // 5. system_read_only_roots — broad default (C:\Windows, Program Files)
69
+ // 6. EXTERNAL_PATH
70
+ //
71
+ // (3) MUST come before (4): the broad default usually CONTAINS the workspace. A globally
72
+ // installed skill lives in ~/.agents/skills, %APPDATA%/... or ~/.config/..., all inside the
73
+ // personal-data root, so with the broad root winning, every in-workspace operation of a real
74
+ // install was denied at any risk — the acceptance-D read of the shipped fixture exited 3
75
+ // instead of 0 from every installed copy while passing in the dev tree at E:\. Specific
76
+ // approval beats the broad default; the broad default still governs everything OUTSIDE the
77
+ // workspace, and an explicit denied_roots entry still beats both.
78
+ function classify(pol, target) {
79
+ if (pol.denied_roots && pol.denied_roots.some(rt => contains(rel(rt), target))) return 'DENIED_ROOT';
80
+ if (target && contains(STATE_DIR, target)) return 'APPROVED_WORKSPACE';
81
+ if (pol.approved_workspaces && pol.approved_workspaces.some(rt => contains(rel(rt), target))) return 'APPROVED_WORKSPACE';
82
+ if (pol.personal_data_roots && pol.personal_data_roots.some(rt => contains(rel(rt), target))) return 'PERSONAL_DATA_PATH';
83
+ if (pol.system_read_only_roots && pol.system_read_only_roots.some(rt => contains(rel(rt), target))) return 'SYSTEM_PATH';
84
+ return 'EXTERNAL_PATH';
85
+ }
86
+
87
+ // evaluate: returns { verdict, reason, risk, level, classification }
88
+ // verdict is exactly ALLOWED | DENIED | NEEDS_AUTH. Unknown risk or unknown grant
89
+ // value is DENIED (default for unknown — never guessed allowed).
90
+ function evaluate(pol, risk, target) {
91
+ const level = pol.risk_to_level[risk];
92
+ if (!level) return { verdict: 'DENIED', reason: 'unknown risk class "' + risk + '" (default denial)', risk, level: null, classification: target ? classify(pol, target) : null };
93
+ // Absolute denials first: personal data and secrets-like paths are DENIED at any risk.
94
+ if (target) {
95
+ const cls = classify(pol, target);
96
+ if (cls === 'PERSONAL_DATA_PATH' || cls === 'DENIED_ROOT') return { verdict: 'DENIED', reason: 'target classified ' + cls + ' — denied at any risk level', risk, level, classification: cls };
97
+ if (level === 'READ_ONLY' || level === 'DISCOVERY_ONLY') {
98
+ if (cls === 'APPROVED_WORKSPACE' || cls === 'SYSTEM_PATH') return { verdict: 'ALLOWED', reason: cls + ' read at ' + risk, risk, level, classification: cls };
99
+ return { verdict: 'DENIED', reason: cls + ' not readable at ' + risk, risk, level, classification: cls };
100
+ }
101
+ if (cls !== 'APPROVED_WORKSPACE') return { verdict: 'DENIED', reason: cls + ' outside approved workspace — writes/effects only inside workspace', risk, level, classification: cls };
102
+ }
103
+ const grant = pol.grants[level];
104
+ if (grant === 'allow') return { verdict: 'ALLOWED', reason: level + ' granted at ' + risk, risk, level, classification: target ? classify(pol, target) : 'APPROVED_WORKSPACE' };
105
+ if (grant === 'needs_auth') return { verdict: 'NEEDS_AUTH', reason: level + ' requires explicit authorization at ' + risk, risk, level, classification: target ? classify(pol, target) : 'APPROVED_WORKSPACE' };
106
+ return { verdict: 'DENIED', reason: 'unknown grant "' + grant + '" for ' + level + ' (default denial)', risk, level, classification: target ? classify(pol, target) : null };
107
+ }
108
+
109
+ function record(rec) {
110
+ try {
111
+ fs.mkdirSync(EXEC_DIR, { recursive: true });
112
+ const id = new Date().toISOString().replace(/[:.]/g, '-') + '-' + process.pid + '-' + Math.random().toString(36).slice(2, 8);
113
+ const full = Object.assign({ id, enforced_before_execution: true }, rec);
114
+ const p = path.join(EXEC_DIR, id + '.json');
115
+ fs.writeFileSync(p, JSON.stringify(full, null, 2) + '\n');
116
+ // A state dir on another volume cannot be expressed relative to REPO: path.relative
117
+ // returns an ABSOLUTE path there, and consumers that blindly joined it onto REPO then
118
+ // failed with ENOENT. Store whatever actually resolves, and never a broken-looking
119
+ // relative path.
120
+ const relToRepo = path.relative(REPO, p);
121
+ full.record_path = path.isAbsolute(relToRepo) ? p : relToRepo;
122
+ return full;
123
+ } catch (e) {
124
+ rec.record_error = String(e && e.message || e);
125
+ return rec;
126
+ }
127
+ }
128
+
129
+ function evaluateCmd() {
130
+ const polP = rel(argOf('--policy', () => DEFAULT_POLICY));
131
+ const { pol, error } = loadPolicy(polP);
132
+ if (error) { out({ error }); process.exit(2); }
133
+ const risk = argOf('--risk', () => null);
134
+ const target = argOf('--target', () => null);
135
+ if (!risk) { out({ error: 'usage: evaluate --risk R0..R4 [--target path]' }); process.exit(2); }
136
+ const v = evaluate(pol, risk, target ? rel(target) : null);
137
+ out(v);
138
+ process.exit(v.verdict === 'ALLOWED' ? 0 : v.verdict === 'DENIED' ? 3 : 4);
139
+ }
140
+
141
+ function runCmd() {
142
+ const polP = rel(argOf('--policy', () => DEFAULT_POLICY));
143
+ const { pol, error } = loadPolicy(polP);
144
+ if (error) { out({ error }); process.exit(2); }
145
+ const risk = argOf('--risk', () => null);
146
+ const op = argOf('--op', () => null);
147
+ const target = argOf('--target', () => null);
148
+ const args = argOf('--args', () => null);
149
+ if (!risk || !op) { out({ error: 'usage: run --risk R0|R1 --op read --target <path> | --op spawn --args "<node args>"' }); process.exit(2); }
150
+
151
+ if (op === 'read') {
152
+ if (!target) { out({ error: 'read op requires --target' }); process.exit(2); }
153
+ const t = rel(target);
154
+ const v = evaluate(pol, risk, t);
155
+ if (v.verdict !== 'ALLOWED') {
156
+ record({ op, risk, verdict: v.verdict, reason: v.reason, target, executed: false });
157
+ out(Object.assign(v, { executed: false }));
158
+ process.exit(v.verdict === 'DENIED' ? 3 : 4);
159
+ }
160
+ try {
161
+ const content = fs.readFileSync(t);
162
+ const rec = record({ op, risk, verdict: 'ALLOWED', reason: v.reason, target, executed: true, bytes: content.length, content_sha256: sha(content) });
163
+ out(Object.assign({ executed: true, bytes: content.length, content_sha256: sha(content) }, { record: rec.record_path }));
164
+ process.exit(0);
165
+ } catch (e) {
166
+ const rec = record({ op, risk, verdict: 'ALLOWED', reason: v.reason, target, executed: false, execution_error: String(e && e.message || e) });
167
+ out({ executed: false, error: String(e && e.message || e), record: rec.record_path });
168
+ process.exit(1);
169
+ }
170
+ }
171
+
172
+ if (op === 'spawn') {
173
+ // Bounded: only the local node binary, args from --args, cwd pinned to the
174
+ // approved workspace, hard timeout, output captured and digested.
175
+ const v = evaluate(pol, risk, REPO); // workspace-rooted operation
176
+ if (v.verdict !== 'ALLOWED') {
177
+ record({ op, risk, verdict: v.verdict, reason: v.reason, executed: false, args });
178
+ out(Object.assign(v, { executed: false }));
179
+ process.exit(v.verdict === 'DENIED' ? 3 : 4);
180
+ }
181
+ if (!args || !args.trim()) { out({ error: 'spawn op requires --args' }); process.exit(2); }
182
+ const childArgs = args.split(' ').filter(Boolean);
183
+ const started = new Date().toISOString();
184
+ const r = spawnSync(process.execPath, childArgs, { cwd: REPO, encoding: 'utf8', timeout: 15000 });
185
+ const rec = record({
186
+ op, risk, verdict: 'ALLOWED', reason: v.reason, executed: true,
187
+ command: 'node', args: childArgs, started,
188
+ ended: new Date().toISOString(),
189
+ exit_code: r.status, signal: r.signal || null,
190
+ stdout_sha256: sha(r.stdout || ''), stderr_sha256: sha(r.stderr || ''),
191
+ stdout_bytes: (r.stdout || '').length, stderr_bytes: (r.stderr || '').length,
192
+ timed_out: Boolean(r.error && String(r.error).includes('ETIMEDOUT')) || r.signal === 'SIGTERM',
193
+ });
194
+ out({ executed: true, exit_code: r.status, signal: r.signal || null, record: rec.record_path });
195
+ process.exit(r.status === null ? 1 : r.status);
196
+ }
197
+
198
+ out({ error: 'unknown op "' + op + '" (skeleton supports: read, spawn)' });
199
+ process.exit(2);
200
+ }
201
+
202
+ // ---- Task 6: durable checkpoint + resume (zero shared memory) ----
203
+
204
+ const ckptPath = session => path.join(CKPT_DIR, String(session).replace(/[^\w.-]/g, '_') + '.json');
205
+ const workLog = session => path.join(JOBS_DIR, String(session).replace(/[^\w.-]/g, '_'), 'work.log');
206
+
207
+ function writeCheckpoint(session, ckpt) {
208
+ fs.mkdirSync(CKPT_DIR, { recursive: true });
209
+ ckpt.updated_at = new Date().toISOString();
210
+ const body = Object.assign({}, ckpt);
211
+ delete body.checkpoint_sha256;
212
+ ckpt.checkpoint_sha256 = sha(JSON.stringify(body));
213
+ fs.writeFileSync(ckptPath(session), JSON.stringify(ckpt, null, 2) + '\n');
214
+ }
215
+
216
+ function readCheckpoint(session) {
217
+ const p = ckptPath(session);
218
+ if (!fs.existsSync(p)) return { error: 'no checkpoint for session "' + session + '" (resume requires durable state — never guessed)', code: 4 };
219
+ let ckpt;
220
+ try { ckpt = JSON.parse(fs.readFileSync(p, 'utf8')); }
221
+ catch (e) { return { error: 'corrupt checkpoint (not valid JSON): ' + p, code: 5 }; }
222
+ const body = Object.assign({}, ckpt);
223
+ const expect = body.checkpoint_sha256;
224
+ delete body.checkpoint_sha256;
225
+ if (!expect || sha(JSON.stringify(body)) !== expect) return { error: 'corrupt checkpoint (integrity seal mismatch): ' + p, code: 5 };
226
+ return { ckpt };
227
+ }
228
+
229
+ function execStep(session, seq, pol) {
230
+ // Each bounded step: permission-gated (R1 workspace write), then the step work
231
+ // itself — an append to the session work log. No in-memory carryover between
232
+ // runs: everything resume needs lives in the checkpoint + work log on disk.
233
+ const v = evaluate(pol, 'R1', JOBS_DIR);
234
+ if (v.verdict !== 'ALLOWED') return { error: 'step ' + seq + ' not permitted: ' + v.reason, code: v.verdict === 'DENIED' ? 3 : 4 };
235
+ fs.mkdirSync(path.dirname(workLog(session)), { recursive: true });
236
+ fs.appendFileSync(workLog(session), 'step ' + seq + ' at ' + new Date().toISOString() + '\n');
237
+ return { ok: true };
238
+ }
239
+
240
+ function parseIntArg(name, min) {
241
+ const raw = argOf(name, () => null);
242
+ const n = Number(raw);
243
+ if (!raw || !Number.isInteger(n) || n < min) { out({ error: name + ' must be an integer >= ' + min }); process.exit(2); }
244
+ return n;
245
+ }
246
+
247
+ function jobCmd() {
248
+ const { pol, error } = loadPolicy(rel(argOf('--policy', () => DEFAULT_POLICY)));
249
+ if (error) { out({ error }); process.exit(2); }
250
+ const session = argOf('--session', () => null);
251
+ if (!session) { out({ error: 'job requires --session' }); process.exit(2); }
252
+ const steps = parseIntArg('--steps', 1);
253
+ const failAt = argOf('--fail-at', () => null);
254
+ if (failAt !== null) {
255
+ const k = Number(failAt);
256
+ if (!Number.isInteger(k) || k < 2 || k > steps) { out({ error: '--fail-at must be an integer in [2,' + steps + ']' }); process.exit(2); }
257
+ }
258
+ const existing = readCheckpoint(session);
259
+ if (existing.ckpt) { out({ error: 'checkpoint already exists for session "' + session + '" — use resume' }); process.exit(2); }
260
+ runJob(session, steps, failAt === null ? null : Number(failAt), pol, { seq: 0, steps: [] });
261
+ }
262
+
263
+ function resumeCmd() {
264
+ const { pol, error } = loadPolicy(rel(argOf('--policy', () => DEFAULT_POLICY)));
265
+ if (error) { out({ error }); process.exit(2); }
266
+ const session = argOf('--session', () => null);
267
+ if (!session) { out({ error: 'resume requires --session' }); process.exit(2); }
268
+ const steps = parseIntArg('--steps', 1);
269
+ const { ckpt, error: ckptError, code } = readCheckpoint(session);
270
+ if (ckptError) { out({ error: ckptError }); process.exit(code); }
271
+ if (ckpt.completed) { out({ session, already_completed: true, seq: ckpt.seq, steps_total: ckpt.steps_total }); process.exit(0); }
272
+ if (ckpt.steps_total && steps !== ckpt.steps_total) {
273
+ out({ error: '--steps ' + steps + ' mismatches the durable checkpoint (steps_total ' + ckpt.steps_total + ') — resume must match the recorded job shape' });
274
+ process.exit(2);
275
+ }
276
+ runJob(session, steps, null, pol, ckpt);
277
+ }
278
+
279
+ function runJob(session, steps, failAt, pol, ckpt) {
280
+ let seq = ckpt.seq || 0;
281
+ ckpt.steps = ckpt.steps || [];
282
+ while (seq < steps) {
283
+ const next = seq + 1;
284
+ if (failAt !== null && next === failAt) {
285
+ // Simulated abrupt kill BEFORE executing this step and BEFORE any checkpoint
286
+ // for it: the last durable state is seq = next-1. A real crash (SIGKILL,
287
+ // power loss) leaves exactly the same on-disk state.
288
+ out({ session, killed_at_step: next, checkpoint_seq: seq, note: 'simulated abrupt kill (exit 137) — durable checkpoint intact at seq ' + seq });
289
+ process.exit(137);
290
+ }
291
+ const r = execStep(session, next, pol);
292
+ if (r.error) { out({ error: r.error }); process.exit(r.code); }
293
+ seq = next;
294
+ ckpt.seq = seq;
295
+ ckpt.steps.push({ seq, ts: new Date().toISOString(), op: 'work-log-append' });
296
+ ckpt.steps_total = steps;
297
+ ckpt.session = session;
298
+ writeCheckpoint(session, ckpt);
299
+ }
300
+ ckpt.completed = true;
301
+ writeCheckpoint(session, ckpt);
302
+ out({ session, completed: true, steps, seq, work_log: path.relative(REPO, workLog(session)) });
303
+ process.exit(0);
304
+ }
305
+
306
+ function statusCmd() {
307
+ const session = argOf('--session', () => null);
308
+ if (!session) { out({ error: 'status requires --session' }); process.exit(2); }
309
+ const { ckpt, error, code } = readCheckpoint(session);
310
+ if (error) { out({ error }); process.exit(code); }
311
+ const log = fs.existsSync(workLog(session)) ? fs.readFileSync(workLog(session), 'utf8').split(/\r?\n/).filter(Boolean) : [];
312
+ out({ session, seq: ckpt.seq, steps_total: ckpt.steps_total || null, completed: Boolean(ckpt.completed), work_log_lines: log.length });
313
+ process.exit(0);
314
+ }
315
+
316
+ function policyCmd() {
317
+ const { pol, error } = loadPolicy(rel(argOf('--policy', () => DEFAULT_POLICY)));
318
+ if (error) { out({ error }); process.exit(2); }
319
+ out(pol);
320
+ process.exit(0);
321
+ }
322
+
323
+ const cmd = process.argv[2];
324
+ if (cmd === 'evaluate') evaluateCmd();
325
+ else if (cmd === 'run') runCmd();
326
+ else if (cmd === 'job') jobCmd();
327
+ else if (cmd === 'resume') resumeCmd();
328
+ else if (cmd === 'status') statusCmd();
329
+ else if (cmd === 'policy') policyCmd();
330
+ else { out({ error: 'usage: runtime-engine.mjs evaluate|run|job|resume|status|policy' }); process.exit(2); }