jev-layer 0.1.0 → 0.2.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 (83) hide show
  1. package/CHANGELOG.md +45 -34
  2. package/CONTRIBUTING.md +146 -146
  3. package/LICENSE +21 -21
  4. package/README.md +123 -126
  5. package/README.ru.md +138 -117
  6. package/README.zh-CN.md +137 -117
  7. package/RELEASE.md +53 -53
  8. package/SECURITY.md +56 -56
  9. package/bin/jev.mjs +213 -213
  10. package/config/codex.mcp.toml +8 -8
  11. package/config/generic-mcp.json +13 -13
  12. package/config/hermes.mcp.yaml +9 -9
  13. package/config/jev.example.json +22 -22
  14. package/config/omp.mcp.json +14 -14
  15. package/config/providers.env.example +13 -13
  16. package/docs/AGENT-IMPLEMENTATION.md +250 -0
  17. package/docs/COMPATIBILITY.md +15 -15
  18. package/docs/HERMES-LOCAL-AGENT-HANDOFF.ru.md +177 -0
  19. package/docs/PROVIDERS.md +67 -0
  20. package/docs/SCHEMA-VERSIONING.md +96 -94
  21. package/docs/architecture.svg +77 -0
  22. package/docs/jev-layer-banner.svg +38 -0
  23. package/examples/capabilities.json +39 -39
  24. package/examples/route-request.json +28 -28
  25. package/integrations/codex/.codex-plugin/plugin.json +19 -19
  26. package/integrations/codex/.mcp.json +11 -11
  27. package/integrations/codex/AGENTS.md +1 -1
  28. package/integrations/codex/run-mcp.mjs +9 -9
  29. package/integrations/codex/skills/jev-route/SKILL.md +17 -17
  30. package/integrations/hermes/__init__.py +150 -51
  31. package/integrations/hermes/plugin.yaml +10 -5
  32. package/integrations/hermes/schemas.py +89 -12
  33. package/integrations/omp/extension.js +105 -105
  34. package/integrations/template/README.md +10 -10
  35. package/integrations/template/adapter.mjs +87 -87
  36. package/package.json +60 -59
  37. package/scripts/benchmark.mjs +35 -35
  38. package/scripts/browser-benchmark.mjs +92 -92
  39. package/scripts/browser-e2e.mjs +117 -117
  40. package/scripts/capability-e2e.mjs +31 -31
  41. package/scripts/clean-install-smoke.mjs +162 -162
  42. package/scripts/codex-mcp-smoke.mjs +119 -119
  43. package/scripts/context-filter-e2e.mjs +45 -45
  44. package/scripts/fail-open-smoke.mjs +120 -120
  45. package/scripts/feature-flags-smoke.mjs +46 -46
  46. package/scripts/mcp-receipt-smoke.mjs +113 -98
  47. package/scripts/model-route-report.mjs +15 -0
  48. package/scripts/openrouter-choice.mjs +59 -59
  49. package/scripts/replay-eval.mjs +124 -124
  50. package/scripts/smoke.mjs +34 -34
  51. package/scripts/supervision-e2e.mjs +138 -109
  52. package/src/browser.mjs +571 -569
  53. package/src/cli.mjs +33 -33
  54. package/src/config.mjs +85 -75
  55. package/src/context-filter.mjs +56 -56
  56. package/src/contract.mjs +72 -72
  57. package/src/discovery.mjs +65 -65
  58. package/src/hermes-adapter.mjs +123 -0
  59. package/src/mcp-server.mjs +267 -210
  60. package/src/model-route-metrics.mjs +89 -0
  61. package/src/model-routing.mjs +87 -0
  62. package/src/providers/demo.mjs +52 -52
  63. package/src/providers/index.mjs +12 -0
  64. package/src/providers/typesafe.mjs +126 -126
  65. package/src/receipts.mjs +227 -226
  66. package/src/registry.mjs +109 -109
  67. package/src/relevance-filter.mjs +99 -99
  68. package/src/route.mjs +219 -221
  69. package/src/shadow-compaction.mjs +170 -0
  70. package/src/supervision.mjs +268 -244
  71. package/test/browser.test.mjs +199 -199
  72. package/test/capability.test.mjs +54 -54
  73. package/test/context-filter.test.mjs +65 -65
  74. package/test/hermes-adapter.test.mjs +62 -0
  75. package/test/model-route-metrics.test.mjs +61 -0
  76. package/test/model-routing.test.mjs +59 -0
  77. package/test/openrouter-provider.test.mjs +55 -55
  78. package/test/provider-transports.test.mjs +205 -0
  79. package/test/providers.test.mjs +126 -0
  80. package/test/receipts.test.mjs +71 -71
  81. package/test/route.test.mjs +74 -74
  82. package/test/shadow-compaction.test.mjs +67 -0
  83. package/test/supervision.test.mjs +117 -99
