specweave 1.0.300 → 1.0.301

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.
@@ -0,0 +1,126 @@
1
+ #!/bin/bash
2
+ # Integration test: scored selection + enriched feedback (T-012, AC-US1-01, AC-US2-01, AC-US3-01)
3
+ # Creates a mock .specweave/ project, scores increments, and verifies enrichment helpers.
4
+
5
+ set -e
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ SCORE_SCRIPT="$SCRIPT_DIR/../lib/score-increment.sh"
9
+ STOP_HOOK="$SCRIPT_DIR/../stop-auto-v5.sh"
10
+ TMPDIR_ROOT=$(mktemp -d)
11
+ trap 'rm -rf "$TMPDIR_ROOT"' EXIT
12
+
13
+ PASS=0; FAIL=0
14
+
15
+ assert_eq() {
16
+ if [ "$1" = "$2" ]; then
17
+ echo " ✓ $3"; PASS=$((PASS+1))
18
+ else
19
+ echo " ✗ $3 (expected '$2', got '$1')"; FAIL=$((FAIL+1))
20
+ fi
21
+ }
22
+
23
+ assert_gt() {
24
+ if [ "$1" -gt "$2" ]; then
25
+ echo " ✓ $3 ($1 > $2)"; PASS=$((PASS+1))
26
+ else
27
+ echo " ✗ $3 (expected $1 > $2)"; FAIL=$((FAIL+1))
28
+ fi
29
+ }
30
+
31
+ assert_contains() {
32
+ if echo "$1" | grep -q "$2"; then
33
+ echo " ✓ $3"; PASS=$((PASS+1))
34
+ else
35
+ echo " ✗ $3 (expected '$1' to contain '$2')"; FAIL=$((FAIL+1))
36
+ fi
37
+ }
38
+
39
+ echo "Integration: scored selection + enriched feedback"
40
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
41
+
42
+ # Setup mock .specweave/ structure
43
+ SW="$TMPDIR_ROOT/.specweave"
44
+ INC_DIR="$SW/increments"
45
+ STATE_DIR="$SW/state"
46
+ LOGS_DIR="$SW/logs"
47
+ mkdir -p "$INC_DIR/0100-user-auth" "$INC_DIR/0101-deploy-pipeline" "$STATE_DIR" "$LOGS_DIR"
48
+
49
+ # Create increment 0100: auth
50
+ echo '{"title":"User Authentication Feature","status":"active","lastActivity":"2026-02-18T10:00:00Z"}' \
51
+ > "$INC_DIR/0100-user-auth/metadata.json"
52
+ cat > "$INC_DIR/0100-user-auth/spec.md" << 'EOF'
53
+ # User Authentication Feature
54
+
55
+ Implement login, signup, and OAuth for user authentication.
56
+ EOF
57
+ cat > "$INC_DIR/0100-user-auth/tasks.md" << 'EOF'
58
+ ### T-001: Add login endpoint
59
+ **Status**: [x] completed
60
+ ### T-002: Add signup endpoint
61
+ **Status**: [x] completed
62
+ ### T-003: OAuth integration
63
+ **Status**: [ ] pending
64
+ ### T-004: Session management
65
+ **Status**: [ ] pending
66
+ EOF
67
+
68
+ # Create increment 0101: deploy
69
+ echo '{"title":"CI/CD Deploy Pipeline","status":"active","lastActivity":"2026-02-17T10:00:00Z"}' \
70
+ > "$INC_DIR/0101-deploy-pipeline/metadata.json"
71
+ cat > "$INC_DIR/0101-deploy-pipeline/spec.md" << 'EOF'
72
+ # CI/CD Deploy Pipeline
73
+
74
+ Setup continuous integration and deployment pipeline with Docker and GitHub Actions.
75
+ EOF
76
+ cat > "$INC_DIR/0101-deploy-pipeline/tasks.md" << 'EOF'
77
+ ### T-001: Create Dockerfile
78
+ **Status**: [x] completed
79
+ ### T-002: Configure GitHub Actions
80
+ **Status**: [ ] pending
81
+ ### T-003: Set up staging environment
82
+ **Status**: [ ] pending
83
+ EOF
84
+
85
+ # TC-015: Scored selection — auth scores higher for "fix authentication"
86
+ s_auth=$(bash "$SCORE_SCRIPT" "$INC_DIR/0100-user-auth" "fix authentication" 2>/dev/null)
87
+ s_deploy=$(bash "$SCORE_SCRIPT" "$INC_DIR/0101-deploy-pipeline" "fix authentication" 2>/dev/null)
88
+ assert_gt "$s_auth" "$s_deploy" "TC-015a: auth scores higher than deploy for auth prompt"
89
+
90
+ # TC-015b: Deploy scores higher for "deploy pipeline docker"
91
+ s_auth2=$(bash "$SCORE_SCRIPT" "$INC_DIR/0100-user-auth" "deploy pipeline docker" 2>/dev/null)
92
+ s_deploy2=$(bash "$SCORE_SCRIPT" "$INC_DIR/0101-deploy-pipeline" "deploy pipeline docker" 2>/dev/null)
93
+ assert_gt "$s_deploy2" "$s_auth2" "TC-015b: deploy scores higher for deploy prompt"
94
+
95
+ # TC-015c: userGoal wiring — verify logic matches setup-auto.sh
96
+ AUTO_MODE_FILE="$STATE_DIR/auto-mode.json"
97
+ PROMPT="fix authentication"
98
+ if [ -f "$AUTO_MODE_FILE" ]; then
99
+ if [ -n "$PROMPT" ]; then
100
+ _UPDATED=$(jq --arg g "$PROMPT" '.userGoal = $g' "$AUTO_MODE_FILE" 2>/dev/null)
101
+ else
102
+ _UPDATED=$(jq '.userGoal = null' "$AUTO_MODE_FILE" 2>/dev/null)
103
+ fi
104
+ [ -n "$_UPDATED" ] && echo "$_UPDATED" > "$AUTO_MODE_FILE"
105
+ elif [ -n "$PROMPT" ]; then
106
+ jq -n --arg g "$PROMPT" '{"active":false,"userGoal":$g}' > "$AUTO_MODE_FILE"
107
+ fi
108
+ goal=$(jq -r '.userGoal' "$AUTO_MODE_FILE")
109
+ assert_eq "$goal" "fix authentication" "TC-015c: userGoal written to auto-mode.json"
110
+
111
+ # TC-015d: Stop hook enrichment — source hook, test helpers
112
+ export PROJECT_ROOT="$TMPDIR_ROOT"
113
+ export __STOP_AUTO_V5_SOURCED=1
114
+ source "$STOP_HOOK" 2>/dev/null
115
+
116
+ next_auth=$(get_next_task_title "$INC_DIR/0100-user-auth/tasks.md")
117
+ assert_eq "$next_auth" "OAuth integration" "TC-015d: enriched next task from auth increment"
118
+
119
+ done_auth=$(count_completed_tasks "$INC_DIR/0100-user-auth/tasks.md")
120
+ pending_auth=$(count_pending_tasks "$INC_DIR/0100-user-auth/tasks.md")
121
+ assert_eq "$done_auth" "2" "TC-015e: auth increment done count = 2"
122
+ assert_eq "$pending_auth" "2" "TC-015f: auth increment pending count = 2"
123
+
124
+ echo ""
125
+ echo "Results: $PASS passed, $FAIL failed"
126
+ [ "$FAIL" -eq 0 ] || exit 1
@@ -0,0 +1,128 @@
1
+ #!/bin/bash
2
+ # Tests for enriched stop hook feedback (AC-US3-01, AC-US3-02, AC-US3-03, AC-US4-01, AC-US4-03)
3
+ # Sources stop-auto-v5.sh to load helper functions only.
4
+
5
+ set -e
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ STOP_HOOK="$SCRIPT_DIR/../stop-auto-v5.sh"
9
+ TMPDIR_ROOT=$(mktemp -d)
10
+ trap 'rm -rf "$TMPDIR_ROOT"' EXIT
11
+
12
+ PASS=0; FAIL=0
13
+
14
+ assert_eq() {
15
+ if [ "$1" = "$2" ]; then
16
+ echo " ✓ $3"; PASS=$((PASS+1))
17
+ else
18
+ echo " ✗ $3 (expected '$2', got '$1')"; FAIL=$((FAIL+1))
19
+ fi
20
+ }
21
+
22
+ assert_ge() {
23
+ if [ "$1" -ge "$2" ]; then
24
+ echo " ✓ $3 ($1 >= $2)"; PASS=$((PASS+1))
25
+ else
26
+ echo " ✗ $3 (expected >= $2, got $1)"; FAIL=$((FAIL+1))
27
+ fi
28
+ }
29
+
30
+ # Set up minimal .specweave structure so sourcing succeeds
31
+ export PROJECT_ROOT="$TMPDIR_ROOT"
32
+ mkdir -p "$TMPDIR_ROOT/.specweave/logs" "$TMPDIR_ROOT/.specweave/state" "$TMPDIR_ROOT/.specweave/increments"
33
+
34
+ # Source only (loads helper functions, skips main execution)
35
+ export __STOP_AUTO_V5_SOURCED=1
36
+ source "$STOP_HOOK" 2>/dev/null
37
+
38
+ echo "stop-auto-v5.sh enrichment function tests"
39
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
40
+
41
+ # Create a tasks.md with known counts
42
+ TASKS_FILE="$TMPDIR_ROOT/tasks.md"
43
+ cat > "$TASKS_FILE" << 'EOF'
44
+ ### T-001: Setup database
45
+ **User Story**: US-001 | **Status**: [x] completed
46
+
47
+ ### T-002: Implement login
48
+ **User Story**: US-001 | **Status**: [x] completed
49
+
50
+ ### T-003: Add unit tests
51
+ **User Story**: US-001 | **Status**: [x] completed
52
+
53
+ ### T-004: Write integration tests
54
+ **User Story**: US-001 | **Status**: [x] completed
55
+
56
+ ### T-005: Deploy to staging
57
+ **User Story**: US-001 | **Status**: [x] completed
58
+
59
+ ### T-006: Configure auth middleware
60
+ **User Story**: US-001 | **Status**: [ ] pending
61
+
62
+ ### T-007: Add error handling
63
+ **User Story**: US-001 | **Status**: [ ] pending
64
+
65
+ ### T-008: Final review and cleanup
66
+ **User Story**: US-001 | **Status**: [ ] pending
67
+ EOF
68
+
69
+ # TC-010: Block message includes next task title (AC-US3-01)
70
+ result=$(get_next_task_title "$TASKS_FILE")
71
+ assert_eq "$result" "Configure auth middleware" "TC-010: get_next_task_title returns first pending task"
72
+
73
+ # TC-011: count_completed_tasks counts [x] entries
74
+ done_count=$(count_completed_tasks "$TASKS_FILE")
75
+ assert_eq "$done_count" "5" "TC-011: count_completed_tasks = 5"
76
+
77
+ # TC-012: count_pending_tasks counts [ ] entries (AC-US3-03)
78
+ pending_count=$(count_pending_tasks "$TASKS_FILE")
79
+ assert_eq "$pending_count" "3" "TC-012: count_pending_tasks = 3"
80
+
81
+ # TC-012b: Progress fraction calculation
82
+ total=$((done_count + pending_count))
83
+ assert_eq "$total" "8" "TC-012b: total = done + pending = 8"
84
+
85
+ # TC-013: Empty tasks file → zeros
86
+ EMPTY_FILE="$TMPDIR_ROOT/empty.md"
87
+ touch "$EMPTY_FILE"
88
+ done_empty=$(count_completed_tasks "$EMPTY_FILE")
89
+ pending_empty=$(count_pending_tasks "$EMPTY_FILE")
90
+ assert_eq "$done_empty" "0" "TC-013: empty file → 0 completed"
91
+ assert_eq "$pending_empty" "0" "TC-013b: empty file → 0 pending"
92
+
93
+ # TC-013c: get_next_task_title on empty file → empty string
94
+ next_empty=$(get_next_task_title "$EMPTY_FILE")
95
+ assert_eq "$next_empty" "" "TC-013c: empty file → no next title"
96
+
97
+ # TC-014: Missing file → zeros
98
+ done_missing=$(count_completed_tasks "/nonexistent/tasks.md")
99
+ assert_eq "$done_missing" "0" "TC-014: missing file → 0 completed"
100
+
101
+ # TC-015: get_next_task_title with all completed → empty
102
+ ALL_DONE_FILE="$TMPDIR_ROOT/all-done.md"
103
+ cat > "$ALL_DONE_FILE" << 'EOF'
104
+ ### T-001: Done task one
105
+ **Status**: [x] completed
106
+
107
+ ### T-002: Done task two
108
+ **Status**: [x] completed
109
+ EOF
110
+ next_done=$(get_next_task_title "$ALL_DONE_FILE")
111
+ assert_eq "$next_done" "" "TC-015: all done → no next title"
112
+
113
+ # TC-016: get_next_task_title with task without title pattern
114
+ MIXED_FILE="$TMPDIR_ROOT/mixed.md"
115
+ cat > "$MIXED_FILE" << 'EOF'
116
+ ### T-001: First Task Title
117
+ **Status**: [x] completed
118
+ ### T-002: Second Task Title
119
+ **Status**: [ ] pending
120
+ ### T-003: Third Task Title
121
+ **Status**: [ ] pending
122
+ EOF
123
+ next_mixed=$(get_next_task_title "$MIXED_FILE")
124
+ assert_eq "$next_mixed" "Second Task Title" "TC-016: returns first pending task title"
125
+
126
+ echo ""
127
+ echo "Results: $PASS passed, $FAIL failed"
128
+ [ "$FAIL" -eq 0 ] || exit 1
@@ -5,10 +5,10 @@
5
5
  # Purpose: Auto-load plugins, discipline validation, context injection, instant command execution
