openclaw-node-harness 2.0.4 → 2.1.1

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 (134) hide show
  1. package/README.md +646 -3
  2. package/bin/hyperagent.mjs +419 -0
  3. package/bin/lane-watchdog.js +23 -2
  4. package/bin/mesh-agent.js +439 -28
  5. package/bin/mesh-bridge.js +69 -3
  6. package/bin/mesh-health-publisher.js +41 -1
  7. package/bin/mesh-task-daemon.js +821 -26
  8. package/bin/mesh.js +411 -20
  9. package/config/claude-settings.json +95 -0
  10. package/config/daemon.json.template +2 -1
  11. package/config/git-hooks/pre-commit +13 -0
  12. package/config/git-hooks/pre-push +12 -0
  13. package/config/harness-rules.json +174 -0
  14. package/config/plan-templates/team-bugfix.yaml +52 -0
  15. package/config/plan-templates/team-deploy.yaml +50 -0
  16. package/config/plan-templates/team-feature.yaml +71 -0
  17. package/config/roles/qa-engineer.yaml +36 -0
  18. package/config/roles/solidity-dev.yaml +51 -0
  19. package/config/roles/tech-architect.yaml +36 -0
  20. package/config/rules/framework/solidity.md +22 -0
  21. package/config/rules/framework/typescript.md +21 -0
  22. package/config/rules/framework/unity.md +21 -0
  23. package/config/rules/universal/design-docs.md +18 -0
  24. package/config/rules/universal/git-hygiene.md +18 -0
  25. package/config/rules/universal/security.md +19 -0
  26. package/config/rules/universal/test-standards.md +19 -0
  27. package/identity/DELEGATION.md +6 -6
  28. package/install.sh +296 -10
  29. package/lib/agent-activity.js +2 -2
  30. package/lib/circling-parser.js +119 -0
  31. package/lib/exec-safety.js +105 -0
  32. package/lib/hyperagent-store.mjs +652 -0
  33. package/lib/kanban-io.js +24 -31
  34. package/lib/llm-providers.js +16 -0
  35. package/lib/mcp-knowledge/bench.mjs +118 -0
  36. package/lib/mcp-knowledge/core.mjs +530 -0
  37. package/lib/mcp-knowledge/package.json +25 -0
  38. package/lib/mcp-knowledge/server.mjs +252 -0
  39. package/lib/mcp-knowledge/test.mjs +802 -0
  40. package/lib/memory-budget.mjs +261 -0
  41. package/lib/mesh-collab.js +483 -165
  42. package/lib/mesh-harness.js +427 -0
  43. package/lib/mesh-plans.js +79 -50
  44. package/lib/mesh-tasks.js +132 -49
  45. package/lib/nats-resolve.js +4 -4
  46. package/lib/plan-templates.js +226 -0
  47. package/lib/pre-compression-flush.mjs +322 -0
  48. package/lib/role-loader.js +292 -0
  49. package/lib/rule-loader.js +358 -0
  50. package/lib/session-store.mjs +461 -0
  51. package/lib/transcript-parser.mjs +292 -0
  52. package/mission-control/drizzle/soul_schema_update.sql +29 -0
  53. package/mission-control/drizzle.config.ts +1 -4
  54. package/mission-control/package-lock.json +1571 -83
  55. package/mission-control/package.json +6 -2
  56. package/mission-control/scripts/gen-chronology.js +3 -3
  57. package/mission-control/scripts/import-pipeline-v2.js +0 -16
  58. package/mission-control/scripts/import-pipeline.js +0 -15
  59. package/mission-control/src/app/api/cowork/clusters/[id]/members/route.ts +117 -0
  60. package/mission-control/src/app/api/cowork/clusters/[id]/route.ts +84 -0
  61. package/mission-control/src/app/api/cowork/clusters/route.ts +141 -0
  62. package/mission-control/src/app/api/cowork/dispatch/route.ts +128 -0
  63. package/mission-control/src/app/api/cowork/events/route.ts +65 -0
  64. package/mission-control/src/app/api/cowork/intervene/route.ts +259 -0
  65. package/mission-control/src/app/api/cowork/sessions/[id]/route.ts +37 -0
  66. package/mission-control/src/app/api/cowork/sessions/route.ts +64 -0
  67. package/mission-control/src/app/api/diagnostics/route.ts +97 -0
  68. package/mission-control/src/app/api/diagnostics/test-runner/route.ts +990 -0
  69. package/mission-control/src/app/api/memory/search/route.ts +6 -3
  70. package/mission-control/src/app/api/mesh/events/route.ts +95 -19
  71. package/mission-control/src/app/api/mesh/identity/route.ts +11 -0
  72. package/mission-control/src/app/api/mesh/tasks/[id]/route.ts +92 -0
  73. package/mission-control/src/app/api/mesh/tasks/route.ts +91 -0
  74. package/mission-control/src/app/api/souls/[id]/evolution/route.ts +21 -5
  75. package/mission-control/src/app/api/souls/[id]/prompt/route.ts +7 -1
  76. package/mission-control/src/app/api/souls/[id]/propagate/route.ts +14 -2
  77. package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +8 -2
  78. package/mission-control/src/app/api/tasks/[id]/route.ts +90 -4
  79. package/mission-control/src/app/api/tasks/route.ts +21 -30
  80. package/mission-control/src/app/api/workspace/read/route.ts +11 -0
  81. package/mission-control/src/app/cowork/page.tsx +261 -0
  82. package/mission-control/src/app/diagnostics/page.tsx +385 -0
  83. package/mission-control/src/app/graph/page.tsx +26 -0
  84. package/mission-control/src/app/memory/page.tsx +1 -1
  85. package/mission-control/src/app/obsidian/page.tsx +36 -6
  86. package/mission-control/src/app/roadmap/page.tsx +24 -0
  87. package/mission-control/src/app/souls/page.tsx +2 -2
  88. package/mission-control/src/components/board/execution-config.tsx +431 -0
  89. package/mission-control/src/components/board/kanban-board.tsx +75 -9
  90. package/mission-control/src/components/board/kanban-column.tsx +135 -19
  91. package/mission-control/src/components/board/task-card.tsx +55 -2
  92. package/mission-control/src/components/board/unified-task-dialog.tsx +82 -4
  93. package/mission-control/src/components/cowork/cluster-card.tsx +176 -0
  94. package/mission-control/src/components/cowork/create-cluster-dialog.tsx +251 -0
  95. package/mission-control/src/components/cowork/dispatch-form.tsx +423 -0
  96. package/mission-control/src/components/cowork/role-picker.tsx +102 -0
  97. package/mission-control/src/components/cowork/session-card.tsx +284 -0
  98. package/mission-control/src/components/layout/sidebar.tsx +39 -2
  99. package/mission-control/src/lib/__tests__/daily-log.test.ts +82 -0
  100. package/mission-control/src/lib/__tests__/memory-md.test.ts +87 -0
  101. package/mission-control/src/lib/__tests__/mesh-kv-sync.test.ts +465 -0
  102. package/mission-control/src/lib/__tests__/mocks/mock-kv.ts +131 -0
  103. package/mission-control/src/lib/__tests__/status-kanban.test.ts +46 -0
  104. package/mission-control/src/lib/__tests__/task-markdown.test.ts +188 -0
  105. package/mission-control/src/lib/__tests__/wikilinks.test.ts +175 -0
  106. package/mission-control/src/lib/config.ts +67 -0
  107. package/mission-control/src/lib/db/index.ts +85 -1
  108. package/mission-control/src/lib/db/schema.ts +61 -3
  109. package/mission-control/src/lib/hooks.ts +309 -0
  110. package/mission-control/src/lib/memory/entities.ts +3 -2
  111. package/mission-control/src/lib/memory/extract.ts +2 -1
  112. package/mission-control/src/lib/memory/retrieval.ts +3 -2
  113. package/mission-control/src/lib/nats.ts +66 -1
  114. package/mission-control/src/lib/parsers/task-markdown.ts +52 -2
  115. package/mission-control/src/lib/parsers/transcript.ts +4 -4
  116. package/mission-control/src/lib/scheduler.ts +12 -11
  117. package/mission-control/src/lib/sync/mesh-kv.ts +279 -0
  118. package/mission-control/src/lib/sync/tasks.ts +23 -1
  119. package/mission-control/src/lib/task-id.ts +32 -0
  120. package/mission-control/src/lib/tts/index.ts +33 -9
  121. package/mission-control/src/middleware.ts +82 -0
  122. package/mission-control/tsconfig.json +2 -1
  123. package/mission-control/vitest.config.ts +14 -0
  124. package/package.json +15 -2
  125. package/services/launchd/ai.openclaw.log-rotate.plist +11 -0
  126. package/services/launchd/ai.openclaw.mesh-deploy-listener.plist +4 -0
  127. package/services/launchd/ai.openclaw.mesh-health-publisher.plist +4 -0
  128. package/services/launchd/ai.openclaw.mission-control.plist +1 -1
  129. package/services/service-manifest.json +1 -1
  130. package/skills/cc-godmode/references/agents.md +8 -8
  131. package/uninstall.sh +37 -9
  132. package/workspace-bin/memory-daemon.mjs +199 -5
  133. package/workspace-bin/session-search.mjs +204 -0
  134. package/workspace-bin/web-fetch.mjs +65 -0
