Skip to content

Commit 4954d38

Browse files
committed
Merge remote-tracking branch 'origin/main' into symphony/pln-202
2 parents f4435ed + 154d804 commit 4954d38

17 files changed

Lines changed: 130 additions & 69 deletions

install.sh

Lines changed: 86 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@
1010
# 3. Installs all 6 plugins globally (user scope)
1111
# 4. Auto-update is enabled by default — plugins stay current automatically
1212
#
13+
# NOTE: The BASH_VERSION check below can be bypassed by setting the BASH_VERSION
14+
# env var before invoking under sh/dash (e.g. BASH_VERSION=x sh install.sh).
15+
# This is a known limitation: the guard is a best-effort hint, not a security boundary.
16+
if [ -z "${BASH_VERSION:-}" ]; then
17+
printf 'Error: This script requires bash. Run: bash install.sh\n or: curl -fsSL https://raw.githubusercontent.com/closedloop-ai/claude-plugins/main/install.sh | bash\n' >&2
18+
exit 1
19+
fi
20+
if [[ "${BASH_VERSINFO[0]:-0}" -lt 3 || ("${BASH_VERSINFO[0]:-0}" -eq 3 && "${BASH_VERSINFO[1]:-0}" -lt 2) ]]; then
21+
printf 'Error: Bash 3.2+ required (found %s)\n' "$BASH_VERSION" >&2
22+
exit 1
23+
fi
1324
set -euo pipefail
1425

1526
# ── Colors ───────────────────────────────────────────────────────────────────
@@ -24,12 +35,26 @@ info() { echo -e "${GREEN}[✓]${NC} $1"; }
2435
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
2536
err() { echo -e "${RED}[✗]${NC} $1"; }
2637
step() { echo -e "${BLUE}[→]${NC} ${BOLD}$1${NC}"; }
38+
snapshot_version() { grep -m1 "^$2 " "$1" 2>/dev/null | awk '{print $2}' || true; }
39+
sanitize_stderr() {
40+
# Strip ANSI color escapes, then drop non-printable control chars.
41+
# Uses bash ANSI-C quoting for a literal ESC so this works under BSD sed (macOS)
42+
# as well as GNU sed. `tr` with octal ranges is POSIX-portable across both.
43+
local esc=$'\033'
44+
sed "s/${esc}\[[0-9;]*[a-zA-Z]//g" "$1" | tr -d '\000-\010\013-\037\177' >&2
45+
}
2746

2847
# ── Constants ────────────────────────────────────────────────────────────────
2948
MARKETPLACE_SOURCE="closedloop-ai/claude-plugins"
3049
MARKETPLACE_NAME="closedloop-ai"
3150
PLUGINS=(bootstrap code code-review judges platform self-learning)
3251

52+
# ── Per-run working directory ────────────────────────────────────────────────
53+
WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/closedloop-install.XXXXXX")
54+
chmod 700 "$WORK_DIR"
55+
_cleanup() { rm -rf "$WORK_DIR"; }
56+
trap _cleanup EXIT
57+
3358
# ── Preflight checks ────────────────────────────────────────────────────────
3459
echo
3560
echo -e "${BOLD}ClosedLoop Claude Plugins Installer${NC}"
@@ -49,8 +74,8 @@ info "Claude Code CLI found: $(claude --version 2>/dev/null || echo 'unknown ver
4974
# Python 3.11+
5075
if command -v python3 &>/dev/null; then
5176
PY_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
52-
PY_MAJOR=$(echo "$PY_VERSION" | cut -d. -f1)
53-
PY_MINOR=$(echo "$PY_VERSION" | cut -d. -f2)
77+
PY_MAJOR="${PY_VERSION%%.*}"
78+
PY_MINOR="${PY_VERSION##*.}"
5479
if [[ "$PY_MAJOR" -gt 3 || ( "$PY_MAJOR" -eq 3 && "$PY_MINOR" -ge 11 ) ]]; then
5580
info "Python $PY_VERSION"
5681
else
@@ -61,23 +86,32 @@ else
6186
fi
6287

6388
# jq
64-
if command -v jq &>/dev/null; then
65-
info "jq found"
89+
if ! command -v jq &>/dev/null; then
90+
err "jq is required but not found."
91+
echo " Install: brew install jq (macOS)"
92+
echo " Install: apt install jq (Debian/Ubuntu)"
93+
exit 1
6694
else
67-
warn "jq not found — some plugin features require it"
68-
echo " Install: brew install jq (macOS) / apt install jq (Linux)"
95+
info "jq found"
6996
fi
7097

