openclaw-node-harness 2.0.3 → 2.1.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 (118) hide show
  1. package/README.md +646 -3
  2. package/bin/hyperagent.mjs +419 -0
  3. package/bin/mesh-agent.js +603 -81
  4. package/bin/mesh-bridge.js +340 -11
  5. package/bin/mesh-deploy-listener.js +119 -97
  6. package/bin/mesh-deploy.js +8 -0
  7. package/bin/mesh-task-daemon.js +1005 -40
  8. package/bin/mesh.js +423 -6
  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 +300 -8
  29. package/lib/circling-parser.js +119 -0
  30. package/lib/hyperagent-store.mjs +652 -0
  31. package/lib/kanban-io.js +59 -10
  32. package/lib/mcp-knowledge/bench.mjs +118 -0
  33. package/lib/mcp-knowledge/core.mjs +528 -0
  34. package/lib/mcp-knowledge/package.json +25 -0
  35. package/lib/mcp-knowledge/server.mjs +245 -0
  36. package/lib/mcp-knowledge/test.mjs +802 -0
  37. package/lib/memory-budget.mjs +261 -0
  38. package/lib/mesh-collab.js +354 -4
  39. package/lib/mesh-harness.js +427 -0
  40. package/lib/mesh-plans.js +13 -5
  41. package/lib/mesh-registry.js +11 -2
  42. package/lib/mesh-tasks.js +67 -0
  43. package/lib/plan-templates.js +226 -0
  44. package/lib/pre-compression-flush.mjs +320 -0
  45. package/lib/role-loader.js +292 -0
  46. package/lib/rule-loader.js +358 -0
  47. package/lib/session-store.mjs +458 -0
  48. package/lib/transcript-parser.mjs +292 -0
  49. package/mission-control/drizzle/soul_schema_update.sql +29 -0
  50. package/mission-control/drizzle.config.ts +1 -4
  51. package/mission-control/package-lock.json +1571 -83
  52. package/mission-control/package.json +6 -2
  53. package/mission-control/scripts/gen-chronology.js +3 -3
  54. package/mission-control/scripts/import-pipeline-v2.js +0 -16
  55. package/mission-control/scripts/import-pipeline.js +0 -15
  56. package/mission-control/src/app/api/cowork/clusters/[id]/members/route.ts +117 -0
  57. package/mission-control/src/app/api/cowork/clusters/[id]/route.ts +84 -0
  58. package/mission-control/src/app/api/cowork/clusters/route.ts +141 -0
  59. package/mission-control/src/app/api/cowork/dispatch/route.ts +128 -0
  60. package/mission-control/src/app/api/cowork/events/route.ts +65 -0
  61. package/mission-control/src/app/api/cowork/intervene/route.ts +259 -0
  62. package/mission-control/src/app/api/cowork/sessions/[id]/route.ts +37 -0
  63. package/mission-control/src/app/api/cowork/sessions/route.ts +64 -0
  64. package/mission-control/src/app/api/diagnostics/route.ts +97 -0
  65. package/mission-control/src/app/api/diagnostics/test-runner/route.ts +990 -0
  66. package/mission-control/src/app/api/mesh/events/route.ts +95 -19
  67. package/mission-control/src/app/api/mesh/identity/route.ts +11 -0
  68. package/mission-control/src/app/api/mesh/tasks/[id]/route.ts +92 -0
  69. package/mission-control/src/app/api/mesh/tasks/route.ts +91 -0
  70. package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +1 -1
  71. package/mission-control/src/app/api/tasks/[id]/route.ts +90 -4
  72. package/mission-control/src/app/api/tasks/route.ts +21 -30
  73. package/mission-control/src/app/cowork/page.tsx +261 -0
  74. package/mission-control/src/app/diagnostics/page.tsx +385 -0
  75. package/mission-control/src/app/graph/page.tsx +26 -0
  76. package/mission-control/src/app/memory/page.tsx +1 -1
  77. package/mission-control/src/app/obsidian/page.tsx +36 -6
  78. package/mission-control/src/app/roadmap/page.tsx +24 -0
  79. package/mission-control/src/app/souls/page.tsx +2 -2
  80. package/mission-control/src/components/board/execution-config.tsx +431 -0
  81. package/mission-control/src/components/board/kanban-board.tsx +75 -9
  82. package/mission-control/src/components/board/kanban-column.tsx +135 -19
  83. package/mission-control/src/components/board/task-card.tsx +55 -2
  84. package/mission-control/src/components/board/unified-task-dialog.tsx +82 -4
  85. package/mission-control/src/components/cowork/cluster-card.tsx +176 -0
  86. package/mission-control/src/components/cowork/create-cluster-dialog.tsx +251 -0
  87. package/mission-control/src/components/cowork/dispatch-form.tsx +423 -0
  88. package/mission-control/src/components/cowork/role-picker.tsx +102 -0
  89. package/mission-control/src/components/cowork/session-card.tsx +284 -0
  90. package/mission-control/src/components/layout/sidebar.tsx +39 -2
  91. package/mission-control/src/lib/__tests__/daily-log.test.ts +82 -0
  92. package/mission-control/src/lib/__tests__/memory-md.test.ts +87 -0
  93. package/mission-control/src/lib/__tests__/mesh-kv-sync.test.ts +465 -0
  94. package/mission-control/src/lib/__tests__/mocks/mock-kv.ts +131 -0
  95. package/mission-control/src/lib/__tests__/status-kanban.test.ts +46 -0
  96. package/mission-control/src/lib/__tests__/task-markdown.test.ts +188 -0
  97. package/mission-control/src/lib/__tests__/wikilinks.test.ts +175 -0
  98. package/mission-control/src/lib/config.ts +58 -0
  99. package/mission-control/src/lib/db/index.ts +69 -0
  100. package/mission-control/src/lib/db/schema.ts +61 -3
  101. package/mission-control/src/lib/hooks.ts +309 -0
  102. package/mission-control/src/lib/memory/entities.ts +3 -2
  103. package/mission-control/src/lib/nats.ts +66 -1
  104. package/mission-control/src/lib/parsers/task-markdown.ts +52 -2
  105. package/mission-control/src/lib/parsers/transcript.ts +4 -4
  106. package/mission-control/src/lib/scheduler.ts +12 -11
  107. package/mission-control/src/lib/sync/mesh-kv.ts +279 -0
  108. package/mission-control/src/lib/sync/tasks.ts +23 -1
  109. package/mission-control/src/lib/task-id.ts +32 -0
  110. package/mission-control/src/lib/tts/index.ts +33 -9
  111. package/mission-control/tsconfig.json +2 -1
  112. package/mission-control/vitest.config.ts +14 -0
  113. package/package.json +15 -2
  114. package/services/service-manifest.json +1 -1
  115. package/skills/cc-godmode/references/agents.md +8 -8
  116. package/workspace-bin/memory-daemon.mjs +199 -5
  117. package/workspace-bin/session-search.mjs +204 -0
  118. 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