package/install.sh CHANGED
@@ -7,6 +7,14 @@
7
7
  # bash install.sh # Full install
8
8
  # bash install.sh --dry-run # Show what would happen
9
9
  # bash install.sh --update # Re-copy scripts/configs, skip deps
10
+ #
11
+ # --dry-run behavior:
12
+ # Echoes every command that would execute (prefixed with [DRY-RUN]) without
13
+ # modifying the filesystem. Also verifies that all source paths exist —
14
+ # a missing source prints [DRY-RUN ERROR] so path bugs (like SCRIPT_DIR)
15
+ # are caught without running the install. Does NOT check destination
16
+ # writability (that requires actual fs calls). Exit code 0 on success,
17
+ # 1 if any source path is missing.
10
18
 
11
19
  set -euo pipefail
12
20
 
@@ -60,10 +68,28 @@ step() { echo -e "\n${GREEN}━━━ $* ━━━${NC}"; }
60
68
  run() {
61
69
  if $DRY_RUN; then
62
70
  echo " [dry-run] $*"
71
+ # Verify source paths exist for cp/rsync commands (catches path bugs)
72
+ case "$1" in
73
+ cp)
74
+ if [ ! -e "$2" ]; then
75
+ error "[dry-run] SOURCE MISSING: $2"
76
+ DRY_RUN_ERRORS=$((${DRY_RUN_ERRORS:-0} + 1))
77
+ fi
78
+ ;;
79
+ rsync)
80
+ # rsync source is the last arg before the destination
81
+ local src="${@:(-2):1}"
82
+ if [ ! -e "${src%/}" ] && [ ! -d "${src%/}" ]; then
83
+ error "[dry-run] SOURCE MISSING: ${src}"
84
+ DRY_RUN_ERRORS=$((${DRY_RUN_ERRORS:-0} + 1))
85
+ fi
86
+ ;;
87
+ esac
63
88
  else