7198
echo
7299

73100
# ── Add marketplace ─────────────────────────────────────────────────────────
74101
step "Registering closedloop-ai marketplace..."
75102

76-
if claude plugin marketplace add "$MARKETPLACE_SOURCE" 2>/dev/null; then
77-
info "Marketplace registered: $MARKETPLACE_SOURCE"
103+
_MARKETPLACE_LIST=$(claude plugin marketplace list --json 2>/dev/null)
104+
if [[ -n "$_MARKETPLACE_LIST" ]] \
105+
&& echo "$_MARKETPLACE_LIST" | jq -e --arg name "$MARKETPLACE_NAME" 'any(.name == $name)' &>/dev/null; then
106+
info "Marketplace already registered: $MARKETPLACE_NAME"
78107
else
79-
# May already be registered — not a fatal error
80-
warn "Marketplace may already be registered (continuing)"
108+
[[ -z "$_MARKETPLACE_LIST" ]] && warn "Could not query marketplace list — attempting add anyway"
109+
if claude plugin marketplace add "$MARKETPLACE_SOURCE" 2>"$WORK_DIR/marketplace_err"; then
110+
info "Marketplace registered: $MARKETPLACE_SOURCE"
111+
else
112+
warn "Marketplace add failed:"
113+
sanitize_stderr "$WORK_DIR/marketplace_err"
114+
fi
81115
fi
82116

83117
echo
@@ -86,33 +120,62 @@ echo
86120
step "Installing plugins (user scope)..."
87121

88122
INSTALLED=0
123+
UPDATED=0
124+
UP_TO_DATE=0
89125
FAILED=0
90126

127+
SNAPSHOT_PRE="$WORK_DIR/snapshot_pre"
128+
SNAPSHOT_POST="$WORK_DIR/snapshot_post"
129+
STDERR_FILE="$WORK_DIR/install_err"
130+
131+
claude plugin list --json 2>/dev/null \
132+
| jq -r '.[] | .id + " " + .version' > "$SNAPSHOT_PRE" 2>/dev/null || true
133+
[[ -s "$SNAPSHOT_PRE" ]] || warn "Could not snapshot installed plugins — state detection will be approximate"
134+
135+
SUCCESSFUL_PLUGINS=()
136+
91137
for plugin in "${PLUGINS[@]}"; do
92-
PLUGIN_REF="${plugin}@${MARKETPLACE_NAME}"
93-
if claude plugin install "$PLUGIN_REF" --scope user 2>/dev/null; then
94-
info "Installed: $plugin"
138+
plugin_ref="${plugin}@${MARKETPLACE_NAME}"
139+
if claude plugin install "$plugin_ref" --scope user 2>"$STDERR_FILE"; then
140+
SUCCESSFUL_PLUGINS+=("$plugin_ref")
141+
# Install failed — may already exist; try update instead
142+
elif claude plugin update "$plugin_ref" --scope user 2>"$STDERR_FILE"; then
143+
SUCCESSFUL_PLUGINS+=("$plugin_ref")
144+
else
145+
[[ -s "$STDERR_FILE" ]] && sanitize_stderr "$STDERR_FILE"
146+
warn "Could not install/update: $plugin"
147+
FAILED=$((FAILED + 1))
148+
fi
149+
done
150+
151+
claude plugin list --json 2>/dev/null \
152+
| jq -r '.[] | .id + " " + .version' > "$SNAPSHOT_POST" 2>/dev/null || true
153+
154+
for plugin_ref in "${SUCCESSFUL_PLUGINS[@]+"${SUCCESSFUL_PLUGINS[@]}"}"; do
155+
plugin="${plugin_ref%@*}"
156+
pre_ver=$(snapshot_version "$SNAPSHOT_PRE" "$plugin_ref")
157+
post_ver=$(snapshot_version "$SNAPSHOT_POST" "$plugin_ref")
158+
if [[ -z "$pre_ver" || -z "$post_ver" ]]; then
95159
INSTALLED=$((INSTALLED + 1))
160+
info "Installed: $plugin"
161+
elif [[ "$pre_ver" == "$post_ver" ]]; then
162+
UP_TO_DATE=$((UP_TO_DATE + 1))
163+
info "Already up to date: $plugin"
96164
else
97-
# May already be installed — try to update instead
98-
if claude plugin update "$PLUGIN_REF" --scope user 2>/dev/null; then
99-
info "Updated: $plugin"
100-
INSTALLED=$((INSTALLED + 1))
101-
else
102-
warn "Could not install/update: $plugin"
103-
FAILED=$((FAILED + 1))
104-
fi
165+
UPDATED=$((UPDATED + 1))
166+
info "Updated: $plugin ($pre_ver -> $post_ver)"
105167
fi
106168
done
107169