@@ -230,6 +256,10 @@ if [ -z "$NODE_ROLE" ]; then
230
256
  NODE_ROLE="worker"
231
257
  fi
232
258
  fi
259
+ if [ "$NODE_ROLE" != "lead" ] && [ "$NODE_ROLE" != "worker" ]; then
260
+ error "Invalid role: $NODE_ROLE (must be 'lead' or 'worker')"
261
+ exit 1
262
+ fi
233
263
  export OPENCLAW_NODE_ROLE="$NODE_ROLE"
234
264
  info "Node role: $NODE_ROLE"
235
265
 
@@ -532,10 +562,54 @@ fi
532
562
  run mkdir -p "$MC_DIR/data"
533
563
 
534
564
  # ============================================================
535
- # 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)
580
+ # ============================================================
581
+
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
536
610
  # ============================================================
537
611
 
538
- step "Step 12: ClawVault"
612
+ step "Step 14: ClawVault"
539
613
 
540
614
  if command -v clawvault >/dev/null 2>&1; then
541
615
  info "ClawVault already installed: $(which clawvault)"
@@ -552,10 +626,10 @@ else
552
626
  fi
553
627
 
554
628
  # ============================================================
555
- # Step 13: Initialize Memory
629
+ # Step 15: Initialize Memory
556
630
  # ============================================================
557
631
 
558
- step "Step 13: Initialize Memory"
632
+ step "Step 15: Initialize Memory"
559
633
 
560
634
  TODAY=$(date +%Y-%m-%d)
561
635
  DAILY_FILE="$WORKSPACE/memory/$TODAY.md"