6
6
  #
7
7
  # FEATURES:
8
- # - v1.0.278: DIRECT PLUGIN COPY - sw-* plugins installed via direct file copy instead of
9
- # npx vskill (which was broken for all production users). Uses install_plugin_direct() that
10
- # reads marketplace.json, copies source dir to ~/.claude/commands/<name>/, and fixes .sh
11
- # permissions. No external dependencies. vskill.lock fast-path still used for skip check.
8
+ # - v1.0.279: VSKILL INSTALL - sw-* plugins installed via vskill at project scope.
9
+ # Uses install_plugin_via_vskill(): node <vskill-cli> install <specweave-dir>
10
+ # --plugin <name> --plugin-dir <dir> --force, run from SW_PROJECT_ROOT.
11
+ # Installs to .claude/commands/<name>/ (project scope). Never ~/.claude/commands/.
12
12
  # - v1.0.201: LSP CLI FALLBACK INSTRUCTIONS - When LSP requested, instruct Claude to use
13
13
  # `specweave lsp` commands instead of Grep. These use TsServerClient for REAL semantic
14
14
  # analysis. Key fix: "find references" now gets semantic refs, not text matches!
@@ -266,61 +266,72 @@ fi
266
266
 
267
267
  if [[ "$SCOPE_GUARD_RUN" == "true" ]] && command -v jq >/dev/null 2>&1 && command -v claude >/dev/null 2>&1; then