64
89
  "$@"
65
90
  fi
66
91
  }
92
+ DRY_RUN_ERRORS=0
67
93
 
68
94
  detect_os() {
69
95
  case "$(uname -s)" in
@@ -536,10 +562,54 @@ fi
536
562
  run mkdir -p "$MC_DIR/data"
537
563
 
538
564
  # ============================================================
539
- # Step 12: ClawVault
565
+ # Step 12: Playwright (web-fetch fallback)
566
+ # ============================================================
567
+
568
+ step "Step 12: Playwright Browser"
569
+
570
+ if [ -f "$WORKSPACE/node_modules/.package-lock.json" ] && grep -q '"playwright"' "$WORKSPACE/node_modules/.package-lock.json" 2>/dev/null; then
571
+ info "Playwright already installed in workspace"
572
+ else
573
+ info "Installing Playwright + Chromium (web-fetch fallback for anti-bot sites)..."
574
+ (cd "$WORKSPACE" && run npm install --save playwright 2>/dev/null) || warn "Playwright npm install failed"
575
+ (cd "$WORKSPACE" && run npx playwright install chromium 2>/dev/null) || warn "Chromium browser install failed"
576
+ fi
577
+
578
+ # ============================================================
579
+ # Step 13: Companion Bridge (OpenAI-compatible Claude adapter)
540
580
  # ============================================================
541
581
 
542
- step "Step 12: ClawVault"
582
+ step "Step 13: Companion Bridge"
583
+
584
+ if command -v companion-bridge >/dev/null 2>&1; then
585
+ info "companion-bridge already installed: $(companion-bridge --version 2>/dev/null || echo 'found')"
586
+ else
587
+ info "Installing companion-bridge (OpenAI-compatible adapter for Claude Code)..."
588
+ if [ "$OS" = "linux" ]; then
589
+ run sudo npm install -g companion-bridge || warn "companion-bridge install failed"
590
+ else
591
+ run npm install -g companion-bridge || warn "companion-bridge install failed"
592
+ fi
593
+ fi
594
+
595
+ # Deploy harness rules (user-level override for companion-bridge)
596
+ HARNESS_SRC="${REPO_DIR}/config/harness-rules.json"
597
+ HARNESS_DST="${HOME}/.openclaw/harness-rules.json"
598
+ if [ -f "$HARNESS_SRC" ]; then
599
+ if [ ! -f "$HARNESS_DST" ]; then
600
+ info "Deploying default harness rules to $HARNESS_DST"
601
+ mkdir -p "$(dirname "$HARNESS_DST")"
602
+ cp "$HARNESS_SRC" "$HARNESS_DST"
603
+ else
604
+ info "Harness rules already exist at $HARNESS_DST (skipping — user-owned)"
605
+ fi
606
+ fi
607
+
608
+ # ============================================================
609
+ # Step 14: ClawVault
610
+ # ============================================================
611
+
612
+ step "Step 14: ClawVault"
543
613
 
544
614
  if command -v clawvault >/dev/null 2>&1; then
545
615
  info "ClawVault already installed: $(which clawvault)"
@@ -556,10 +626,10 @@ else
556
626
  fi
557
627
 
558
628
  # ============================================================
559
- # Step 13: Initialize Memory
629
+ # Step 15: Initialize Memory
560
630
  # ============================================================
561
631
 
562
- step "Step 13: Initialize Memory"
632
+ step "Step 15: Initialize Memory"
563
633
 
564
634
  TODAY=$(date +%Y-%m-%d)
565
635
  DAILY_FILE="$WORKSPACE/memory/$TODAY.md"
@@ -650,10 +720,27 @@ MEM
650
720
  fi
651
721
 
652
722
  # ============================================================
653
- # Step 14: Install Services (role-aware, template-based)
723
+ # Step 15.5: HyperAgent Protocol
654
724
  # ============================================================
655
725
 
656
- step "Step 14: Install Services (role=$NODE_ROLE)"
726
+ step "Step 15.5: HyperAgent Protocol"
727
+
728
+ if [ -f "$MESH_BIN/hyperagent.mjs" ]; then
729
+ mkdir -p "$HOME/.openclaw/state"
730
+ if node "$MESH_BIN/hyperagent.mjs" status 2>/dev/null; then
731
+ info "HyperAgent store initialized"
732
+ else
733
+ warn "HyperAgent init deferred (will init on first use)"
734
+ fi
735
+ else
736
+ warn "hyperagent.mjs not found in $MESH_BIN — skipping"
737
+ fi
738
+
739
+ # ============================================================
740
+ # Step 16: Install Services (role-aware, template-based)
741
+ # ============================================================
742
+
743
+ step "Step 16: Install Services (role=$NODE_ROLE)"
657
744
 
658
745
  MANIFEST="$REPO_DIR/services/service-manifest.json"
659
746
  LAUNCHD_TEMPLATES="$REPO_DIR/services/launchd"
@@ -683,8 +770,9 @@ else
683
770
  fi
684
771
 
685
772
  if [ "$OS" = "macos" ]; then
686
- TEMPLATE="$LAUNCHD_TEMPLATES/ai.openclaw.${SVC_NAME}.plist"
687
- DEST="$LAUNCHD_DEST/ai.openclaw.${SVC_NAME}.plist"
773
+ LAUNCHD_SVC_NAME="${SVC_NAME#openclaw-}"
774
+ TEMPLATE="$LAUNCHD_TEMPLATES/ai.openclaw.${LAUNCHD_SVC_NAME}.plist"
775
+ DEST="$LAUNCHD_DEST/ai.openclaw.${LAUNCHD_SVC_NAME}.plist"
688
776
 
689
777
  if [ ! -f "$TEMPLATE" ]; then
690
778
  warn " Template not found: $TEMPLATE"
@@ -807,10 +895,10 @@ else
807
895
  fi
808
896
 
809
897
  # ============================================================
810
- # Step 15: Mesh Network (optional — if Tailscale detected)
898
+ # Step 17: Mesh Network (optional — if Tailscale detected)
811
899
  # ============================================================
812
900
 
813
- step "Step 15: Mesh Network"
901
+ step "Step 17: Mesh Network"
814
902
 
815
903
  if $SKIP_MESH; then
816
904
  info "Skipped (--skip-mesh flag set by meta-installer)"
@@ -862,6 +950,204 @@ fi
862
950
 
863
951
  fi # end SKIP_MESH else block
864
952
 
953
+ # ============================================================
954
+ # Step 18: Path-Scoped Rules
955
+ # ============================================================
956
+
957
+ step "Step 18: Path-Scoped Rules"
958
+
959
+ RULES_DIR="${OPENCLAW_ROOT}/rules"
960
+ mkdir -p "$RULES_DIR"
961
+
962
+ # install_rule — version-aware rule deployment.
963
+ # Fresh install: copy. Update: compare versions. If source is newer and local
964
+ # was modified (hash mismatch), save as .new instead of overwriting.
965
+ install_rule() {
966
+ local src="$1" dst="$2" name="$3"
967
+ if [ ! -f "$src" ]; then return; fi
968
+
969
+ if [ ! -f "$dst" ]; then
970
+ cp "$src" "$dst"
971
+ info "Installed rule: ${name}"
972
+ return
973
+ fi
974
+
975
+ # Both exist — compare version fields
976
+ local src_ver dst_ver
977
+ src_ver=$(grep -m1 '^version:' "$src" 2>/dev/null | sed 's/version:[[:space:]]*//' || echo "0.0.0")
978
+ dst_ver=$(grep -m1 '^version:' "$dst" 2>/dev/null | sed 's/version:[[:space:]]*//' || echo "0.0.0")
979
+
980
+ if [ "$src_ver" = "$dst_ver" ]; then
981
+ return # Same version, nothing to do
982
+ fi
983
+
984
+ # Different versions — check if user modified the local copy
985
+ local src_hash dst_hash
986
+ if command -v md5sum &>/dev/null; then
987
+ src_hash=$(md5sum "$src" | cut -d' ' -f1)
988
+ dst_hash=$(md5sum "$dst" | cut -d' ' -f1)
989
+ elif command -v md5 &>/dev/null; then
990
+ src_hash=$(md5 -q "$src")
991
+ dst_hash=$(md5 -q "$dst")
992
+ else
993
+ # Can't compare hashes — save as .new to be safe
994
+ cp "$src" "${dst}.new"
995
+ warn "Rule ${name}: new version ${src_ver} available (saved as ${name}.new)"
996
+ return
997
+ fi
998
+
999
+ if [ "$src_hash" = "$dst_hash" ]; then
1000
+ # Same content despite different version — just update
1001
+ cp "$src" "$dst"
1002
+ info "Updated rule: ${name} (${dst_ver} → ${src_ver})"
1003
+ else
1004
+ # User-modified — don't overwrite, save as .new
1005
+ cp "$src" "${dst}.new"
1006
+ warn "Rule ${name}: new version ${src_ver} available but local copy modified. Saved as ${name}.new for manual merge."
1007
+ fi
1008
+ }
1009
+
1010
+ # Copy universal rules (always)
1011
+ for rule in security test-standards design-docs git-hygiene; do
1012
+ install_rule "${REPO_DIR}/config/rules/universal/${rule}.md" "${RULES_DIR}/${rule}.md" "${rule}.md"
1013
+ done
1014
+
1015
+ # Detect frameworks and install matching rules
1016
+ if [ -f "package.json" ] || [ -f "${WORKSPACE}/../../package.json" ]; then
1017
+ PKG_FILE="package.json"
1018
+ [ ! -f "$PKG_FILE" ] && PKG_FILE="${WORKSPACE}/../../package.json"
1019
+
1020
+ if [ -f "$PKG_FILE" ]; then
1021
+ # Solidity detection
1022
+ if grep -q '"hardhat"' "$PKG_FILE" 2>/dev/null || [ -f "hardhat.config.js" ] || [ -f "hardhat.config.ts" ] || [ -f "foundry.toml" ]; then
1023
+ install_rule "${REPO_DIR}/config/rules/framework/solidity.md" "${RULES_DIR}/solidity.md" "solidity.md"
1024
+ [ ! -f "${RULES_DIR}/solidity.md.new" ] || true # install_rule handles logging
1025
+ fi
1026
+
1027
+ # TypeScript detection
1028
+ if [ -f "tsconfig.json" ] || [ -f "${WORKSPACE}/../../tsconfig.json" ]; then
1029
+ install_rule "${REPO_DIR}/config/rules/framework/typescript.md" "${RULES_DIR}/typescript.md" "typescript.md"
1030
+ fi
1031
+
1032
+ # Unity detection
1033
+ if [ -d "ProjectSettings" ] || [ -d "Assets" ]; then
1034
+ install_rule "${REPO_DIR}/config/rules/framework/unity.md" "${RULES_DIR}/unity.md" "unity.md"
1035
+ fi
1036
+ fi
1037
+ fi
1038
+
1039
+ info "Rules directory: ${RULES_DIR} ($(ls -1 "$RULES_DIR" 2>/dev/null | wc -l | tr -d ' ') rules)"
1040
+
1041
+ # ============================================================
1042
+ # Step 19: Plan Templates
1043
+ # ============================================================
1044
+
1045
+ step "Step 19: Plan Templates"
1046
+
1047
+ TEMPLATES_DIR="${OPENCLAW_ROOT}/plan-templates"
1048
+ mkdir -p "$TEMPLATES_DIR"
1049
+
1050
+ for tmpl in team-feature team-bugfix team-deploy; do
1051
+ TMPL_SRC="${REPO_DIR}/config/plan-templates/${tmpl}.yaml"
1052
+ TMPL_DST="${TEMPLATES_DIR}/${tmpl}.yaml"
1053
+ if [ -f "$TMPL_SRC" ] && [ ! -f "$TMPL_DST" ]; then
1054
+ cp "$TMPL_SRC" "$TMPL_DST"
1055
+ info "Installed plan template: ${tmpl}.yaml"
1056
+ fi
1057
+ done
1058
+
1059
+ info "Templates directory: ${TEMPLATES_DIR}"
1060
+
1061
+ # ============================================================
1062
+ # Step 20: Claude Code Integration
1063
+ # ============================================================
1064
+
1065
+ step "Step 20: Claude Code Integration"
1066
+
1067
+ # Create .claude directory structure (in workspace root)
1068
+ CLAUDE_DIR="${WORKSPACE}/.claude"
1069
+ mkdir -p "${CLAUDE_DIR}/hooks"
1070
+
1071
+ # Symlink rules directory
1072
+ RULES_LINK="${CLAUDE_DIR}/rules"
1073
+ if [ ! -L "$RULES_LINK" ] && [ ! -d "$RULES_LINK" ]; then
1074
+ ln -s "${RULES_DIR}" "$RULES_LINK"
1075
+ info "Symlinked .claude/rules → ${RULES_DIR}"
1076
+ fi
1077
+
1078
+ # Deploy settings.json — merge hooks into existing if present, never overwrite permissions
1079
+ SETTINGS_SRC="${REPO_DIR}/config/claude-settings.json"
1080
+ SETTINGS_DST="${CLAUDE_DIR}/settings.json"
1081
+ if [ -f "$SETTINGS_SRC" ]; then
1082
+ if [ ! -f "$SETTINGS_DST" ]; then
1083
+ # Fresh install — copy wholesale
1084
+ cp "$SETTINGS_SRC" "$SETTINGS_DST"
1085
+ info "Deployed Claude Code settings.json"
1086
+ elif command -v jq &>/dev/null; then
1087
+ # Existing settings — merge hooks only, preserve user permissions
1088
+ # Strategy: for each hook lifecycle key (SessionStart, PreToolUse, etc.),
1089
+ # append our hook entries if they don't already exist (matched by command string)
1090
+ MERGED=$(jq -s '
1091
+ .[0] as $existing | .[1] as $new |
1092
+ $existing * {
1093
+ hooks: (
1094
+ ($new.hooks // {}) | to_entries | reduce .[] as $entry (
1095
+ ($existing.hooks // {});
1096
+ .[$entry.key] as $current |
1097
+ if $current == null then
1098
+ . + {($entry.key): $entry.value}
1099
+ else
1100
+ # Append hook entries whose command is not already present
1101
+ ($current | map(.hooks) | flatten | map(.command)) as $existing_cmds |
1102
+ ($entry.value | map(
1103
+ .hooks |= [.[] | select(.command as $cmd | $existing_cmds | index($cmd) | not)]
1104
+ | select(.hooks | length > 0)
1105
+ )) as $new_entries |
1106
+ if ($new_entries | length) > 0 then
1107
+ . + {($entry.key): ($current + $new_entries)}
1108
+ else .
1109
+ end
1110
+ end
1111
+ )
1112
+ )
1113
+ }
1114
+ ' "$SETTINGS_DST" "$SETTINGS_SRC")
1115
+ echo "$MERGED" > "$SETTINGS_DST"
1116
+ info "Merged OpenClaw hooks into existing settings.json (permissions preserved)"
1117
+ else
1118
+ # No jq — can't safely merge. Dump patch file for manual merge.
1119
+ cp "$SETTINGS_SRC" "${SETTINGS_DST}.openclaw-hooks"
1120
+ warn "jq not found — hooks config saved to settings.json.openclaw-hooks for manual merge"
1121
+ fi
1122
+ fi
1123
+
1124
+ # Deploy hook scripts
1125
+ for hook in session-start validate-commit validate-push pre-compact session-stop log-agent; do
1126
+ HOOK_SRC="${REPO_DIR}/.claude/hooks/${hook}.sh"
1127
+ HOOK_DST="${CLAUDE_DIR}/hooks/${hook}.sh"
1128
+ if [ -f "$HOOK_SRC" ]; then
1129
+ cp "$HOOK_SRC" "$HOOK_DST"
1130
+ chmod +x "$HOOK_DST"
1131
+ fi
1132
+ done
1133
+ info "Deployed Claude Code hooks"
1134
+
1135
+ # Deploy git hooks (LLM-agnostic enforcement)
1136
+ if [ -d ".git/hooks" ] || [ -d "${WORKSPACE}/../../.git/hooks" ]; then
1137
+ GIT_HOOKS_DIR=".git/hooks"
1138
+ [ ! -d "$GIT_HOOKS_DIR" ] && GIT_HOOKS_DIR="${WORKSPACE}/../../.git/hooks"
1139
+
1140
+ for ghook in pre-commit pre-push; do
1141
+ GHOOK_SRC="${REPO_DIR}/config/git-hooks/${ghook}"
1142
+ GHOOK_DST="${GIT_HOOKS_DIR}/${ghook}"
1143
+ if [ -f "$GHOOK_SRC" ] && [ ! -f "$GHOOK_DST" ]; then
1144
+ cp "$GHOOK_SRC" "$GHOOK_DST"
1145
+ chmod +x "$GHOOK_DST"
1146
+ info "Installed git hook: ${ghook}"
1147
+ fi
1148
+ done
1149
+ fi
1150
+
865
1151
  # ============================================================
866
1152
  # Done!
867
1153
  # ============================================================
@@ -147,7 +147,7 @@ async function readLastEntry(filePath) {
147
147
  } else {
148
148
  const fh = await open(filePath, 'r');
149
149
  try {
150
- const buffer = Buffer.allocUnsafe(chunkSize);
150
+ const buffer = Buffer.alloc(chunkSize);
151
151
  await fh.read(buffer, 0, chunkSize, offset);
152
152
  content = buffer.toString('utf-8');
153
153
  } finally {
@@ -195,7 +195,7 @@ async function parseJsonlTail(filePath, maxBytes = 131072) {
195
195
  const fh = await open(filePath, 'r');
196
196
  try {
197
197
  const length = size - offset;
198
- const buffer = Buffer.allocUnsafe(length);
198
+ const buffer = Buffer.alloc(length);
199
199
  await fh.read(buffer, 0, length, offset);
200
200
  content = buffer.toString('utf-8');
201
201
  } finally {
@@ -0,0 +1,119 @@
1
+ /**
2
+ * circling-parser.js — Standalone parser for Circling Strategy LLM output.
3
+ *
4
+ * Extracted from mesh-agent.js so both production code and tests import
5
+ * the same module. Zero external dependencies.
6
+ *
7
+ * Handles both single-artifact and multi-artifact output formats.
8
+ *
9
+ * Single artifact: everything before ===CIRCLING_REFLECTION=== is the artifact.
10
+ * Multi artifact: ===CIRCLING_ARTIFACT=== / ===END_ARTIFACT=== pairs delimit
11
+ * artifact content by position (content BETWEEN previous END_ARTIFACT and
12
+ * next CIRCLING_ARTIFACT marker).
13
+ *
14
+ * @param {string} output — raw LLM output
15
+ * @param {object} [opts]
16
+ * @param {function} [opts.log] — optional logger (default: no-op)
17
+ * @param {function} [opts.legacyParser] — optional fallback parser for output
18
+ * without circling delimiters. Called as legacyParser(output). Should return
19
+ * { summary, confidence, vote, parse_failed }. If not provided, missing
20
+ * delimiters produce parse_failed: true.
21
+ * @returns {{ circling_artifacts: Array<{type: string, content: string}>, summary: string, confidence: number, vote: string, parse_failed: boolean }}
22
+ */
23
+ function parseCirclingReflection(output, opts = {}) {
24
+ const log = opts.log || (() => {});
25
+
26
+ const VALID_VOTES = new Set(['continue', 'converged', 'blocked']);
27
+ const result = {
28
+ circling_artifacts: [],
29
+ summary: '',
30
+ confidence: 0.5,
31
+ vote: 'continue',
32
+ parse_failed: false,
33
+ };
34
+
35
+ // Extract the reflection metadata block
36
+ const reflMatch = output.match(/===CIRCLING_REFLECTION===([\s\S]*?)===END_REFLECTION===/);
37
+ if (!reflMatch) {
38
+ // No circling delimiters — try legacy fallback if provided
39
+ if (opts.legacyParser) {
40
+ const legacy = opts.legacyParser(output);
41
+ return {
42
+ circling_artifacts: [],
43
+ summary: legacy.summary,
44
+ confidence: legacy.confidence,
45
+ vote: legacy.vote,
46
+ parse_failed: legacy.parse_failed,
47
+ };
48
+ }
49
+ return { ...result, parse_failed: true, vote: 'parse_error' };
50
+ }
51
+
52
+ // Parse reflection key-value pairs
53
+ const reflBlock = reflMatch[1];
54
+ const typeMatch = reflBlock.match(/^type:\s*(.+)$/m);
55
+ const summaryMatch = reflBlock.match(/^summary:\s*(.+)$/m);
56
+ const confMatch = reflBlock.match(/^confidence:\s*([\d.]+)$/m);
57
+ const voteMatch = reflBlock.match(/^vote:\s*(\w+)$/m);
58
+
59
+ result.summary = summaryMatch ? summaryMatch[1].trim() : '';
60
+ result.confidence = confMatch ? parseFloat(confMatch[1]) : 0.5;
61
+ const voteRaw = voteMatch ? voteMatch[1].trim().toLowerCase() : 'continue';
62
+ result.vote = VALID_VOTES.has(voteRaw) ? voteRaw : 'parse_error';
63
+ if (!VALID_VOTES.has(voteRaw)) result.parse_failed = true;
64
+
65
+ const artifactType = typeMatch ? typeMatch[1].trim() : 'unknown';
66
+
67
+ // Check for multi-artifact format
68
+ const artifactBlocks = [...output.matchAll(/===CIRCLING_ARTIFACT===([\s\S]*?)===END_ARTIFACT===/g)];
69
+
70
+ if (artifactBlocks.length > 0) {
71
+ // Multi-artifact: parse each block.
72
+ // Content for artifact N is between the previous ===END_ARTIFACT=== (or start
73
+ // of output for N=0) and this artifact's ===CIRCLING_ARTIFACT=== marker.
74
+ const parts = output.split('===CIRCLING_REFLECTION===')[0]; // everything before reflection
75
+ const artMatches = [...parts.matchAll(/===CIRCLING_ARTIFACT===\s*\n([\s\S]*?)===END_ARTIFACT===/g)];
76
+ const chunks = [];
77
+
78
+ for (let i = 0; i < artMatches.length; i++) {
79
+ const m = artMatches[i];
80
+ const header = m[1].trim();
81
+ const typeLineMatch = header.match(/^type:\s*(.+)$/m);
82
+ const artType = typeLineMatch ? typeLineMatch[1].trim() : `artifact_${i}`;
83
+
84
+ const artStart = m.index;
85
+ const prevEnd = i === 0 ? 0 : (artMatches[i - 1].index + artMatches[i - 1][0].length);
86
+ const content = parts.slice(prevEnd, artStart).trim();
87
+
88
+ if (content) {
89
+ chunks.push({ type: artType, content });
90
+ }
91
+ }
92
+
93
+ // Last chunk: content after the last END_ARTIFACT before CIRCLING_REFLECTION
94
+ if (artMatches.length > 0) {
95
+ const lastArt = artMatches[artMatches.length - 1];
96
+ const afterLast = parts.slice(lastArt.index + lastArt[0].length).trim();
97
+ if (afterLast) {
98
+ chunks.push({ type: 'extra', content: afterLast });
99
+ }
100
+ }
101
+
102
+ result.circling_artifacts = chunks;
103
+
104
+ } else {
105
+ // Single-artifact: everything before ===CIRCLING_REFLECTION=== is the artifact
106
+ const beforeReflection = output.split('===CIRCLING_REFLECTION===')[0].trim();
107
+ if (beforeReflection) {
108
+ result.circling_artifacts = [{ type: artifactType, content: beforeReflection }];
109
+ }
110
+ }
111
+
112
+ if (result.circling_artifacts.length === 0 && !result.parse_failed) {
113
+ log('CIRCLING PARSE WARNING: No artifacts extracted from output');
114
+ }
115
+
116
+ return result;
117
+ }
118
+
119
+ module.exports = { parseCirclingReflection };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * exec-safety.js — Shared command safety filtering for mesh exec.
3
+ *
4
+ * Used by both CLI-side (mesh.js) and server-side (NATS exec handler)
5
+ * to block destructive or unauthorized commands before execution.
6
+ *
7
+ * Two layers:
8
+ * 1. DESTRUCTIVE_PATTERNS — blocklist of known-dangerous patterns
9
+ * 2. ALLOWED_PREFIXES — allowlist for server-side execution (opt-in)
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const DESTRUCTIVE_PATTERNS = [
15
+ /\brm\s+(-[a-zA-Z]*)?r[a-zA-Z]*f/, // rm -rf, rm -fr, rm --recursive --force
16
+ /\brm\s+(-[a-zA-Z]*)?f[a-zA-Z]*r/, // rm -fr variants
17
+ /\bmkfs\b/, // format filesystem
18
+ /\bdd\s+.*of=/, // raw disk write
19
+ /\b>\s*\/dev\/[sh]d/, // write to raw device
20
+ /\bcurl\b.*\|\s*(ba)?sh/, // curl pipe to shell
21
+ /\bwget\b.*\|\s*(ba)?sh/, // wget pipe to shell
22
+ /\bchmod\s+(-[a-zA-Z]*\s+)?777\s+\//, // chmod 777 on root paths
23
+ /\b:(){ :\|:& };:/, // fork bomb
24
+ /\bsudo\b/, // sudo escalation
25
+ /\bsu\s+-?\s/, // su user switch
26
+ /\bpasswd\b/, // password change
27
+ /\buseradd\b|\buserdel\b/, // user management
28
+ /\biptables\b|\bnft\b/, // firewall modification
29
+ /\bsystemctl\s+(stop|disable|mask)/, // service disruption
30
+ /\blaunchctl\s+(unload|remove)/, // macOS service disruption
31
+ /\bkill\s+-9\s+1\b/, // kill init/launchd
32
+ />\s*\/etc\//, // overwrite system config
33
+ /\beval\b.*\$\(/, // eval with command substitution
34
+ ];
35
+
36
+ /**
37
+ * Allowed command prefixes for server-side NATS exec.
38
+ * Only commands starting with one of these are permitted.
39
+ * CLI-side uses blocklist only; server-side uses both blocklist + allowlist.
40
+ */
41
+ const ALLOWED_EXEC_PREFIXES = [
42
+ 'git ', 'npm ', 'node ', 'npx ', 'python ', 'python3 ',
43
+ 'cat ', 'ls ', 'head ', 'tail ', 'grep ', 'find ', 'wc ',
44
+ 'echo ', 'date ', 'uptime ', 'df ', 'free ', 'ps ',
45
+ 'bash openclaw/', 'bash ~/openclaw/', 'bash ./bin/',
46
+ 'cd ', 'pwd', 'which ', 'env ', 'printenv ',
47
+ 'cargo ', 'go ', 'make ', 'pytest ', 'jest ', 'vitest ',
48
+ ];
49
+
50
+ /**
51
+ * Check if a command matches any destructive pattern.
52
+ * @param {string} command
53
+ * @returns {{ blocked: boolean, pattern?: RegExp }}
54
+ */
55
+ function checkDestructivePatterns(command) {
56
+ for (const pattern of DESTRUCTIVE_PATTERNS) {
57
+ if (pattern.test(command)) {
58
+ return { blocked: true, pattern };
59
+ }
60
+ }
61
+ return { blocked: false };
62
+ }
63
+
64
+ /**
65
+ * Check if a command is allowed by the server-side allowlist.
66
+ * @param {string} command
67
+ * @returns {boolean}
68
+ */
69
+ function isAllowedExecCommand(command) {
70
+ const trimmed = (command || '').trim();
71
+ if (!trimmed) return false;
72
+ return ALLOWED_EXEC_PREFIXES.some(p => trimmed.startsWith(p));
73
+ }
74
+
75
+ /**
76
+ * Full server-side validation: blocklist + allowlist.
77
+ * Returns { allowed: true } or { allowed: false, reason: string }.
78
+ * @param {string} command
79
+ * @returns {{ allowed: boolean, reason?: string }}
80
+ */
81
+ function validateExecCommand(command) {
82
+ const trimmed = (command || '').trim();
83
+ if (!trimmed) {
84
+ return { allowed: false, reason: 'Empty command' };
85
+ }
86
+
87
+ const destructive = checkDestructivePatterns(trimmed);
88
+ if (destructive.blocked) {
89
+ return { allowed: false, reason: `Blocked by destructive pattern: ${destructive.pattern}` };
90
+ }
91
+
92
+ if (!isAllowedExecCommand(trimmed)) {
93
+ return { allowed: false, reason: `Command not in server-side allowlist: ${trimmed.slice(0, 80)}` };
94
+ }
95
+
96
+ return { allowed: true };
97
+ }
98
+
99
+ module.exports = {
100
+ DESTRUCTIVE_PATTERNS,
101
+ ALLOWED_EXEC_PREFIXES,
102
+ checkDestructivePatterns,
103
+ isAllowedExecCommand,
104
+ validateExecCommand,
105
+ };