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.
- package/README.md +646 -3
- package/bin/hyperagent.mjs +419 -0
- package/bin/lane-watchdog.js +23 -2
- package/bin/mesh-agent.js +439 -28
- package/bin/mesh-bridge.js +69 -3
- package/bin/mesh-health-publisher.js +41 -1
- package/bin/mesh-task-daemon.js +821 -26
- package/bin/mesh.js +411 -20
- package/config/claude-settings.json +95 -0
- package/config/daemon.json.template +2 -1
- package/config/git-hooks/pre-commit +13 -0
- package/config/git-hooks/pre-push +12 -0
- package/config/harness-rules.json +174 -0
- package/config/plan-templates/team-bugfix.yaml +52 -0
- package/config/plan-templates/team-deploy.yaml +50 -0
- package/config/plan-templates/team-feature.yaml +71 -0
- package/config/roles/qa-engineer.yaml +36 -0
- package/config/roles/solidity-dev.yaml +51 -0
- package/config/roles/tech-architect.yaml +36 -0
- package/config/rules/framework/solidity.md +22 -0
- package/config/rules/framework/typescript.md +21 -0
- package/config/rules/framework/unity.md +21 -0
- package/config/rules/universal/design-docs.md +18 -0
- package/config/rules/universal/git-hygiene.md +18 -0
- package/config/rules/universal/security.md +19 -0
- package/config/rules/universal/test-standards.md +19 -0
- package/identity/DELEGATION.md +6 -6
- package/install.sh +296 -10
- package/lib/agent-activity.js +2 -2
- package/lib/circling-parser.js +119 -0
- package/lib/exec-safety.js +105 -0
- package/lib/hyperagent-store.mjs +652 -0
- package/lib/kanban-io.js +24 -31
- package/lib/llm-providers.js +16 -0
- package/lib/mcp-knowledge/bench.mjs +118 -0
- package/lib/mcp-knowledge/core.mjs +530 -0
- package/lib/mcp-knowledge/package.json +25 -0
- package/lib/mcp-knowledge/server.mjs +252 -0
- package/lib/mcp-knowledge/test.mjs +802 -0
- package/lib/memory-budget.mjs +261 -0
- package/lib/mesh-collab.js +483 -165
- package/lib/mesh-harness.js +427 -0
- package/lib/mesh-plans.js +79 -50
- package/lib/mesh-tasks.js +132 -49
- package/lib/nats-resolve.js +4 -4
- package/lib/plan-templates.js +226 -0
- package/lib/pre-compression-flush.mjs +322 -0
- package/lib/role-loader.js +292 -0
- package/lib/rule-loader.js +358 -0
- package/lib/session-store.mjs +461 -0
- package/lib/transcript-parser.mjs +292 -0
- package/mission-control/drizzle/soul_schema_update.sql +29 -0
- package/mission-control/drizzle.config.ts +1 -4
- package/mission-control/package-lock.json +1571 -83
- package/mission-control/package.json +6 -2
- package/mission-control/scripts/gen-chronology.js +3 -3
- package/mission-control/scripts/import-pipeline-v2.js +0 -16
- package/mission-control/scripts/import-pipeline.js +0 -15
- package/mission-control/src/app/api/cowork/clusters/[id]/members/route.ts +117 -0
- package/mission-control/src/app/api/cowork/clusters/[id]/route.ts +84 -0
- package/mission-control/src/app/api/cowork/clusters/route.ts +141 -0
- package/mission-control/src/app/api/cowork/dispatch/route.ts +128 -0
- package/mission-control/src/app/api/cowork/events/route.ts +65 -0
- package/mission-control/src/app/api/cowork/intervene/route.ts +259 -0
- package/mission-control/src/app/api/cowork/sessions/[id]/route.ts +37 -0
- package/mission-control/src/app/api/cowork/sessions/route.ts +64 -0
- package/mission-control/src/app/api/diagnostics/route.ts +97 -0
- package/mission-control/src/app/api/diagnostics/test-runner/route.ts +990 -0
- package/mission-control/src/app/api/memory/search/route.ts +6 -3
- package/mission-control/src/app/api/mesh/events/route.ts +95 -19
- package/mission-control/src/app/api/mesh/identity/route.ts +11 -0
- package/mission-control/src/app/api/mesh/tasks/[id]/route.ts +92 -0
- package/mission-control/src/app/api/mesh/tasks/route.ts +91 -0
- package/mission-control/src/app/api/souls/[id]/evolution/route.ts +21 -5
- package/mission-control/src/app/api/souls/[id]/prompt/route.ts +7 -1
- package/mission-control/src/app/api/souls/[id]/propagate/route.ts +14 -2
- package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +8 -2
- package/mission-control/src/app/api/tasks/[id]/route.ts +90 -4
- package/mission-control/src/app/api/tasks/route.ts +21 -30
- package/mission-control/src/app/api/workspace/read/route.ts +11 -0
- package/mission-control/src/app/cowork/page.tsx +261 -0
- package/mission-control/src/app/diagnostics/page.tsx +385 -0
- package/mission-control/src/app/graph/page.tsx +26 -0
- package/mission-control/src/app/memory/page.tsx +1 -1
- package/mission-control/src/app/obsidian/page.tsx +36 -6
- package/mission-control/src/app/roadmap/page.tsx +24 -0
- package/mission-control/src/app/souls/page.tsx +2 -2
- package/mission-control/src/components/board/execution-config.tsx +431 -0
- package/mission-control/src/components/board/kanban-board.tsx +75 -9
- package/mission-control/src/components/board/kanban-column.tsx +135 -19
- package/mission-control/src/components/board/task-card.tsx +55 -2
- package/mission-control/src/components/board/unified-task-dialog.tsx +82 -4
- package/mission-control/src/components/cowork/cluster-card.tsx +176 -0
- package/mission-control/src/components/cowork/create-cluster-dialog.tsx +251 -0
- package/mission-control/src/components/cowork/dispatch-form.tsx +423 -0
- package/mission-control/src/components/cowork/role-picker.tsx +102 -0
- package/mission-control/src/components/cowork/session-card.tsx +284 -0
- package/mission-control/src/components/layout/sidebar.tsx +39 -2
- package/mission-control/src/lib/__tests__/daily-log.test.ts +82 -0
- package/mission-control/src/lib/__tests__/memory-md.test.ts +87 -0
- package/mission-control/src/lib/__tests__/mesh-kv-sync.test.ts +465 -0
- package/mission-control/src/lib/__tests__/mocks/mock-kv.ts +131 -0
- package/mission-control/src/lib/__tests__/status-kanban.test.ts +46 -0
- package/mission-control/src/lib/__tests__/task-markdown.test.ts +188 -0
- package/mission-control/src/lib/__tests__/wikilinks.test.ts +175 -0
- package/mission-control/src/lib/config.ts +67 -0
- package/mission-control/src/lib/db/index.ts +85 -1
- package/mission-control/src/lib/db/schema.ts +61 -3
- package/mission-control/src/lib/hooks.ts +309 -0
- package/mission-control/src/lib/memory/entities.ts +3 -2
- package/mission-control/src/lib/memory/extract.ts +2 -1
- package/mission-control/src/lib/memory/retrieval.ts +3 -2
- package/mission-control/src/lib/nats.ts +66 -1
- package/mission-control/src/lib/parsers/task-markdown.ts +52 -2
- package/mission-control/src/lib/parsers/transcript.ts +4 -4
- package/mission-control/src/lib/scheduler.ts +12 -11
- package/mission-control/src/lib/sync/mesh-kv.ts +279 -0
- package/mission-control/src/lib/sync/tasks.ts +23 -1
- package/mission-control/src/lib/task-id.ts +32 -0
- package/mission-control/src/lib/tts/index.ts +33 -9
- package/mission-control/src/middleware.ts +82 -0
- package/mission-control/tsconfig.json +2 -1
- package/mission-control/vitest.config.ts +14 -0
- package/package.json +15 -2
- package/services/launchd/ai.openclaw.log-rotate.plist +11 -0
- package/services/launchd/ai.openclaw.mesh-deploy-listener.plist +4 -0
- package/services/launchd/ai.openclaw.mesh-health-publisher.plist +4 -0
- package/services/launchd/ai.openclaw.mission-control.plist +1 -1
- package/services/service-manifest.json +1 -1
- package/skills/cc-godmode/references/agents.md +8 -8
- package/uninstall.sh +37 -9
- package/workspace-bin/memory-daemon.mjs +199 -5
- package/workspace-bin/session-search.mjs +204 -0
- 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:
|
|
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
|
|
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
|
|
629
|
+
# Step 15: Initialize Memory
|
|
560
630
|
# ============================================================
|
|
561
631
|
|
|
562
|
-
step "Step
|
|
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
|
|
723
|
+
# Step 15.5: HyperAgent Protocol
|
|
654
724
|
# ============================================================
|
|
655
725
|
|
|
656
|
-
step "Step
|
|
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
|
-
|
|
687
|
-
|
|
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
|
|
898
|
+
# Step 17: Mesh Network (optional — if Tailscale detected)
|
|
811
899
|
# ============================================================
|
|
812
900
|
|
|
813
|
-
step "Step
|
|
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
|
# ============================================================
|
package/lib/agent-activity.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
+
};
|