108170
echo
109171

110172
# ── Summary ──────────────────────────────────────────────────────────────────
173+
TOTAL=$((INSTALLED + UPDATED + UP_TO_DATE + FAILED))
111174
echo "────────────────────────────────────"
112175
if [[ $FAILED -eq 0 ]]; then
113-
echo -e "${GREEN}${BOLD}All $INSTALLED plugins installed successfully!${NC}"
176+
echo -e "${GREEN}${BOLD}All $TOTAL plugins ready ($INSTALLED installed, $UPDATED updated, $UP_TO_DATE already up to date).${NC}"
114177
else
115-
echo -e "${YELLOW}${BOLD}$INSTALLED installed, $FAILED failed${NC}"
178+
echo -e "${YELLOW}${BOLD}$TOTAL plugins processed: $INSTALLED installed, $UPDATED updated, $UP_TO_DATE already up to date, $FAILED failed.${NC}"
116179
fi
117180

118181
echo

plugins/code/agents/plan-importer.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ You import an external markdown plan into the ClosedLoop `plan.json` format. You
2020

2121
- `$CLOSEDLOOP_WORKDIR/plan.json` - Schema-compliant plan JSON
2222
- `$CLOSEDLOOP_WORKDIR/plan.md` - The markdown `content` field value (for human review)
23-
- `$CLOSEDLOOP_WORKDIR/.closedloop/imported-plan` - Marker file written on successful validation
23+
- `$CLOSEDLOOP_WORKDIR/.closedloop-ai/imported-plan` - Marker file written on successful validation
2424

2525
## Process
2626

@@ -174,8 +174,8 @@ Common issues to fix:
174174
On successful validation (`status: "VALID"`), create the imported-plan marker:
175175

176176
```bash
177-
mkdir -p "$CLOSEDLOOP_WORKDIR/.closedloop"
178-
echo "imported" > "$CLOSEDLOOP_WORKDIR/.closedloop/imported-plan"
177+
mkdir -p "$CLOSEDLOOP_WORKDIR/.closedloop-ai"
178+
echo "imported" > "$CLOSEDLOOP_WORKDIR/.closedloop-ai/imported-plan"
179179
```
180180

181181
## Quality Checklist
@@ -189,12 +189,12 @@ echo "imported" > "$CLOSEDLOOP_WORKDIR/.closedloop/imported-plan"
189189
| JSON sync | Structured arrays match markdown content exactly |
190190
| Valid JSON | No trailing commas, newlines escaped as `\n` in content string |
191191
| Validation | `validate_plan.py` returns `status: "VALID"` |
192-
| Marker file | `.closedloop/imported-plan` written on success |
192+
| Marker file | `.closedloop-ai/imported-plan` written on success |
193193

194194
## Completion
195195

196196
Output `<promise>PLAN_IMPORTED</promise>` ONLY when ALL are true:
197197

198198
1. `$CLOSEDLOOP_WORKDIR/plan.json` exists and passed validation (`status: "VALID"`)
199199
2. `$CLOSEDLOOP_WORKDIR/plan.md` exists and contains the markdown content
200-
3. `$CLOSEDLOOP_WORKDIR/.closedloop/imported-plan` marker file exists
200+
3. `$CLOSEDLOOP_WORKDIR/.closedloop-ai/imported-plan` marker file exists

plugins/code/hooks/loop-stop-hook.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ fi
4747

