@neuroverseos/nv-sim 0.1.7 → 0.1.10

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 (42) hide show
  1. package/README.md +375 -197
  2. package/connectors/nv_mirofish_wrapper.py +841 -0
  3. package/connectors/nv_scienceclaw_wrapper.py +453 -0
  4. package/dist/adapters/scienceclaw.js +52 -2
  5. package/dist/assets/index-B43_0HyO.css +1 -0
  6. package/dist/assets/index-CdghpsS8.js +595 -0
  7. package/dist/assets/{reportEngine-BVdQ2_nW.js → reportEngine-CYSZfooa.js} +1 -1
  8. package/dist/connectors/nv-scienceclaw-post.js +376 -0
  9. package/dist/engine/aiProvider.js +82 -3
  10. package/dist/engine/analyzer.js +12 -24
  11. package/dist/engine/chaosEngine.js +3 -9
  12. package/dist/engine/cli.js +123 -218
  13. package/dist/engine/dynamicsGovernance.js +4 -0
  14. package/dist/engine/fullGovernedLoop.js +16 -1
  15. package/dist/engine/goalEngine.js +3 -4
  16. package/dist/engine/governance.js +18 -0
  17. package/dist/engine/index.js +19 -29
  18. package/dist/engine/intentTranslator.js +281 -0
  19. package/dist/engine/liveAdapter.js +100 -18
  20. package/dist/engine/liveVisualizer.js +2656 -866
  21. package/dist/engine/narrativeInjection.js +78 -89
  22. package/dist/engine/policyEngine.js +171 -58
  23. package/dist/engine/primeRadiant.js +2 -8
  24. package/dist/engine/reasoningEngine.js +2 -7
  25. package/dist/engine/scenarioCapsule.js +77 -133
  26. package/dist/engine/scenarioLibrary.js +52 -131
  27. package/dist/engine/swarmSimulation.js +1 -9
  28. package/dist/engine/worldBridge.js +22 -8
  29. package/dist/engine/worldComparison.js +12 -25
  30. package/dist/index.html +2 -2
  31. package/dist/lib/reasoningEngine.js +17 -1
  32. package/dist/lib/simulationAdapter.js +11 -11
  33. package/dist/lib/swarmParser.js +1 -1
  34. package/dist/runtime/govern.js +160 -7
  35. package/dist/runtime/index.js +1 -4
  36. package/dist/runtime/types.js +91 -0
  37. package/package.json +23 -6
  38. package/dist/adapters/mirofish.js +0 -461
  39. package/dist/assets/index-CHmUN8s0.js +0 -532
  40. package/dist/assets/index-DWgMnB7I.css +0 -1
  41. package/dist/assets/mirotir-logo-DUexumBH.svg +0 -185
  42. package/dist/engine/mirofish.js +0 -295