268
268
  USER_SETTINGS="$HOME/.claude/settings.json"
269
+ PROJECT_SETTINGS="${SW_PROJECT_ROOT}/.claude/settings.json"
269
270
 
271
+ # ---- 1. Clean user-level settings ----
272
+ # Remove: sw-* domain plugins at user scope (should be project-scoped via vskill)
273
+ # any *@claude-plugins-official (never allowed)
274
+ # Exempt: sw@specweave (core plugin, user-scoped by design)
270
275
  if [[ -f "$USER_SETTINGS" ]]; then
271
- # Find SpecWeave domain plugins and LSP plugins at user level
272
- # Exempt: sw@specweave (core plugin, intentionally user-scoped)
273
- POLLUTED_PLUGINS=$(jq -r '
276
+ POLLUTED_USER=$(jq -r '
274
277
  .enabledPlugins // {} | to_entries[]
275
278
  | select(
276
279
  (.key | test("^sw-.*@specweave$")) or
277
- (.key | test("-lsp@"))
280
+ (.key | test("@claude-plugins-official$"))
278
281
  )
279
282
  | .key
280
283
  ' "$USER_SETTINGS" 2>/dev/null)
281
284
 
282
- if [[ -n "$POLLUTED_PLUGINS" ]]; then
283
- MIGRATED=""
284
- for plugin_key in $POLLUTED_PLUGINS; do
285
- # Uninstall from user scope
285
+ if [[ -n "$POLLUTED_USER" ]]; then
286
+ REMOVED=""
287
+ REINSTALLED=""
288
+ for plugin_key in $POLLUTED_USER; do
286
289
  if timeout 5 claude plugin uninstall "$plugin_key" >/dev/null 2>&1; then
287
- # v1.0.278: sw-* plugins reinstall via direct copy, LSP plugins via claude CLI
288
290
  if [[ "$plugin_key" == sw-*@specweave ]]; then
289
- # Extract plugin name from "sw-name@specweave" format
291
+ # Reinstall sw-* at project scope via vskill
290
292
  _sw_name="${plugin_key%%@*}"
291
- if install_plugin_direct "$_sw_name"; then
292
- [[ -n "$MIGRATED" ]] && MIGRATED="$MIGRATED, "
293
- MIGRATED="${MIGRATED}${plugin_key}"
294
- fi
295
- else
296
- # LSP and other plugins: reinstall via claude CLI at project scope
297
- if timeout 10 claude plugin install "$plugin_key" --scope project >/dev/null 2>&1; then
298
- [[ -n "$MIGRATED" ]] && MIGRATED="$MIGRATED, "
299
- MIGRATED="${MIGRATED}${plugin_key}"
293
+ if install_plugin_via_vskill "$_sw_name"; then
294
+ [[ -n "$REINSTALLED" ]] && REINSTALLED="$REINSTALLED, "
295
+ REINSTALLED="${REINSTALLED}${plugin_key}"
300
296
  fi
301
297
  fi
298
+ # *@claude-plugins-official: uninstall only — never reinstall
299
+ [[ -n "$REMOVED" ]] && REMOVED="$REMOVED, "
300
+ REMOVED="${REMOVED}${plugin_key}"
302
301
  fi
303
302
  done
304
303
 
305
- if [[ -n "$MIGRATED" ]]; then
306
- echo "[$(date -Iseconds)] scope-guard | migrated userproject: $MIGRATED" >> "$SW_PROJECT_ROOT/.specweave/state/hook.log" 2>/dev/null || true
304
+ if [[ -n "$REMOVED" ]]; then
305
+ echo "[$(date -Iseconds)] scope-guard | removed user-level: $REMOVED | reinstalled@project: ${REINSTALLED:-none}" >> "$SW_PROJECT_ROOT/.specweave/state/hook.log" 2>/dev/null || true
307
306
  fi
308
307
 
309
- # CRITICAL FIX: Restore sw@specweave enabled state after uninstall operations
310
- # The `claude plugin uninstall` commands above may corrupt ~/.claude/settings.json
311
- # and disable sw@specweave as collateral damage. Re-enable it explicitly.
312
- if [[ -f "$USER_SETTINGS" ]]; then
313
- SW_ENABLED=$(jq -r '.enabledPlugins."sw@specweave" // "not_set"' "$USER_SETTINGS" 2>/dev/null)
314
- if [[ "$SW_ENABLED" != "true" ]]; then
315
- # Re-enable core plugin (preserves all other settings)
316
- jq '.enabledPlugins."sw@specweave" = true' "$USER_SETTINGS" > "${USER_SETTINGS}.tmp" 2>/dev/null && \
317
- mv "${USER_SETTINGS}.tmp" "$USER_SETTINGS" 2>/dev/null || true
318
- echo "[$(date -Iseconds)] scope-guard | restored sw@specweave enabled state" >> "$SW_PROJECT_ROOT/.specweave/state/hook.log" 2>/dev/null || true
319
- fi
308
+ # Restore sw@specweave core plugin enabled state (uninstall may collateral-damage it)
309
+ SW_ENABLED=$(jq -r '.enabledPlugins."sw@specweave" // "not_set"' "$USER_SETTINGS" 2>/dev/null)
310
+ if [[ "$SW_ENABLED" != "true" ]]; then
311
+ jq '.enabledPlugins."sw@specweave" = true' "$USER_SETTINGS" > "${USER_SETTINGS}.tmp" 2>/dev/null && \
312
+ mv "${USER_SETTINGS}.tmp" "$USER_SETTINGS" 2>/dev/null || true
313
+ echo "[$(date -Iseconds)] scope-guard | restored sw@specweave" >> "$SW_PROJECT_ROOT/.specweave/state/hook.log" 2>/dev/null || true
320
314
  fi
321
315
  fi
322
316
  fi
323
317
 
318
+ # ---- 2. Clean project-level settings ----
319
+ # Remove any *@claude-plugins-official from project settings — never allowed
320
+ if [[ -f "$PROJECT_SETTINGS" ]]; then
321
+ POLLUTED_PROJECT=$(jq -r '
322
+ .enabledPlugins // {} | to_entries[]
323
+ | select(.key | test("@claude-plugins-official$"))
324
+ | .key
325
+ ' "$PROJECT_SETTINGS" 2>/dev/null)
326
+
327
+ if [[ -n "$POLLUTED_PROJECT" ]]; then
328
+ for plugin_key in $POLLUTED_PROJECT; do
329
+ timeout 5 claude plugin uninstall "$plugin_key" >/dev/null 2>&1 || true
330
+ done
331
+ echo "[$(date -Iseconds)] scope-guard | removed project official plugins: $POLLUTED_PROJECT" >> "$SW_PROJECT_ROOT/.specweave/state/hook.log" 2>/dev/null || true
332
+ fi
333
+ fi
334
+
324
335
  # Write today's marker
325
336
  mkdir -p "$(dirname "$SCOPE_GUARD_MARKER")" 2>/dev/null
326
337
  date +%Y-%m-%d > "$SCOPE_GUARD_MARKER" 2>/dev/null || true
@@ -472,85 +483,59 @@ check_plugin_in_vskill_lock() {
472
483
  fi
473
484
  }
474
485
 
475
- # Helper: Install sw-* plugin via direct copy (v1.0.278)
476
- # Copies plugin source dir to ~/.claude/commands/<name>/ and fixes hook permissions.
477
- # No external dependencies (replaces npx vskill shell-out).
486
+ # Helper: Install sw-* plugin via vskill (v1.0.279)
487
+ # Uses: node <vskill-cli> install <specweave-dir> --plugin <name> --plugin-dir <specweave-dir> --force
488
+ # Installs to project scope: ${SW_PROJECT_ROOT}/.claude/commands/<name>/
478
489
  # Args: $1=plugin name (e.g., "sw-frontend")
479
490
  # Returns: 0 if installed successfully, 1 if failed
480
491
  # Sets VSKILL_INSTALL_OUTPUT with status message
481
- install_plugin_direct() {
492
+ install_plugin_via_vskill() {
482
493
  local plugin="$1"
483
- local plugin_dir="${HOME}/.claude/plugins/marketplaces/specweave"
484
- local marketplace_json="$plugin_dir/.claude-plugin/marketplace.json"
485
-
486
- # Verify marketplace directory exists
487
- if [[ ! -d "$plugin_dir" ]] || [[ ! -f "$marketplace_json" ]]; then
488
- VSKILL_INSTALL_OUTPUT="marketplace directory not found at $plugin_dir"
494
+ local specweave_dir="${HOME}/.claude/plugins/marketplaces/specweave"
495
+ local project_dir="${SW_PROJECT_ROOT:-$PWD}"
496
+
497
+ # Find node binary
498
+ local node_bin
499
+ node_bin=$(command -v node 2>/dev/null)
500
+ if [[ -z "$node_bin" ]]; then
501
+ VSKILL_INSTALL_OUTPUT="node not found in PATH"
489
502
  return 1
490
503
  fi
491
504
 
492
- # Resolve plugin source directory from marketplace.json
493
- local source_rel=""
494
- if command -v jq >/dev/null 2>&1; then
495
- source_rel=$(jq -r --arg name "$plugin" '.plugins[] | select(.name == $name) | .source' "$marketplace_json" 2>/dev/null)
505
+ # Find vskill CLI check global first, then bundled with specweave
506
+ local vskill_cli=""
507
+ if command -v vskill >/dev/null 2>&1; then
508
+ vskill_cli=$(command -v vskill)
496
509
  else
497
- # Fallback: grep-based extraction
498
- source_rel=$(grep -oP "\"name\"\\s*:\\s*\"${plugin}\"[^}]*\"source\"\\s*:\\s*\"([^\"]+)\"" "$marketplace_json" | grep -oP '"source"\s*:\s*"\K[^"]+' || true)
510
+ # Look for vskill bundled alongside specweave in node_modules
511
+ local candidate="${specweave_dir}/../../../node_modules/.bin/vskill"
512
+ if [[ -f "$candidate" ]]; then
513
+ vskill_cli="$candidate"
514
+ fi
499
515
  fi
500
516
 
501
- if [[ -z "$source_rel" ]]; then
502
- VSKILL_INSTALL_OUTPUT="plugin '$plugin' not found in marketplace.json"
517
+ if [[ -z "$vskill_cli" ]]; then
518
+ VSKILL_INSTALL_OUTPUT="vskill not found install with: npm install -g vskill"
503
519
  return 1
504
520
  fi
505
521
 
506
- # Resolve full path (source is relative to plugin_dir, e.g. ./plugins/specweave)
507
- local source_dir="$plugin_dir/${source_rel#./}"
508
- if [[ ! -d "$source_dir" ]]; then
509
- VSKILL_INSTALL_OUTPUT="source dir not found: $source_dir"
522
+ # Verify specweave marketplace dir exists
523
+ if [[ ! -d "$specweave_dir" ]]; then
524
+ VSKILL_INSTALL_OUTPUT="specweave marketplace dir not found at $specweave_dir"
510
525
  return 1
511
526
  fi
512
527
 
513
- # Copy to target
514
- local target_dir="${HOME}/.claude/commands/${plugin}"
515
- mkdir -p "$target_dir"
516
- if cp -R "$source_dir/." "$target_dir/" 2>/dev/null; then
517
- # Fix hook permissions (.sh files need to be executable)
518
- find "$target_dir" -name "*.sh" -exec chmod 755 {} \; 2>/dev/null || true
519
-
520
- # Write lockfile entry so subsequent prompts skip re-install
521
- local lock_dir="${SW_PROJECT_ROOT:-$PWD}"
522
- local lockfile="$lock_dir/vskill.lock"
523
- local now
524
- now=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -Iseconds 2>/dev/null)
525
-
526
- if command -v jq >/dev/null 2>&1 && [[ -n "$lock_dir" ]]; then
527
- local skill_entry
528
- skill_entry=$(jq -n \
529
- --arg ver "0.0.0" \
530
- --arg now "$now" \
531
- '{version: $ver, sha: "", tier: "BUNDLED", installedAt: $now, source: "local:specweave"}')
532
-
533
- if [[ -f "$lockfile" ]]; then
534
- # Merge into existing lockfile
535
- jq --arg key "$plugin" --argjson entry "$skill_entry" --arg now "$now" \
536
- '.skills[$key] = $entry | .updatedAt = $now' \
537
- "$lockfile" > "${lockfile}.tmp" 2>/dev/null && \
538
- mv "${lockfile}.tmp" "$lockfile" 2>/dev/null || true
539
- else
540
- # Create new lockfile
541
- jq -n \
542
- --arg now "$now" \
543
- --arg key "$plugin" \
544
- --argjson entry "$skill_entry" \
545
- '{version: 1, agents: ["claude-code"], skills: {($key): $entry}, createdAt: $now, updatedAt: $now}' \
546
- > "$lockfile" 2>/dev/null || true
547
- fi
548
- fi
528
+ # Run vskill install at project scope (cwd = project root)
529
+ local output
530
+ output=$(cd "$project_dir" && "$node_bin" "$vskill_cli" install "$specweave_dir" \
531
+ --plugin "$plugin" --plugin-dir "$specweave_dir" --force 2>&1)
532
+ local exit_code=$?
549
533
 
550
- VSKILL_INSTALL_OUTPUT="installed $plugin via direct copy"
534
+ if [[ $exit_code -eq 0 ]]; then
535
+ VSKILL_INSTALL_OUTPUT="installed $plugin via vskill (project scope)"
551
536
  return 0
552
537
  else
553
- VSKILL_INSTALL_OUTPUT="copy failed for $plugin"
538
+ VSKILL_INSTALL_OUTPUT="vskill install failed for $plugin: $output"
554
539
  return 1
555
540
  fi
556
541
  }
@@ -1270,13 +1255,10 @@ if [[ "${SPECWEAVE_DISABLE_AUTO_LOAD:-0}" != "1" ]] && [[ "${SPECWEAVE_DISABLE_H
1270
1255
  for plugin in $DETECTED_PLUGINS; do
1271
1256
  [[ -z "$plugin" ]] && continue
1272
1257
 
1273
- # v1.0.159: Determine marketplace based on plugin name
1274
- # sw-* plugins → @specweave (via vskill), others @claude-plugins-official (via claude CLI)
1275
- # v1.0.240 (0198): context7/playwright removed from auto-install
1276
- # v1.0.278 (0241): sw-* plugins now installed via direct copy (no vskill dependency)
1258
+ # v1.0.279: Only @specweave plugins allowed no claude-plugins-official
1259
+ # Install via vskill (project scope: .claude/commands/<name>/)
1277
1260
  if [[ "$plugin" == sw-* ]] || [[ "$plugin" == "sw" ]]; then
1278
- # ---- SW-* PLUGINS: Install via direct copy (v1.0.278) ----
1279
- # Fast-path: check vskill.lock first (no CLI invocation needed)
1261
+ # Fast-path: check vskill.lock first
1280
1262
  if check_plugin_in_vskill_lock "$plugin"; then
1281
1263
  [[ -n "$PLUGINS_ALREADY" ]] && PLUGINS_ALREADY="$PLUGINS_ALREADY, "
1282
1264
  PLUGINS_ALREADY="${PLUGINS_ALREADY}${plugin}"
@@ -1285,61 +1267,18 @@ if [[ "${SPECWEAVE_DISABLE_AUTO_LOAD:-0}" != "1" ]] && [[ "${SPECWEAVE_DISABLE_H
1285
1267
  [[ -n "$PLUGINS_ALREADY" ]] && PLUGINS_ALREADY="$PLUGINS_ALREADY, "
1286
1268
  PLUGINS_ALREADY="${PLUGINS_ALREADY}${plugin}"
1287
1269
  else
1288
- # Not installed - install via direct copy
1289
- if install_plugin_direct "$plugin"; then
1270
+ # Install via vskill at project scope
1271
+ if install_plugin_via_vskill "$plugin"; then
1290
1272
  [[ -n "$PLUGINS_INSTALLED" ]] && PLUGINS_INSTALLED="$PLUGINS_INSTALLED, "
1291
1273
  PLUGINS_INSTALLED="${PLUGINS_INSTALLED}${plugin}"
1292
-
1293
- # Display scan result if available
1294
- if [[ -n "$VSKILL_INSTALL_OUTPUT" ]]; then
1295
- SCAN_RESULT=$(echo "$VSKILL_INSTALL_OUTPUT" | grep -oE "Score:[[:space:]]*[0-9]+/100[[:space:]]*Verdict:[[:space:]]*[A-Z]+" || true)
1296
- [[ -n "$SCAN_RESULT" ]] && echo "[$(date -Iseconds)] vskill | ${plugin} | ${SCAN_RESULT}" >> "$LAZY_LOAD_LOG"
1297
- fi
1274
+ echo "[$(date -Iseconds)] vskill | ${plugin} | ${VSKILL_INSTALL_OUTPUT:-ok}" >> "$LAZY_LOAD_LOG"
1298
1275
  else
1299
1276
  echo "[$(date -Iseconds)] vskill | ${plugin} | FAILED: ${VSKILL_INSTALL_OUTPUT:-unknown}" >> "$LAZY_LOAD_LOG"
1300
1277
  fi
1301
1278
  fi
1302
1279
  else
1303
- # ---- NON-SW PLUGINS: Install via claude CLI (unchanged) ----
1304
- MARKETPLACE="claude-plugins-official"
1305
- PLUGIN_SCOPE="$DEFAULT_PLUGIN_SCOPE"
1306
- FULL_PLUGIN_NAME="${plugin}@${MARKETPLACE}"
1307
- ALREADY_INSTALLED=false
1308
-
1309
- if check_plugin_installed_from_json "$plugin" "$MARKETPLACE"; then
1310
- ALREADY_INSTALLED=true
1311
- else
1312
- CURRENT_PLUGINS=""
1313
- if command -v timeout >/dev/null 2>&1; then
1314
- CURRENT_PLUGINS=$(timeout 10 claude plugin list 2>/dev/null | grep -E "^ ❯ " | sed 's/^ ❯ //' || true)
1315
- else
1316
- CURRENT_PLUGINS=$(claude plugin list 2>/dev/null | grep -E "^ ❯ " | sed 's/^ ❯ //' || true)
1317
- fi
1318
- if echo "$CURRENT_PLUGINS" | grep -q "^${FULL_PLUGIN_NAME}$"; then
1319
- ALREADY_INSTALLED=true
1320
- fi
1321
- fi
1322
-
1323
- if [[ "$ALREADY_INSTALLED" == "true" ]]; then
1324
- [[ -n "$PLUGINS_ALREADY" ]] && PLUGINS_ALREADY="$PLUGINS_ALREADY, "
1325
- PLUGINS_ALREADY="${PLUGINS_ALREADY}${plugin}"
1326
- else
1327
- if command -v timeout >/dev/null 2>&1; then
1328
- OUT=$(timeout 10 claude plugin install "${FULL_PLUGIN_NAME}" --scope "$PLUGIN_SCOPE" 2>&1) || true
1329
- else
1330
- OUT=$(claude plugin install "${FULL_PLUGIN_NAME}" --scope "$PLUGIN_SCOPE" 2>&1) || true
1331
- fi
1332
- if echo "$OUT" | grep -qiE "(success|installed)"; then
1333
- sleep 0.5
1334
- if check_plugin_installed_from_json "$plugin" "$MARKETPLACE"; then
1335
- [[ -n "$PLUGINS_INSTALLED" ]] && PLUGINS_INSTALLED="$PLUGINS_INSTALLED, "
1336
- PLUGINS_INSTALLED="${PLUGINS_INSTALLED}${plugin}"
1337
- else
1338
- [[ -n "$PLUGINS_ALREADY" ]] && PLUGINS_ALREADY="$PLUGINS_ALREADY, "
1339
- PLUGINS_ALREADY="${PLUGINS_ALREADY}${plugin}"
1340
- fi
1341
- fi
1342
- fi
1280
+ # Non-sw-* plugin detected skip (only @specweave plugins allowed)
1281
+ echo "[$(date -Iseconds)] plugins | SKIPPED non-specweave plugin: $plugin" >> "$LAZY_LOAD_LOG"
1343
1282
  fi
1344
1283
  done
1345
1284