@@ -1,51 +1,150 @@
1
- """Hermes adapter for the standalone Jev layer.
2
-
3
- This plugin exposes only a decision tool. The selected capability remains
4
- owned and executed by Hermes through its normal approval/tool path.
5
- """
6
-
7
- import json
8
- import os
9
- import subprocess
10
- from pathlib import Path
11
-
12
- from .schemas import ROUTE
13
-
14
- ROOT = Path(os.environ.get("JEV_LAYER_ROOT", Path(__file__).resolve().parents[2])).expanduser().resolve()
15
- CLI = ROOT / "src" / "cli.mjs"
16
-
17
- def _fallback(reason: str) -> str:
18
- return json.dumps({
19
- "schema_version": 1,
20
- "status": "fallback",
21
- "selected": None,
22
- "reason": reason,
23
- "fallback": {"type": "adapter_error", "reason": reason},
24
- "execution": {"enabled": False, "status": "not_started"},
25
- })
26
-
27
-
28
- def jev_route(args: dict, **kwargs) -> str:
29
- """Return a bounded decision as JSON; never execute the selected target."""
30
- try:
31
- completed = subprocess.run(
32
- [os.environ.get("JEV_NODE", "node"), str(CLI), "--provider", os.environ.get("JEV_LAYER_PROVIDER", "demo")],
33
- input=json.dumps(args) + "\n",
34
- capture_output=True,
35
- text=True,
36
- timeout=float(os.environ.get("JEV_LAYER_TIMEOUT_S", "3")),
37
- check=False,
38
- )
39
- if completed.returncode != 0:
40
- return _fallback(completed.stderr.strip() or f"core exited with status {completed.returncode}")
41
- line = next((line for line in completed.stdout.splitlines() if line.strip()), "")
42
- if not line:
43
- return _fallback("core returned no decision")
44
- json.loads(line)
45
- return line
46
- except Exception as error:
47
- return _fallback(str(error))
48
-
49
-
50
- def register(ctx):
51
- ctx.register_tool(name="jev_route", toolset="jev_layer", schema=ROUTE, handler=jev_route)
1
+ """Full Hermes adapter for the standalone Jev decision layer.
2
+
3
+ Jev receives only host-supplied bounded choices. Hermes retains tool lookup,
4
+ permissions, approvals, native execution, retries, recovery, and final output.
5
+ The adapter is fail-open: transport or core errors become a Jev fallback and
6
+ never block Hermes's normal path.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import subprocess
14
+ import threading
15
+ from collections import OrderedDict
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from .schemas import BROWSER_STEP, MODEL_ROUTE, RECORD_EXECUTION, ROUTE, SHADOW_COMPACTION, SUPERVISE
20
+
21
+ ROOT = Path(os.environ.get("JEV_LAYER_ROOT", Path(__file__).resolve().parents[2])).expanduser().resolve()
22
+ CORE = ROOT / "src" / "hermes-adapter.mjs"
23
+ _MAX_PENDING_DECISIONS = 64
24
+ _pending_decisions: OrderedDict[str, dict[str, Any]] = OrderedDict()
25
+ _pending_lock = threading.Lock()
26
+
27
+
28
+ def _fallback(reason: str) -> str:
29
+ return json.dumps({
30
+ "schema_version": 1,
31
+ "status": "fallback",
32
+ "selected": None,
33
+ "reason": reason,
34
+ "fallback": {"type": "adapter_error", "reason": reason},
35
+ "execution": {"enabled": False, "status": "not_started"},
36
+ })
37
+
38
+
39
+ def _runtime_env() -> dict[str, str]:
40
+ env = os.environ.copy()
41
+ # Resolve only through Hermes's profile-scoped store, then pass it only to
42
+ # this short-lived local Node adapter. It is never returned or logged.
43
+ try:
44
+ from agent.secret_scope import get_secret
45
+ openrouter_key = get_secret("OPENROUTER_API_KEY")
46
+ if openrouter_key:
47
+ env["OPENROUTER_API_KEY"] = openrouter_key
48
+ except Exception:
49
+ # The core remains fail-open when the optional provider credential is unavailable.
50
+ pass
51
+ # Keep replay evidence in the active Hermes profile, never in the source checkout.
52
+ env.setdefault("JEV_REPLAY_CASES", str(Path(env.get("HERMES_HOME", "~/.hermes")).expanduser() / "jev-layer" / "replay" / "cases.jsonl"))
53
+ return env
54
+
55
+
56
+ def _call_core(operation: str, args: dict, *, decision: dict | None = None) -> dict:
57
+ envelope: dict[str, Any] = {"operation": operation, "args": args}
58
+ if decision is not None:
59
+ envelope["decision"] = decision
60
+ try:
61
+ completed = subprocess.run(
62
+ [os.environ.get("JEV_NODE", "node"), str(CORE)],
63
+ input=json.dumps(envelope) + "\n", capture_output=True, text=True,
64
+ timeout=float(os.environ.get("JEV_LAYER_TIMEOUT_S", "5")), cwd=ROOT,
65
+ env=_runtime_env(), check=False,
66
+ )
67
+ if completed.returncode != 0:
68
+ return json.loads(_fallback(completed.stderr.strip() or f"core exited with status {completed.returncode}"))
69
+ line = next((line for line in completed.stdout.splitlines() if line.strip()), "")
70
+ if not line:
71
+ return json.loads(_fallback("core returned no response"))
72
+ result = json.loads(line)
73
+ return result if isinstance(result, dict) else json.loads(_fallback("core returned non-object response"))
74
+ except Exception as error:
75
+ return json.loads(_fallback(str(error)))
76
+
77
+
78
+ def _remember(decision: dict) -> None:
79
+ correlation_id = decision.get("correlation_id")
80
+ if not isinstance(correlation_id, str) or not correlation_id:
81
+ return
82
+ with _pending_lock:
83
+ _pending_decisions[correlation_id] = decision
84
+ _pending_decisions.move_to_end(correlation_id)
85
+ while len(_pending_decisions) > _MAX_PENDING_DECISIONS:
86
+ _pending_decisions.popitem(last=False)
87
+
88
+
89
+ def _take(correlation_id: str) -> dict | None:
90
+ with _pending_lock:
91
+ return _pending_decisions.pop(correlation_id, None)
92
+
93
+
94
+ def _route(args: dict, operation: str = "route") -> str:
95
+ request = dict(args)
96
+ request.setdefault("schema_version", 1)
97
+ request.setdefault("harness", "hermes")
98
+ result = _call_core(operation, request)
99
+ if result.get("status") in {"selected", "fallback", "no_decision", "needs_confirmation"} and result.get("correlation_id"):
100
+ _remember(result)
101
+ result.pop("_jev_request", None)
102
+ return json.dumps(result)
103
+
104
+
105
+ def jev_route(args: dict, **kwargs) -> str:
106
+ """Choose from a closed Hermes-owned candidate set; never execute it."""
107
+ return _route(args)
108
+
109
+
110
+ def jev_browser_step(args: dict, **kwargs) -> str:
111
+ """Choose one bounded browser action; Hermes owns observation, approval and execution."""
112
+ return _route(args, operation="browser_step")
113
+
114
+
115
+ def jev_model_route(args: dict, **kwargs) -> str:
116
+ """Recommend a declared model profile; this never changes Hermes's model route."""
117
+ return _route(args, operation="model_route")
118
+
119
+
120
+ def jev_shadow_compaction(args: dict, **kwargs) -> str:
121
+ """Report Jev's conservative keep/drop candidates without mutating host context."""
122
+ return json.dumps(_call_core("shadow_compaction", dict(args)))
123
+
124
+
125
+ def jev_supervise(args: dict, **kwargs) -> str:
126
+ """Return a bounded work-state judgment; Hermes maps it to its own next action."""
127
+ request = dict(args)
128
+ request.setdefault("harness", "hermes")
129
+ return json.dumps(_call_core("supervise", request))
130
+
131
+
132
+ def jev_record_execution(args: dict, **kwargs) -> str:
133
+ """Append an execution receipt for a decision made in this Hermes process."""
134
+ correlation_id = args.get("correlation_id")
135
+ if not isinstance(correlation_id, str) or not correlation_id:
136
+ return _fallback("correlation_id is required")
137
+ decision = _take(correlation_id)
138
+ if decision is None:
139
+ return _fallback("unknown or expired Jev correlation_id; host execution remains authoritative")
140
+ receipt = _call_core("record_execution", dict(args), decision=decision)
141
+ return json.dumps(receipt)
142
+
143
+
144
+ def register(ctx):
145
+ ctx.register_tool(name="jev_route", toolset="jev_layer", schema=ROUTE, handler=jev_route)
146
+ ctx.register_tool(name="jev_model_route", toolset="jev_layer", schema=MODEL_ROUTE, handler=jev_model_route)
147
+ ctx.register_tool(name="jev_shadow_compaction", toolset="jev_layer", schema=SHADOW_COMPACTION, handler=jev_shadow_compaction)
148
+ ctx.register_tool(name="jev_record_execution", toolset="jev_layer", schema=RECORD_EXECUTION, handler=jev_record_execution)
149
+ ctx.register_tool(name="jev_supervise", toolset="jev_layer", schema=SUPERVISE, handler=jev_supervise)
150
+ ctx.register_tool(name="jev_browser_step", toolset="jev_layer", schema=BROWSER_STEP, handler=jev_browser_step)
@@ -1,5 +1,10 @@
1
- name: jev-layer
2
- version: 0.1.0
3
- description: Host-owned Jev capability routing through the standalone jev-layer core
4
- provides_tools:
5
- - jev_route
1
+ name: jev-layer
2
+ version: 0.1.0
3
+ description: Full host-owned Jev routing, receipts, supervision, shadow compaction, model routing, and browser decisions
4
+ provides_tools:
5
+ - jev_route
6
+ - jev_model_route
7
+ - jev_shadow_compaction
8
+ - jev_record_execution
9
+ - jev_supervise
10
+ - jev_browser_step
@@ -1,12 +1,89 @@
1
- ROUTE = {
2
- "type": "object",
3
- "required": ["intent", "capabilities"],
4
- "properties": {
5
- "intent": {"type": "string"},
6
- "harness": {"type": "string"},
7
- "context": {"type": "object"},
8
- "actor_permissions": {"type": "array", "items": {"type": "string"}},
9
- "capabilities": {"type": "array", "items": {"type": "object"}},
10
- "policy": {"type": "object"},
11
- },
12
- }
1
+ ROUTE = {
2
+ "type": "object",
3
+ "required": ["intent", "capabilities"],
4
+ "properties": {
5
+ "intent": {"type": "string"},
6
+ "harness": {"type": "string"},
7
+ "context": {"type": "object"},
8
+ "actor_permissions": {"type": "array", "items": {"type": "string"}},
9
+ "capabilities": {"type": "array", "items": {"type": "object"}},
10
+ "policy": {"type": "object"},
11
+ "provider": {"type": "string", "enum": ["demo", "typesafe", "openrouter"]},
12
+ "engine": {"type": "string", "enum": ["native", "jevrouter"]},
13
+ },
14
+ }
15
+
16
+ RECORD_EXECUTION = {
17
+ "type": "object",
18
+ "required": ["correlation_id", "status"],
19
+ "properties": {
20
+ "correlation_id": {"type": "string"},
21
+ "harness": {"type": "string"},
22
+ "capability_id": {"type": "string"},
23
+ "status": {"type": "string", "enum": ["completed", "failed", "not_started"]},
24
+ "result": {},
25
+ "error": {},
26
+ "browser": {"type": "object"},
27
+ "exit_status": {"type": ["integer", "null"]},
28
+ "duration_ms": {"type": ["number", "null"]},
29
+ "started_at": {"type": ["string", "null"]},
30
+ "completed_at": {"type": ["string", "null"]},
31
+ },
32
+ }
33
+
34
+ MODEL_ROUTE = {
35
+ "type": "object",
36
+ "required": ["intent", "models"],
37
+ "properties": {
38
+ "intent": {"type": "string"},
39
+ "context": {"type": "object"},
40
+ "models": {"type": "array", "minItems": 1, "items": {"type": "object"}},
41
+ "harness": {"type": "string"},
42
+ "policy": {"type": "object"},
43
+ "provider": {"type": "string", "enum": ["demo", "typesafe", "openrouter"]},
44
+ },
45
+ }
46
+
47
+ SHADOW_COMPACTION = {
48
+ "type": "object",
49
+ "required": ["intent", "context"],
50
+ "properties": {
51
+ "intent": {"type": "string"},
52
+ "context": {"type": "object"},
53
+ "provider": {"type": "string", "enum": ["demo", "typesafe", "openrouter"]},
54
+ "batch_size": {"type": "integer", "minimum": 1, "maximum": 8},
55
+ "keep_threshold": {"type": "number", "minimum": 0, "maximum": 1},
56
+ "min_confidence": {"type": "number", "minimum": 0, "maximum": 1},
57
+ },
58
+ }
59
+
60
+ SUPERVISE = {
61
+ "type": "object",
62
+ "required": ["job", "observation"],
63
+ "properties": {
64
+ "job": {"type": "object"},
65
+ "observation": {"type": "object"},
66
+ "evidence": {"type": "object"},
67
+ "harness": {"type": "string"},
68
+ "actor_permissions": {"type": "array", "items": {"type": "string"}},
69
+ "policy": {"type": "object"},
70
+ "attempts": {"type": "integer", "minimum": 0},
71
+ "provider": {"type": "string", "enum": ["demo", "typesafe", "openrouter"]},
72
+ "enabled": {"type": "boolean"},
73
+ },
74
+ }
75
+
76
+ BROWSER_STEP = {
77
+ "type": "object",
78
+ "required": ["goal", "observation"],
79
+ "properties": {
80
+ "goal": {"type": "string"},
81
+ "harness": {"type": "string"},
82
+ "start_url": {"type": ["string", "null"]},
83
+ "observation": {"type": "object"},
84
+ "progress": {"type": "object"},
85
+ "policy": {"type": "object"},
86
+ "provider": {"type": "string", "enum": ["demo", "typesafe", "openrouter"]},
87
+ "enabled": {"type": "boolean"},
88
+ },
89
+ }
@@ -1,105 +1,105 @@
1
- import { spawn } from "node:child_process";
2
- import { resolve } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
-
5
- const PACKAGE_ROOT = process.env.JEV_LAYER_ROOT
6
- ? resolve(process.env.JEV_LAYER_ROOT)
7
- : fileURLToPath(new URL("../..", import.meta.url));
8
- const CORE = resolve(PACKAGE_ROOT, "src/cli.mjs");
9
- export default function jevLayerExtension(pi) {
10
- const z = pi.zod;
11
- pi.registerTool({
12
- name: "jev_route",
13
- label: "Jev route",
14
- description: "Return one bounded Jev capability decision. The host executes the selected capability; this tool never executes it.",
15
- parameters: z.object({
16
- intent: z.string(),
17
- harness: z.string().optional(),
18
- context: z.unknown().optional(),
19
- actor_permissions: z.array(z.string()).optional(),
20
- capabilities: z.array(z.unknown()),
21
- policy: z.unknown().optional(),
22
- }),
23
- async execute(_toolCallId, params, signal) {
24
- const decision = await runCore({ ...params, harness: params.harness ?? "omp" }, signal);
25
- return {
26
- content: [{ type: "text", text: JSON.stringify(decision) }],
27
- details: decision,
28
- };
29
- },
30
- });
31
- }
32
-
33
- async function runCore(payload, signal) {
34
- if (globalThis.Bun?.spawn) return runCoreWithBun(payload, signal);
35
- return runCoreWithNode(payload, signal);
36
- }
37
-
38
- async function runCoreWithBun(payload, signal) {
39
- const child = Bun.spawn([process.env.JEV_NODE ?? "node", CORE, "--provider", process.env.JEV_LAYER_PROVIDER ?? "demo"], {
40
- cwd: process.cwd(),
41
- env: process.env,
42
- stdin: "pipe",
43
- stdout: "pipe",
44
- stderr: "pipe",
45
- });
46
- const abort = () => child.kill("SIGTERM");
47
- signal?.addEventListener("abort", abort, { once: true });
48
- child.stdin.write(`${JSON.stringify(payload)}\n`);
49
- child.stdin.end();
50
- const [stdout, stderr, code] = await Promise.all([
51
- new Response(child.stdout).text(),
52
- new Response(child.stderr).text(),
53
- child.exited,
54
- ]);
55
- signal?.removeEventListener("abort", abort);
56
- return parseCoreOutput(stdout, stderr, code);
57
- }
58
-
59
- function runCoreWithNode(payload, signal) {
60
- return new Promise((resolve) => {
61
- const child = spawn(process.env.JEV_NODE ?? "node", [CORE, "--provider", process.env.JEV_LAYER_PROVIDER ?? "demo"], {
62
- cwd: process.cwd(),
63
- env: process.env,
64
- stdio: ["pipe", "pipe", "pipe"],
65
- });
66
- let stdout = "";
67
- let stderr = "";
68
- child.stdout.on("data", (chunk) => { stdout += chunk; });
69
- child.stderr.on("data", (chunk) => { stderr += chunk; });
70
- const abort = () => child.kill("SIGTERM");
71
- signal?.addEventListener("abort", abort, { once: true });
72
- child.on("error", (error) => resolve(fallback(error.message)));
73
- child.on("close", (code) => {
74
- signal?.removeEventListener("abort", abort);
75
- resolve(parseCoreOutput(stdout, stderr, code));
76
- });
77
- child.stdin.end(`${JSON.stringify(payload)}\n`);
78
- });
79
- }
80
-
81
- function parseCoreOutput(stdout, stderr, code) {
82
- if (code !== 0) return fallback(stderr.trim() || `core exited with status ${code}`);
83
- const lines = stdout.split(/\r?\n/);
84
- for (const line of lines) {
85
- const jsonStart = line.indexOf("{");
86
- if (jsonStart < 0) continue;
87
- try {
88
- return JSON.parse(line.slice(jsonStart));
89
- } catch {
90
- // Ignore non-decision lines emitted by a host runtime wrapper.
91
- }
92
- }
93
- return fallback("core returned no valid JSON decision");
94
- }
95
-
96
- function fallback(reason) {
97
- return {
98
- schema_version: 1,
99
- status: "fallback",
100
- selected: null,
101
- reason,
102
- fallback: { type: "adapter_error", reason },
103
- execution: { enabled: false, status: "not_started" },
104
- };
105
- }
1
+ import { spawn } from "node:child_process";
2
+ import { resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const PACKAGE_ROOT = process.env.JEV_LAYER_ROOT
6
+ ? resolve(process.env.JEV_LAYER_ROOT)
7
+ : fileURLToPath(new URL("../..", import.meta.url));
8
+ const CORE = resolve(PACKAGE_ROOT, "src/cli.mjs");
9
+ export default function jevLayerExtension(pi) {
10
+ const z = pi.zod;
11
+ pi.registerTool({
12
+ name: "jev_route",
13
+ label: "Jev route",
14
+ description: "Return one bounded Jev capability decision. The host executes the selected capability; this tool never executes it.",
15
+ parameters: z.object({
16
+ intent: z.string(),
17
+ harness: z.string().optional(),
18
+ context: z.unknown().optional(),
19
+ actor_permissions: z.array(z.string()).optional(),
20
+ capabilities: z.array(z.unknown()),
21
+ policy: z.unknown().optional(),
22
+ }),
23
+ async execute(_toolCallId, params, signal) {
24
+ const decision = await runCore({ ...params, harness: params.harness ?? "omp" }, signal);
25
+ return {
26
+ content: [{ type: "text", text: JSON.stringify(decision) }],
27
+ details: decision,
28
+ };
29
+ },
30
+ });
31
+ }
32
+
33
+ async function runCore(payload, signal) {
34
+ if (globalThis.Bun?.spawn) return runCoreWithBun(payload, signal);
35
+ return runCoreWithNode(payload, signal);
36
+ }
37
+
38
+ async function runCoreWithBun(payload, signal) {
39
+ const child = Bun.spawn([process.env.JEV_NODE ?? "node", CORE], {
40
+ cwd: process.cwd(),
41
+ env: process.env,
42
+ stdin: "pipe",
43
+ stdout: "pipe",
44
+ stderr: "pipe",
45
+ });
46
+ const abort = () => child.kill("SIGTERM");
47
+ signal?.addEventListener("abort", abort, { once: true });
48
+ child.stdin.write(`${JSON.stringify(payload)}\n`);
49
+ child.stdin.end();
50
+ const [stdout, stderr, code] = await Promise.all([
51
+ new Response(child.stdout).text(),
52
+ new Response(child.stderr).text(),
53
+ child.exited,
54
+ ]);
55
+ signal?.removeEventListener("abort", abort);
56
+ return parseCoreOutput(stdout, stderr, code);
57
+ }
58
+
59
+ function runCoreWithNode(payload, signal) {
60
+ return new Promise((resolve) => {
61
+ const child = spawn(process.env.JEV_NODE ?? "node", [CORE], {
62
+ cwd: process.cwd(),
63
+ env: process.env,
64
+ stdio: ["pipe", "pipe", "pipe"],
65
+ });
66
+ let stdout = "";
67
+ let stderr = "";
68
+ child.stdout.on("data", (chunk) => { stdout += chunk; });
69
+ child.stderr.on("data", (chunk) => { stderr += chunk; });
70
+ const abort = () => child.kill("SIGTERM");
71
+ signal?.addEventListener("abort", abort, { once: true });
72
+ child.on("error", (error) => resolve(fallback(error.message)));
73
+ child.on("close", (code) => {
74
+ signal?.removeEventListener("abort", abort);
75
+ resolve(parseCoreOutput(stdout, stderr, code));
76
+ });
77
+ child.stdin.end(`${JSON.stringify(payload)}\n`);
78
+ });
79
+ }
80
+
81
+ function parseCoreOutput(stdout, stderr, code) {
82
+ if (code !== 0) return fallback(stderr.trim() || `core exited with status ${code}`);
83
+ const lines = stdout.split(/\r?\n/);
84
+ for (const line of lines) {
85
+ const jsonStart = line.indexOf("{");
86
+ if (jsonStart < 0) continue;
87
+ try {
88
+ return JSON.parse(line.slice(jsonStart));
89
+ } catch {
90
+ // Ignore non-decision lines emitted by a host runtime wrapper.
91
+ }
92
+ }
93
+ return fallback("core returned no valid JSON decision");
94
+ }
95
+
96
+ function fallback(reason) {
97
+ return {
98
+ schema_version: 1,
99
+ status: "fallback",
100
+ selected: null,
101
+ reason,
102
+ fallback: { type: "adapter_error", reason },
103
+ execution: { enabled: false, status: "not_started" },
104
+ };
105
+ }
@@ -1,10 +1,10 @@
1
- # Template harness adapter
2
-
3
- Copy `adapter.mjs` to `integrations/<harness>/` and replace the injected callbacks with the harness's native implementations:
4
-
5
- - `jevRoute(request)`: call `jev_route` through the existing stdio MCP or CLI boundary;
6
- - `jevRecordExecution(args)`: call `jev_record_execution` with the original `correlation_id`;
7
- - `approve(capabilityId, decision, request)`: delegate to the host's normal approval UI/policy;
8
- - `executeNative(capabilityId, request, decision)`: resolve and execute the id through the host registry.
9
-
10
- The template intentionally does not discover arbitrary tools, grant permissions, retry actions, or spawn workers. Jev failures return `native_fallback`; the host should continue its normal path. Copy the template only after adding a harness-specific smoke fixture and compatibility entry.
1
+ # Template harness adapter
2
+
3
+ Copy `adapter.mjs` to `integrations/<harness>/` and replace the injected callbacks with the harness's native implementations:
4
+
5
+ - `jevRoute(request)`: call `jev_route` through the existing stdio MCP or CLI boundary;
6
+ - `jevRecordExecution(args)`: call `jev_record_execution` with the original `correlation_id`;
7
+ - `approve(capabilityId, decision, request)`: delegate to the host's normal approval UI/policy;
8
+ - `executeNative(capabilityId, request, decision)`: resolve and execute the id through the host registry.
9
+
10
+ The template intentionally does not discover arbitrary tools, grant permissions, retry actions, or spawn workers. Jev failures return `native_fallback`; the host should continue its normal path. Copy the template only after adding a harness-specific smoke fixture and compatibility entry.