@@ -0,0 +1,453 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ NeuroVerse ScienceClaw Connector — Governed Execution Wrapper
4
+
5
+ Governance layer:
6
+ Level 1 (entry gate): Evaluates the invocation BEFORE ScienceClaw starts
7
+ Level 2 (streaming): Parses stdout, evaluates each artifact/cycle through /api/evaluate
8
+ Level 3 (completion): Reports final state back to governance server
9
+
10
+ Instead of:
11
+ scienceclaw-post --agent MyAgent --topic "CRISPR risks"
12
+
13
+ Run:
14
+ python nv_scienceclaw_wrapper.py --agent MyAgent --topic "CRISPR risks"
15
+
16
+ What happens:
17
+ 1. Evaluates the launch invocation (ALLOW/BLOCK/PENALIZE/REWARD)
18
+ 2. If allowed, spawns scienceclaw-post as subprocess
19
+ 3. Reads stdout line-by-line — parses JSON artifact data
20
+ 4. Each artifact/cycle is evaluated through /api/evaluate
21
+ 5. If governance blocks mid-stream, can terminate the process
22
+ 6. Reports completion back to governance server
23
+
24
+ Requires:
25
+ - NeuroVerse governance server running: nv-sim serve
26
+ - scienceclaw-post on PATH (or set SCIENCECLAW_BIN)
27
+ - pip install requests
28
+
29
+ Zero edits to ScienceClaw required.
30
+ """
31
+
32
+ import subprocess
33
+ import sys
34
+ import os
35
+ import json
36
+ import time
37
+ import signal
38
+ import threading
39
+
40
+ try:
41
+ import requests
42
+ except ImportError:
43
+ print("ERROR: 'requests' package required. Install with: pip install requests")
44
+ sys.exit(1)
45
+
46
+ # ============================================
47
+ # CONFIGURATION
48
+ # ============================================
49
+
50
+ NEUROVERSE_URL = os.environ.get("NEUROVERSE_URL", "http://localhost:3456")
51
+ EVALUATE_ENDPOINT = f"{NEUROVERSE_URL}/api/evaluate"
52
+ SCIENCECLAW_BIN = os.environ.get("SCIENCECLAW_BIN", "scienceclaw-post")
53
+ VERBOSE = os.environ.get("NV_VERBOSE", "0") == "1"
54
+
55
+ # Streaming governance: if True, kill process on BLOCK/PENALIZE mid-stream
56
+ ENFORCE_STREAMING = os.environ.get("NV_ENFORCE_STREAMING", "1") == "1"
57
+
58
+ # ============================================
59
+ # STATS
60
+ # ============================================
61
+
62
+ stats = {
63
+ "evaluated": 0,
64
+ "allowed": 0,
65
+ "blocked": 0,
66
+ "penalized": 0,
67
+ "rewarded": 0,
68
+ "modified": 0,
69
+ "cycles_seen": 0,
70
+ "artifacts_seen": 0,
71
+ }
72
+
73
+ # ============================================
74
+ # ARGUMENT PARSING
75
+ # ============================================
76
+
77
+ def parse_args(argv):
78
+ """Extract key arguments from the scienceclaw-post command line."""
79
+ args = argv[1:]
80
+ parsed = {
81
+ "agent": "unknown",
82
+ "topic": "",
83
+ "skill": None,
84
+ "confidence": None,
85
+ "passthrough_args": list(args),
86
+ }
87
+
88
+ i = 0
89
+ nv_args = set()
90
+ while i < len(args):
91
+ flag = args[i]
92
+ if flag == "--agent" and i + 1 < len(args):
93
+ parsed["agent"] = args[i + 1]
94
+ i += 2
95
+ elif flag == "--topic" and i + 1 < len(args):
96
+ parsed["topic"] = args[i + 1]
97
+ i += 2
98
+ elif flag == "--skill" and i + 1 < len(args):
99
+ parsed["skill"] = args[i + 1]
100
+ i += 2
101
+ elif flag == "--confidence" and i + 1 < len(args):
102
+ parsed["confidence"] = float(args[i + 1])
103
+ i += 2
104
+ elif flag == "--scienceclaw-bin" and i + 1 < len(args):
105
+ nv_args.add(i)
106
+ nv_args.add(i + 1)
107
+ global SCIENCECLAW_BIN
108
+ SCIENCECLAW_BIN = args[i + 1]
109
+ i += 2
110
+ elif flag == "--nv-url" and i + 1 < len(args):
111
+ nv_args.add(i)
112
+ nv_args.add(i + 1)
113
+ global NEUROVERSE_URL, EVALUATE_ENDPOINT
114
+ NEUROVERSE_URL = args[i + 1]
115
+ EVALUATE_ENDPOINT = f"{NEUROVERSE_URL}/api/evaluate"
116
+ i += 2
117
+ elif flag == "--nv-verbose":
118
+ nv_args.add(i)
119
+ global VERBOSE
120
+ VERBOSE = True
121
+ i += 1
122
+ elif flag == "--nv-no-stream":
123
+ nv_args.add(i)
124
+ global ENFORCE_STREAMING
125
+ ENFORCE_STREAMING = False
126
+ i += 1
127
+ else:
128
+ i += 1
129
+
130
+ parsed["passthrough_args"] = [
131
+ a for idx, a in enumerate(args) if idx not in nv_args
132
+ ]
133
+
134
+ return parsed
135
+
136
+ # ============================================
137
+ # GOVERNANCE EVALUATION
138
+ # ============================================
139
+
140
+ def evaluate_action(action_type, agent, payload_extra=None):
141
+ """Call NeuroVerse /api/evaluate and return the verdict."""
142
+ payload = {
143
+ "actor": agent,
144
+ "action": action_type,
145
+ "payload": {
146
+ "description": f"Agent {agent} executing {action_type}",
147
+ },
148
+ "world": "science_research",
149
+ }
150
+
151
+ if payload_extra:
152
+ payload["payload"].update(payload_extra)
153
+
154
+ try:
155
+ resp = requests.post(EVALUATE_ENDPOINT, json=payload, timeout=5)
156
+ resp.raise_for_status()
157
+ result = resp.json()
158
+ stats["evaluated"] += 1
159
+
160
+ decision = result.get("decision", result.get("status", "ALLOW")).upper()
161
+ if decision == "ALLOW":
162
+ stats["allowed"] += 1
163
+ elif decision == "BLOCK":
164
+ stats["blocked"] += 1
165
+ elif decision == "PENALIZE":
166
+ stats["penalized"] += 1
167
+ elif decision == "REWARD":
168
+ stats["rewarded"] += 1
169
+ elif decision == "MODIFY":
170
+ stats["modified"] += 1
171
+
172
+ if VERBOSE:
173
+ print(f" [NV] {action_type} -> {decision}: {result.get('reason', '')}")
174
+
175
+ return result
176
+ except requests.ConnectionError:
177
+ if stats["evaluated"] == 0:
178
+ print(f" [NV] WARNING: NeuroVerse server not reachable at {NEUROVERSE_URL}")
179
+ print(f" [NV] Start it with: nv-sim serve")
180
+ print(f" [NV] Falling back to ALLOW (ungoverned mode)\n")
181
+ return {"decision": "ALLOW", "reason": "Governance server unavailable"}
182
+ except Exception as e:
183
+ return {"decision": "ALLOW", "reason": f"Evaluation error: {e}"}
184
+
185
+ # ============================================
186
+ # STREAMING GOVERNANCE — parse + evaluate output
187
+ # ============================================
188
+
189
+ def govern_output_line(line, agent, proc):
190
+ """Parse a structured JSON line from ScienceClaw and evaluate for governance.
191
+
192
+ Schema-gated: only strict JSON lines are governed.
193
+ Non-JSON output is passed through without evaluation (by design).
194
+ No regex or heuristic parsing — safety and determinism over flexibility.
195
+ """
196
+ line = line.strip()
197
+ if not line:
198
+ return
199
+ if not line.startswith("{"):
200
+ if VERBOSE:
201
+ print(f" [NV] [ungoverned: not_json] {line[:120]}")
202
+ return
203
+
204
+ try:
205
+ data = json.loads(line)
206
+ except json.JSONDecodeError:
207
+ if VERBOSE:
208
+ print(f" [NV] [ungoverned: invalid_json] {line[:120]}")
209
+ return
210
+
211
+ # ── Schema validation: require cycle + artifacts ──
212
+ cycle = data.get("cycle") or data.get("step") or data.get("round")
213
+ artifacts = data.get("artifacts") or data.get("agent_actions") or []
214
+
215
+ if cycle is None:
216
+ if VERBOSE:
217
+ print(f" [NV] [ungoverned: missing_cycle] {line[:120]}")
218
+ return
219
+
220
+ if not artifacts:
221
+ if VERBOSE:
222
+ print(f" [NV] [ungoverned: no_artifacts] cycle={cycle}")
223
+ return
224
+
225
+ # ── Schema-valid: cycle + artifacts present ──
226
+ stats["cycles_seen"] += 1
227
+
228
+ # Evaluate each artifact in the cycle
229
+ for art in artifacts:
230
+ stats["artifacts_seen"] += 1
231
+ art_agent = art.get("agent_id") or art.get("id") or agent
232
+ art_type = art.get("type") or art.get("action") or "publish"
233
+ art_desc = art.get("description") or f"{art_type} artifact"
234
+ art_conf = art.get("confidence")
235
+
236
+ verdict = evaluate_action(
237
+ action_type=f"publish_{art_type}",
238
+ agent=art_agent,
239
+ payload_extra={
240
+ "description": art_desc,
241
+ "artifact_type": art_type,
242
+ "confidence": art_conf,
243
+ "reproduced": art.get("reproduced", False),
244
+ "citations": art.get("citations", 0),
245
+ "cycle": cycle,
246
+ },
247
+ )
248
+
249
+ decision = verdict.get("decision", verdict.get("status", "ALLOW")).upper()
250
+
251
+ if decision in ("BLOCK", "PENALIZE") and ENFORCE_STREAMING:
252
+ reason = verdict.get("reason", "policy violation")
253
+ print(f"\n [NV] GOVERNANCE HALT at cycle {cycle}")
254
+ print(f" Artifact: {art_desc}")
255
+ print(f" Agent: {art_agent}")
256
+ print(f" Decision: {decision}")
257
+ print(f" Reason: {reason}")
258
+
259
+ if decision == "PENALIZE" and verdict.get("consequence"):
260
+ c = verdict["consequence"]
261
+ print(f" Consequence: {c.get('type', 'unknown')} for {c.get('rounds', 1)} round(s)")
262
+
263
+ print(f" Terminating ScienceClaw process...\n")
264
+
265
+ try:
266
+ proc.terminate()
267
+ proc.wait(timeout=5)
268
+ except Exception:
269
+ proc.kill()
270
+ return # stop processing
271
+
272
+ elif decision == "REWARD":
273
+ reward = verdict.get("reward", {})
274
+ if VERBOSE:
275
+ print(f" [NV] Cycle {cycle}: REWARDED {art_agent} — {reward.get('description', '')}")
276
+
277
+ return
278
+
279
+ # ── Single artifact (no cycle wrapper) ──
280
+ if data.get("agent_id") and data.get("type"):
281
+ stats["artifacts_seen"] += 1
282
+ art_agent = data.get("agent_id") or agent
283
+ art_type = data.get("type") or "publish"
284
+
285
+ evaluate_action(
286
+ action_type=f"publish_{art_type}",
287
+ agent=art_agent,
288
+ payload_extra={
289
+ "description": data.get("description", f"{art_type} artifact"),
290
+ "artifact_type": art_type,
291
+ "confidence": data.get("confidence"),
292
+ "reproduced": data.get("reproduced", False),
293
+ },
294
+ )
295
+
296
+
297
+ def stream_stderr(proc):
298
+ """Read stderr in a background thread to prevent blocking."""
299
+ for line in iter(proc.stderr.readline, ""):
300
+ if line.strip():
301
+ sys.stderr.write(line)
302
+ sys.stderr.flush()
303
+
304
+ # ============================================
305
+ # MAIN EXECUTION
306
+ # ============================================
307
+
308
+ def run():
309
+ parsed = parse_args(sys.argv)
310
+ agent = parsed["agent"]
311
+ topic = parsed["topic"]
312
+
313
+ print(f"\n NeuroVerse ScienceClaw Connector")
314
+ print(f" {'=' * 50}")
315
+ print(f" Agent: {agent}")
316
+ print(f" Topic: {topic or '(none)'}")
317
+ if parsed["skill"]:
318
+ print(f" Skill: {parsed['skill']}")
319
+ print(f" Server: {NEUROVERSE_URL}")
320
+ print(f" Streaming: {'ON — each artifact evaluated' if ENFORCE_STREAMING else 'OFF — entry gate only'}")
321
+ print(f" {'=' * 50}\n")
322
+
323
+ # ── Level 1: Entry gate — evaluate the invocation ──
324
+ print(f" [1/4] Evaluating launch governance...")
325
+
326
+ action_type = "run_investigation"
327
+ if parsed["skill"]:
328
+ action_type = f"execute_skill:{parsed['skill']}"
329
+
330
+ verdict = evaluate_action(
331
+ action_type=action_type,
332
+ agent=agent,
333
+ payload_extra={
334
+ "topic": topic,
335
+ "skill": parsed["skill"],
336
+ "confidence": parsed["confidence"],
337
+ "description": f"Agent {agent}: {action_type} on '{topic}'" if topic else f"Agent {agent}: {action_type}",
338
+ },
339
+ )
340
+
341
+ decision = verdict.get("decision", verdict.get("status", "ALLOW")).upper()
342
+ reason = verdict.get("reason", "")
343
+
344
+ if decision in ("BLOCK", "PENALIZE"):
345
+ label = "BLOCKED" if decision == "BLOCK" else "PENALIZED"
346
+ print(f" [2/4] {label} by NeuroVerse governance")
347
+ print(f" Reason: {reason}")
348
+ if decision == "PENALIZE" and verdict.get("consequence"):
349
+ c = verdict["consequence"]
350
+ print(f" Consequence: {c.get('type', 'unknown')} for {c.get('rounds', 1)} round(s)")
351
+ print(f"\n ScienceClaw execution SKIPPED")
352
+ print(f" {'=' * 50}\n")
353
+ sys.exit(1)
354
+
355
+ elif decision == "REWARD":
356
+ print(f" [2/4] REWARDED — proceeding with boosted priority")
357
+ elif decision == "PAUSE":
358
+ print(f" [2/4] PAUSED — waiting 3s before proceeding")
359
+ time.sleep(3)
360
+ else:
361
+ print(f" [2/4] ALLOWED — proceeding")
362
+
363
+ # ── Level 2: Streaming governance — spawn and monitor ──
364
+ cmd = [SCIENCECLAW_BIN] + parsed["passthrough_args"]
365
+ print(f" [3/4] Executing: {' '.join(cmd)}")
366
+ if ENFORCE_STREAMING:
367
+ print(f" Streaming governance active — each artifact will be evaluated\n")
368
+ else:
369
+ print()
370
+ print(f" {'=' * 50}")
371
+
372
+ try:
373
+ proc = subprocess.Popen(
374
+ cmd,
375
+ stdout=subprocess.PIPE,
376
+ stderr=subprocess.PIPE,
377
+ text=True,
378
+ bufsize=1, # line-buffered
379
+ )
380
+ except FileNotFoundError:
381
+ print(f"\n ERROR: '{SCIENCECLAW_BIN}' not found on PATH")
382
+ print(f" Install ScienceClaw or set SCIENCECLAW_BIN=/path/to/scienceclaw-post")
383
+ sys.exit(127)
384
+
385
+ # Read stderr in background thread to prevent blocking
386
+ stderr_thread = threading.Thread(target=stream_stderr, args=(proc,), daemon=True)
387
+ stderr_thread.start()
388
+
389
+ # Read stdout line by line — pass through to user AND evaluate
390
+ terminated_by_governance = False
391
+ try:
392
+ for line in iter(proc.stdout.readline, ""):
393
+ # Always print the line (user sees ScienceClaw output)
394
+ sys.stdout.write(line)
395
+ sys.stdout.flush()
396
+
397
+ # Evaluate if streaming governance is on
398
+ if ENFORCE_STREAMING and proc.poll() is None:
399
+ govern_output_line(line, agent, proc)
400
+ if proc.poll() is not None:
401
+ terminated_by_governance = True
402
+ break
403
+ except KeyboardInterrupt:
404
+ proc.terminate()
405
+ proc.wait(timeout=5)
406
+
407
+ exit_code = proc.wait()
408
+
409
+ # ── Level 3: Completion report ──
410
+ print(f"\n {'=' * 50}")
411
+ print(f" [4/4] Session complete\n")
412
+
413
+ if terminated_by_governance:
414
+ print(f" Status: HALTED by governance mid-execution")
415
+ elif exit_code == 0:
416
+ print(f" Status: Completed successfully")
417
+ else:
418
+ print(f" Status: Exited with code {exit_code}")
419
+
420
+ print(f" Evaluations: {stats['evaluated']}")
421
+ print(f" Cycles: {stats['cycles_seen']}")
422
+ print(f" Artifacts: {stats['artifacts_seen']}")
423
+ print(f" Allowed: {stats['allowed']}")
424
+ print(f" Blocked: {stats['blocked']}")
425
+ print(f" Penalized: {stats['penalized']}")
426
+ print(f" Rewarded: {stats['rewarded']}")
427
+
428
+ # Report final state to governance server
429
+ try:
430
+ requests.post(
431
+ EVALUATE_ENDPOINT,
432
+ json={
433
+ "actor": agent,
434
+ "action": "investigation_complete",
435
+ "payload": {
436
+ "description": f"Agent {agent} investigation {'halted by governance' if terminated_by_governance else 'completed'} on '{topic}'",
437
+ "exit_code": exit_code,
438
+ "halted_by_governance": terminated_by_governance,
439
+ "stats": stats,
440
+ },
441
+ "world": "science_research",
442
+ },
443
+ timeout=3,
444
+ )
445
+ except Exception:
446
+ pass
447
+
448
+ print(f"\n {'=' * 50}\n")
449
+ sys.exit(1 if terminated_by_governance else exit_code)
450
+
451
+
452
+ if __name__ == "__main__":
453
+ run()
@@ -36,9 +36,12 @@
36
36
  * const cycleResult = adapter.governCycle(artifacts, reactorMatches)
37
37
  */
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.SCIENCECLAW_CAPABILITIES = void 0;
40
+ exports.mapScienceClawEvent = mapScienceClawEvent;
39
41
  exports.createScienceClawAdapter = createScienceClawAdapter;
40
42
  exports.generateDemoInvestigation = generateDemoInvestigation;
41
43
  const govern_1 = require("../runtime/govern");
44
+ const types_1 = require("../runtime/types");
42
45
  const dynamicsGovernance_1 = require("../engine/dynamicsGovernance");
43
46
  const science_metrics_1 = require("../engine/metrics/science.metrics");
44
47
  // ============================================
@@ -97,6 +100,31 @@ function artifactToAction(artifact) {
97
100
  },
98
101
  };
99
102
  }
103
+ /**
104
+ * Maps a ScienceClaw artifact to the NV Governance Schema v1.
105
+ * This is the canonical representation that crosses the governance boundary.
106
+ */
107
+ function mapScienceClawEvent(artifact, cycle) {
108
+ const citationWeight = Math.min(1, artifact.citations / 10);
109
+ const magnitude = artifact.confidence * 0.6 + citationWeight * 0.4;
110
+ return {
111
+ schema: 1,
112
+ source: "scienceclaw",
113
+ cycle,
114
+ agentId: artifact.agentId,
115
+ actionType: (0, types_1.normalizeActionType)(artifact.type),
116
+ description: artifact.description,
117
+ confidence: artifact.confidence,
118
+ magnitude: Number(magnitude.toFixed(3)),
119
+ reproduced: artifact.reproduced,
120
+ references: artifact.citations,
121
+ metadata: {
122
+ artifactId: artifact.id,
123
+ provenance: artifact.provenance,
124
+ skill: artifact.skill,
125
+ },
126
+ };
127
+ }
100
128
  /**
101
129
  * Convert artifacts + matches into SwarmAgentReactions for governDynamics().
102
130
  *
@@ -293,6 +321,12 @@ function createScienceClawAdapter(config = {}) {
293
321
  case "PAUSE":
294
322
  actionStats.modified++;
295
323
  break; // count pause as modified
324
+ case "REWARD":
325
+ actionStats.approved++;
326
+ break; // rewarded actions still proceed
327
+ case "PENALIZE":
328
+ actionStats.blocked++;
329
+ break; // penalized actions are blocked
296
330
  }
297
331
  // Build science-specific reason
298
332
  const riskFlags = [];
@@ -309,9 +343,15 @@ function createScienceClawAdapter(config = {}) {
309
343
  if (verdict.status === "BLOCK") {
310
344
  scienceReason = `Artifact blocked: ${artifact.type} from ${artifact.agentId} — ${verdict.reason}`;
311
345
  }
346
+ else if (verdict.status === "PENALIZE") {
347
+ scienceReason = `Artifact penalized: ${artifact.type} from ${artifact.agentId} frozen — ${verdict.consequence?.description ?? verdict.reason}`;
348
+ }
312
349
  else if (verdict.status === "MODIFY") {
313
350
  scienceReason = `Artifact modified: ${artifact.type} impact reduced — ${verdict.reason}`;
314
351
  }
352
+ else if (verdict.status === "REWARD") {
353
+ scienceReason = `Artifact rewarded: ${artifact.type} from ${artifact.agentId} — ${verdict.reward?.description ?? verdict.reason}`;
354
+ }
315
355
  const result = {
316
356
  artifactId: artifact.id,
317
357
  verdict,
@@ -332,11 +372,11 @@ function createScienceClawAdapter(config = {}) {
332
372
  const blockedArtifactIds = new Set();
333
373
  for (const artifact of artifacts) {
334
374
  const verdict = governArtifact(artifact);
335
- if (verdict.verdict.status === "ALLOW") {
375
+ if (verdict.verdict.status === "ALLOW" || verdict.verdict.status === "REWARD") {
336
376
  approved++;
337
377
  allowedArtifacts.push(artifact);
338
378
  }
339
- else if (verdict.verdict.status === "BLOCK") {
379
+ else if (verdict.verdict.status === "BLOCK" || verdict.verdict.status === "PENALIZE") {
340
380
  blocked++;
341
381
  blockedArtifactIds.add(artifact.id);
342
382
  }
@@ -748,3 +788,13 @@ function generateDemoInvestigation() {
748
788
  }
749
789
  return { artifactCycles, matchesByCycle };
750
790
  }
791
+ exports.SCIENCECLAW_CAPABILITIES = {
792
+ id: "scienceclaw",
793
+ label: "ScienceClaw",
794
+ canControlRounds: false,
795
+ canInterveneLive: true,
796
+ canHalt: true,
797
+ governanceTiming: "live",
798
+ setupInstructions: "ScienceClaw runs independently — NeuroVerse intercepts and governs each artifact cycle in real time.\n\nIf ScienceClaw is on your PATH, the dashboard launches and governs it directly.\n\nOtherwise, run the governance wrapper in a separate terminal:\n python connectors/nv_scienceclaw_wrapper.py --agent MyAgent --topic \"...\"\n\nNeuroVerse evaluates every artifact as it's produced — and can block, modify, or halt the investigation live.",
799
+ connectionCheck: "/api/evaluate",
800
+ };