@@ -646,10 +720,27 @@ MEM
646
720
  fi
647
721
 
648
722
  # ============================================================
649
- # Step 14: Install Services (role-aware, template-based)
723
+ # Step 15.5: HyperAgent Protocol
724
+ # ============================================================
725
+
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)
650
741
  # ============================================================
651
742
 
652
- step "Step 14: Install Services (role=$NODE_ROLE)"
743
+ step "Step 16: Install Services (role=$NODE_ROLE)"
653
744
 
654
745
  MANIFEST="$REPO_DIR/services/service-manifest.json"
655
746
  LAUNCHD_TEMPLATES="$REPO_DIR/services/launchd"
@@ -692,6 +783,9 @@ else
692
783
  if command -v envsubst >/dev/null 2>&1; then
693
784
  envsubst < "$TEMPLATE" > "$DEST"
694
785
  else
786
+ # NOTE: sed delimiter is |. If OPENCLAW_NATS_TOKEN ever contains |
787
+ # (unlikely — tokens are hex/base64), this substitution will break.
788
+ # Prefer envsubst (above) when available; it has no delimiter issue.
695
789
  sed \
696
790
  -e "s|\${HOME}|$HOME|g" \
697
791
  -e "s|\${NODE_BIN}|$NODE_BIN|g" \
@@ -800,10 +894,10 @@ else
800
894
  fi
801
895
 
802
896
  # ============================================================
803
- # Step 15: Mesh Network (optional — if Tailscale detected)
897
+ # Step 17: Mesh Network (optional — if Tailscale detected)
804
898
  # ============================================================
805
899
 
806
- step "Step 15: Mesh Network"
900
+ step "Step 17: Mesh Network"
807
901
 
808
902
  if $SKIP_MESH; then
809
903
  info "Skipped (--skip-mesh flag set by meta-installer)"
@@ -855,6 +949,204 @@ fi
855
949
 
856
950
  fi # end SKIP_MESH else block
857
951
 