4848
# Source closedloop config from WORKDIR if found
4949
if [[ -n "$CLOSEDLOOP_WORKDIR" ]]; then
50-
CLOSEDLOOP_CONFIG="$CLOSEDLOOP_WORKDIR/.closedloop/config.env"
50+
CLOSEDLOOP_CONFIG="$CLOSEDLOOP_WORKDIR/.closedloop-ai/config.env"
5151
if [[ -f "$CLOSEDLOOP_CONFIG" ]]; then
5252
source "$CLOSEDLOOP_CONFIG"
5353
fi
@@ -90,14 +90,14 @@ STATE_FILE_SUFFIX=$(echo "$AGENT_CONFIG" | jq -r '.state_file_suffix // "loop.lo
9090

9191
echo "$(date): Agent config - validation=$VALIDATION_SCRIPT, max_iter=$MAX_ITERATIONS_DEFAULT, promise=$PROMISE, state_suffix=$STATE_FILE_SUFFIX" >> "$DEBUG_LOG"
9292

93-
# Build state file path (in CLOSEDLOOP_WORKDIR/.closedloop/)
93+
# Build state file path (in CLOSEDLOOP_WORKDIR/.closedloop-ai/)
9494
# Exit early if CLOSEDLOOP_WORKDIR is not set - no loop context
9595
if [[ -z "$CLOSEDLOOP_WORKDIR" ]]; then
9696
echo "$(date): No CLOSEDLOOP_WORKDIR, exiting loop-stop-hook" >> "$DEBUG_LOG"
9797
exit 0
9898
fi
9999

100-
STATE_FILE="$CLOSEDLOOP_WORKDIR/.closedloop/$STATE_FILE_SUFFIX"
100+
STATE_FILE="$CLOSEDLOOP_WORKDIR/.closedloop-ai/$STATE_FILE_SUFFIX"
101101

102102
if [[ ! -f "$STATE_FILE" ]]; then
103103
echo "$(date): No active loop - state file not found: $STATE_FILE" >> "$DEBUG_LOG"

plugins/code/hooks/pretooluse-hook.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ DEBUG_LOG="$CLOSEDLOOP_WORKDIR/.learnings/pretooluse-hook-debug.log"
139139
echo "$(date): PreToolUse hook started, tool=$TOOL_NAME" >> "$DEBUG_LOG"
140140

141141
# Source closedloop config and skip learning injection if disabled
142-
CLOSEDLOOP_CONFIG="$CLOSEDLOOP_WORKDIR/.closedloop/config.env"
142+
CLOSEDLOOP_CONFIG="$CLOSEDLOOP_WORKDIR/.closedloop-ai/config.env"
143143
if [[ -f "$CLOSEDLOOP_CONFIG" ]]; then
144144
source "$CLOSEDLOOP_CONFIG"
145145
fi

plugins/code/hooks/session-end-hook.sh

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,6 @@ fi
6868
# Remove any orphaned .agent-types files in CLOSEDLOOP_WORKDIR (if known)
6969
# ============================================================================
7070

71-
# Fallback: Check CWD's config.env (CLOSEDLOOP_WORKDIR was discovered above before cleanup)
72-
if [[ -z "$CLOSEDLOOP_WORKDIR" ]] && [[ -f "$CWD/.closedloop/config.env" ]]; then
73-
source "$CWD/.closedloop/config.env"
74-
fi
75-
7671
if [[ -n "$CLOSEDLOOP_WORKDIR" ]] && [[ -d "$CLOSEDLOOP_WORKDIR/.agent-types" ]]; then
7772
echo "$(date): Cleaning up agent-types directory: $CLOSEDLOOP_WORKDIR/.agent-types" >> "$DEBUG_LOG"
7873

plugins/code/hooks/subagent-start-hook.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ fi
3535

3636
# Source closedloop config from WORKDIR if found
3737
if [[ -n "$CLOSEDLOOP_WORKDIR" ]]; then
38-
CLOSEDLOOP_CONFIG="$CLOSEDLOOP_WORKDIR/.closedloop/config.env"
38+
CLOSEDLOOP_CONFIG="$CLOSEDLOOP_WORKDIR/.closedloop-ai/config.env"
3939
if [[ -f "$CLOSEDLOOP_CONFIG" ]]; then
4040
source "$CLOSEDLOOP_CONFIG"
4141
fi
@@ -72,13 +72,13 @@ if [[ -f "$LOOP_CONFIG" ]] && [[ -n "$AGENT_TYPE" ]]; then
7272
MAX_ITERATIONS="${CLOSEDLOOP_MAX_ITERATIONS:-$CONFIG_MAX_ITERATIONS}"
7373
PRD_FILE="${CLOSEDLOOP_PRD_FILE:-}"
7474
WORKDIR="${CLOSEDLOOP_WORKDIR:-$CWD}"
75-
STATE_FILE="$WORKDIR/.closedloop/$STATE_FILE_SUFFIX"
75+
STATE_FILE="$WORKDIR/.closedloop-ai/$STATE_FILE_SUFFIX"
7676

7777
echo "$(date): Loop agent detected: $AGENT_TYPE, state_file=$STATE_FILE" >> "$DEBUG_LOG"
7878

7979
# Only create if state file doesn't exist (idempotent)
8080
if [[ ! -f "$STATE_FILE" ]] && [[ -n "$WORKDIR" ]]; then
81-
mkdir -p "$WORKDIR/.closedloop"
81+
mkdir -p "$WORKDIR/.closedloop-ai"
8282

8383
PROMPT="Create a comprehensive implementation plan for the requirements in @${PRD_FILE}.
8484

plugins/code/hooks/subagent-stop-hook.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ fi
3636

3737
# Source closedloop config from WORKDIR if found
3838
if [[ -n "$CLOSEDLOOP_WORKDIR" ]]; then
39-
CLOSEDLOOP_CONFIG="$CLOSEDLOOP_WORKDIR/.closedloop/config.env"
39+
CLOSEDLOOP_CONFIG="$CLOSEDLOOP_WORKDIR/.closedloop-ai/config.env"
4040
if [[ -f "$CLOSEDLOOP_CONFIG" ]]; then
4141
source "$CLOSEDLOOP_CONFIG"
4242
fi

plugins/code/scripts/run-loop.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -732,8 +732,8 @@ $prompt
732732
EOF
733733

734734
# Update config.env with self-learning flag (preserve other keys)
735-
mkdir -p "$WORKDIR/.closedloop"
736-
CONFIG_FILE="$WORKDIR/.closedloop/config.env"
735+
mkdir -p "$WORKDIR/.closedloop-ai"
736+
CONFIG_FILE="$WORKDIR/.closedloop-ai/config.env"
737737
TMP_FILE="${CONFIG_FILE}.tmp.$$"
738738
if [[ -f "$CONFIG_FILE" ]]; then
739739
sed '/^CLOSEDLOOP_SELF_LEARNING=/d' "$CONFIG_FILE" > "$TMP_FILE"

plugins/code/scripts/setup-closedloop.sh

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ while [[ $CURRENT_PID -gt 1 ]]; do
223223
break
224224
fi
225225
# Get parent PID
226-
CURRENT_PID=$(ps -o ppid= -p $CURRENT_PID 2>/dev/null | tr -d ' ')
226+
CURRENT_PID=$(ps -o ppid= -p """""$CURRENT"_"P"I"D" 2>/dev/null | tr -d ' ')
227227
if [[ -z "$CURRENT_PID" ]]; then
228228
break
229229
fi
@@ -290,7 +290,10 @@ else
290290
exit 1
291291
fi
292292

293-
cat > "$WORKDIR/.closedloop/config.env" << EOF
293+
# Write full config to WORKDIR
294+
mkdir -p "$WORKDIR/.closedloop-ai"
295+
296+
cat > "$WORKDIR/.closedloop-ai/config.env" << EOF
294297
CLOSEDLOOP_WORKDIR="$WORKDIR"
295298
CLOSEDLOOP_PRD_FILE="$PRD_FILE"
296299
CLOSEDLOOP_PLAN_FILE="$PLAN_FILE"
@@ -318,5 +321,5 @@ CLOSEDLOOP_ADD_DIR_NAMES="$add_dir_names_joined"
318321
CLOSEDLOOP_REPO_MAP="$repo_map_joined"
319322
EOF
320323

321-
echo "ClosedLoop config written to $WORKDIR/.closedloop/config.env"
322-
cat "$WORKDIR/.closedloop/config.env"
324+
echo "ClosedLoop config written to $WORKDIR/.closedloop-ai/config.env"
325+
cat "$WORKDIR/.closedloop-ai/config.env"

plugins/code/scripts/setup-loop.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ CONFIG_MAX_ITERATIONS=$(echo "$AGENT_CONFIG" | jq -r '.max_iterations // 10')
118118
# Use command line override or config default
119119
MAX_ITERATIONS="${MAX_ITERATIONS:-$CONFIG_MAX_ITERATIONS}"
120120

121-
STATE_FILE="$WORKDIR/.closedloop/$STATE_FILE_SUFFIX"
121+
STATE_FILE="$WORKDIR/.closedloop-ai/$STATE_FILE_SUFFIX"
122122

123123
# Idempotency checks
124124
if [[ -f "$WORKDIR/plan.json" ]]; then
@@ -132,7 +132,7 @@ if [[ -f "$STATE_FILE" ]]; then
132132
fi
133133

134134
# Create state file
135-
mkdir -p "$WORKDIR/.closedloop"
135+
mkdir -p "$WORKDIR/.closedloop-ai"
136136

137137
PROMPT="Create a comprehensive implementation plan for the requirements in @${PRD_FILE}.
138138

0 commit comments

Comments
 (0)