952
+ # ============================================================
953
+ # Step 18: Path-Scoped Rules
954
+ # ============================================================
955
+
956
+ step "Step 18: Path-Scoped Rules"
957
+
958
+ RULES_DIR="${OPENCLAW_ROOT}/rules"
959
+ mkdir -p "$RULES_DIR"
960
+
961
+ # install_rule — version-aware rule deployment.
962
+ # Fresh install: copy. Update: compare versions. If source is newer and local
963
+ # was modified (hash mismatch), save as .new instead of overwriting.
964
+ install_rule() {
965
+ local src="$1" dst="$2" name="$3"
966
+ if [ ! -f "$src" ]; then return; fi
967
+
968
+ if [ ! -f "$dst" ]; then
969
+ cp "$src" "$dst"
970
+ info "Installed rule: ${name}"
971
+ return
972
+ fi
973
+
974
+ # Both exist — compare version fields
975
+ local src_ver dst_ver
976
+ src_ver=$(grep -m1 '^version:' "$src" 2>/dev/null | sed 's/version:[[:space:]]*//' || echo "0.0.0")
977
+ dst_ver=$(grep -m1 '^version:' "$dst" 2>/dev/null | sed 's/version:[[:space:]]*//' || echo "0.0.0")
978
+
979
+ if [ "$src_ver" = "$dst_ver" ]; then
980
+ return # Same version, nothing to do
981
+ fi
982
+
983
+ # Different versions — check if user modified the local copy
984
+ local src_hash dst_hash
985
+ if command -v md5sum &>/dev/null; then
986
+ src_hash=$(md5sum "$src" | cut -d' ' -f1)
987
+ dst_hash=$(md5sum "$dst" | cut -d' ' -f1)
988
+ elif command -v md5 &>/dev/null; then
989
+ src_hash=$(md5 -q "$src")
990
+ dst_hash=$(md5 -q "$dst")
991
+ else
992
+ # Can't compare hashes — save as .new to be safe
993
+ cp "$src" "${dst}.new"
994
+ warn "Rule ${name}: new version ${src_ver} available (saved as ${name}.new)"
995
+ return
996
+ fi
997
+
998
+ if [ "$src_hash" = "$dst_hash" ]; then
999
+ # Same content despite different version — just update
1000
+ cp "$src" "$dst"
1001
+ info "Updated rule: ${name} (${dst_ver} → ${src_ver})"
1002
+ else
1003
+ # User-modified — don't overwrite, save as .new
1004
+ cp "$src" "${dst}.new"
1005
+ warn "Rule ${name}: new version ${src_ver} available but local copy modified. Saved as ${name}.new for manual merge."
1006
+ fi
1007
+ }
1008
+
1009
+ # Copy universal rules (always)
1010
+ for rule in security test-standards design-docs git-hygiene; do
1011
+ install_rule "${REPO_DIR}/config/rules/universal/${rule}.md" "${RULES_DIR}/${rule}.md" "${rule}.md"
1012
+ done
1013
+
1014
+ # Detect frameworks and install matching rules
1015
+ if [ -f "package.json" ] || [ -f "${WORKSPACE}/../../package.json" ]; then
1016
+ PKG_FILE="package.json"
1017
+ [ ! -f "$PKG_FILE" ] && PKG_FILE="${WORKSPACE}/../../package.json"
1018
+
1019
+ if [ -f "$PKG_FILE" ]; then
1020
+ # Solidity detection
1021
+ if grep -q '"hardhat"' "$PKG_FILE" 2>/dev/null || [ -f "hardhat.config.js" ] || [ -f "hardhat.config.ts" ] || [ -f "foundry.toml" ]; then
1022
+ install_rule "${REPO_DIR}/config/rules/framework/solidity.md" "${RULES_DIR}/solidity.md" "solidity.md"
1023
+ [ ! -f "${RULES_DIR}/solidity.md.new" ] || true # install_rule handles logging
1024
+ fi
1025
+
1026
+ # TypeScript detection
1027
+ if [ -f "tsconfig.json" ] || [ -f "${WORKSPACE}/../../tsconfig.json" ]; then
1028
+ install_rule "${REPO_DIR}/config/rules/framework/typescript.md" "${RULES_DIR}/typescript.md" "typescript.md"
1029
+ fi
1030
+
1031
+ # Unity detection
1032
+ if [ -d "ProjectSettings" ] || [ -d "Assets" ]; then
1033
+ install_rule "${REPO_DIR}/config/rules/framework/unity.md" "${RULES_DIR}/unity.md" "unity.md"
1034
+ fi
1035
+ fi
1036
+ fi
1037
+
1038
+ info "Rules directory: ${RULES_DIR} ($(ls -1 "$RULES_DIR" 2>/dev/null | wc -l | tr -d ' ') rules)"
1039
+
1040
+ # ============================================================
1041
+ # Step 19: Plan Templates
1042
+ # ============================================================
1043
+
1044
+ step "Step 19: Plan Templates"
1045
+
1046
+ TEMPLATES_DIR="${OPENCLAW_ROOT}/plan-templates"
1047
+ mkdir -p "$TEMPLATES_DIR"
1048
+
1049
+ for tmpl in team-feature team-bugfix team-deploy; do
1050
+ TMPL_SRC="${REPO_DIR}/config/plan-templates/${tmpl}.yaml"
1051
+ TMPL_DST="${TEMPLATES_DIR}/${tmpl}.yaml"
1052
+ if [ -f "$TMPL_SRC" ] && [ ! -f "$TMPL_DST" ]; then
1053
+ cp "$TMPL_SRC" "$TMPL_DST"
1054
+ info "Installed plan template: ${tmpl}.yaml"
1055
+ fi
1056
+ done
1057
+
1058
+ info "Templates directory: ${TEMPLATES_DIR}"
1059
+
1060
+ # ============================================================
1061
+ # Step 20: Claude Code Integration
1062
+ # ============================================================
1063
+
1064
+ step "Step 20: Claude Code Integration"
1065
+
1066
+ # Create .claude directory structure (in workspace root)
1067
+ CLAUDE_DIR="${WORKSPACE}/.claude"
1068
+ mkdir -p "${CLAUDE_DIR}/hooks"
1069
+
1070
+ # Symlink rules directory
1071
+ RULES_LINK="${CLAUDE_DIR}/rules"
1072
+ if [ ! -L "$RULES_LINK" ] && [ ! -d "$RULES_LINK" ]; then
1073
+ ln -s "${RULES_DIR}" "$RULES_LINK"
1074
+ info "Symlinked .claude/rules → ${RULES_DIR}"
1075
+ fi
1076
+
1077
+ # Deploy settings.json — merge hooks into existing if present, never overwrite permissions
1078
+ SETTINGS_SRC="${REPO_DIR}/config/claude-settings.json"
1079
+ SETTINGS_DST="${CLAUDE_DIR}/settings.json"
1080
+ if [ -f "$SETTINGS_SRC" ]; then
1081
+ if [ ! -f "$SETTINGS_DST" ]; then
1082
+ # Fresh install — copy wholesale
1083
+ cp "$SETTINGS_SRC" "$SETTINGS_DST"
1084
+ info "Deployed Claude Code settings.json"
1085
+ elif command -v jq &>/dev/null; then
1086
+ # Existing settings — merge hooks only, preserve user permissions
1087
+ # Strategy: for each hook lifecycle key (SessionStart, PreToolUse, etc.),
1088
+ # append our hook entries if they don't already exist (matched by command string)
1089
+ MERGED=$(jq -s '
1090
+ .[0] as $existing | .[1] as $new |
1091
+ $existing * {
1092
+ hooks: (
1093
+ ($new.hooks // {}) | to_entries | reduce .[] as $entry (
1094
+ ($existing.hooks // {});
1095
+ .[$entry.key] as $current |
1096
+ if $current == null then
1097
+ . + {($entry.key): $entry.value}
1098
+ else
1099
+ # Append hook entries whose command is not already present
1100
+ ($current | map(.hooks) | flatten | map(.command)) as $existing_cmds |
1101
+ ($entry.value | map(
1102
+ .hooks |= [.[] | select(.command as $cmd | $existing_cmds | index($cmd) | not)]
1103
+ | select(.hooks | length > 0)
1104
+ )) as $new_entries |
1105
+ if ($new_entries | length) > 0 then
1106
+ . + {($entry.key): ($current + $new_entries)}
1107
+ else .
1108
+ end
1109
+ end
1110
+ )
1111
+ )
1112
+ }
1113
+ ' "$SETTINGS_DST" "$SETTINGS_SRC")
1114
+ echo "$MERGED" > "$SETTINGS_DST"
1115
+ info "Merged OpenClaw hooks into existing settings.json (permissions preserved)"
1116
+ else
1117
+ # No jq — can't safely merge. Dump patch file for manual merge.
1118
+ cp "$SETTINGS_SRC" "${SETTINGS_DST}.openclaw-hooks"
1119
+ warn "jq not found — hooks config saved to settings.json.openclaw-hooks for manual merge"
1120
+ fi
1121
+ fi
1122
+
1123
+ # Deploy hook scripts
1124
+ for hook in session-start validate-commit validate-push pre-compact session-stop log-agent; do
1125
+ HOOK_SRC="${REPO_DIR}/.claude/hooks/${hook}.sh"
1126
+ HOOK_DST="${CLAUDE_DIR}/hooks/${hook}.sh"
1127
+ if [ -f "$HOOK_SRC" ]; then
1128
+ cp "$HOOK_SRC" "$HOOK_DST"
1129
+ chmod +x "$HOOK_DST"
1130
+ fi
1131
+ done
1132
+ info "Deployed Claude Code hooks"
1133
+
1134
+ # Deploy git hooks (LLM-agnostic enforcement)
1135
+ if [ -d ".git/hooks" ] || [ -d "${WORKSPACE}/../../.git/hooks" ]; then
1136
+ GIT_HOOKS_DIR=".git/hooks"
1137
+ [ ! -d "$GIT_HOOKS_DIR" ] && GIT_HOOKS_DIR="${WORKSPACE}/../../.git/hooks"
1138
+
1139
+ for ghook in pre-commit pre-push; do
1140
+ GHOOK_SRC="${REPO_DIR}/config/git-hooks/${ghook}"
1141
+ GHOOK_DST="${GIT_HOOKS_DIR}/${ghook}"
1142
+ if [ -f "$GHOOK_SRC" ] && [ ! -f "$GHOOK_DST" ]; then
1143
+ cp "$GHOOK_SRC" "$GHOOK_DST"
1144
+ chmod +x "$GHOOK_DST"
1145
+ info "Installed git hook: ${ghook}"
1146
+ fi
1147
+ done
1148
+ fi
1149
+
858
1150
  # ============================================================
859
1151
  # Done!
860
1152
  # ============================================================
@@ -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 };