specweave 1.0.299 → 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.
Files changed (49) hide show
  1. package/bin/specweave.js +25 -1
  2. package/dist/src/cli/commands/auto.js +1 -0
  3. package/dist/src/cli/commands/auto.js.map +1 -1
  4. package/dist/src/cli/commands/scan-plugins.d.ts +12 -0
  5. package/dist/src/cli/commands/scan-plugins.d.ts.map +1 -0
  6. package/dist/src/cli/commands/scan-plugins.js +80 -0
  7. package/dist/src/cli/commands/scan-plugins.js.map +1 -0
  8. package/dist/src/core/doctor/checkers/installation-health-checker.js +6 -6
  9. package/dist/src/core/doctor/checkers/installation-health-checker.js.map +1 -1
  10. package/dist/src/core/lazy-loading/llm-plugin-detector.d.ts +8 -27
  11. package/dist/src/core/lazy-loading/llm-plugin-detector.d.ts.map +1 -1
  12. package/dist/src/core/lazy-loading/llm-plugin-detector.js +12 -90
  13. package/dist/src/core/lazy-loading/llm-plugin-detector.js.map +1 -1
  14. package/dist/src/core/skill-security/index.d.ts +9 -0
  15. package/dist/src/core/skill-security/index.d.ts.map +1 -0
  16. package/dist/src/core/skill-security/index.js +5 -0
  17. package/dist/src/core/skill-security/index.js.map +1 -0
  18. package/dist/src/core/skill-security/parser.d.ts +27 -0
  19. package/dist/src/core/skill-security/parser.d.ts.map +1 -0
  20. package/dist/src/core/skill-security/parser.js +55 -0
  21. package/dist/src/core/skill-security/parser.js.map +1 -0
  22. package/dist/src/core/skill-security/reporter.d.ts +21 -0
  23. package/dist/src/core/skill-security/reporter.d.ts.map +1 -0
  24. package/dist/src/core/skill-security/reporter.js +121 -0
  25. package/dist/src/core/skill-security/reporter.js.map +1 -0
  26. package/dist/src/core/skill-security/rules.d.ts +25 -0
  27. package/dist/src/core/skill-security/rules.d.ts.map +1 -0
  28. package/dist/src/core/skill-security/rules.js +137 -0
  29. package/dist/src/core/skill-security/rules.js.map +1 -0
  30. package/dist/src/core/skill-security/scanner.d.ts +41 -0
  31. package/dist/src/core/skill-security/scanner.d.ts.map +1 -0
  32. package/dist/src/core/skill-security/scanner.js +78 -0
  33. package/dist/src/core/skill-security/scanner.js.map +1 -0
  34. package/package.json +1 -1
  35. package/plugins/specweave/hooks/lib/score-increment.sh +87 -0
  36. package/plugins/specweave/hooks/stop-auto-v5.sh +55 -9
  37. package/plugins/specweave/hooks/tests/test-auto-context-integration.sh +126 -0
  38. package/plugins/specweave/hooks/tests/test-stop-auto-enriched.sh +128 -0
  39. package/plugins/specweave/hooks/user-prompt-submit.sh +99 -150
  40. package/plugins/specweave/scripts/setup-auto.sh +58 -4
  41. package/plugins/specweave/scripts/tests/test-setup-auto-selection.sh +74 -0
  42. package/plugins/specweave/scripts/tests/test-setup-auto-usergoal.sh +83 -0
  43. package/plugins/specweave/skills/auto/SKILL.md +3 -1
  44. package/plugins/specweave/skills/do/SKILL.md +11 -0
  45. package/plugins/specweave/skills/increment/SKILL.md +8 -2
  46. package/plugins/specweave/skills/team-lead/SKILL.md +69 -5
  47. package/plugins/specweave-jira/skills/jira-mapper/SKILL.md +13 -14
  48. package/plugins/specweave-jira/skills/jira-resource-validator/SKILL.md +74 -4
  49. package/plugins/specweave-jira/skills/jira-sync/SKILL.md +18 -27
@@ -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
  }
@@ -1158,6 +1143,16 @@ if [[ "${SPECWEAVE_DISABLE_AUTO_LOAD:-0}" != "1" ]] && [[ "${SPECWEAVE_DISABLE_H
1158
1143
  # Quick skip: already using /sw: commands (user is in workflow)
1159
1144
  if ! echo "$PROMPT" | grep -qE "^[[:space:]]*/sw:"; then
1160
1145
 
1146
+ # BYPASS: Native Claude Code slash commands (e.g., /context, /help, /doctor)
1147
+ # Prevents 15s detect-intent timeout -> LLM_DETECTION_FAILED -> keyword fallback
1148
+ # that falsely matches "test" as substring inside "/context". Pattern matches
1149
+ # /word or /word-word prompts that do not mention specweave.
1150
+ if echo "$PROMPT" | grep -qE "^[[:space:]]*/[a-z][a-z0-9-]*([[:space:]]|$)" &&
1151
+ ! echo "$PROMPT" | grep -qiE "specweave"; then
1152
+ echo '{"decision":"approve"}'
1153
+ exit 0
1154
+ fi
1155
+
1161
1156
  # Check if specweave CLI is available
1162
1157
  if command -v specweave >/dev/null 2>&1; then
1163
1158
  # Setup logging (use project root, never create dirs at $HOME)
@@ -1260,13 +1255,10 @@ if [[ "${SPECWEAVE_DISABLE_AUTO_LOAD:-0}" != "1" ]] && [[ "${SPECWEAVE_DISABLE_H
1260
1255
  for plugin in $DETECTED_PLUGINS; do
1261
1256
  [[ -z "$plugin" ]] && continue
1262
1257
 
1263
- # v1.0.159: Determine marketplace based on plugin name
1264
- # sw-* plugins → @specweave (via vskill), others @claude-plugins-official (via claude CLI)
1265
- # v1.0.240 (0198): context7/playwright removed from auto-install
1266
- # 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>/)
1267
1260
  if [[ "$plugin" == sw-* ]] || [[ "$plugin" == "sw" ]]; then
1268
- # ---- SW-* PLUGINS: Install via direct copy (v1.0.278) ----
1269
- # Fast-path: check vskill.lock first (no CLI invocation needed)
1261
+ # Fast-path: check vskill.lock first
1270
1262
  if check_plugin_in_vskill_lock "$plugin"; then
1271
1263
  [[ -n "$PLUGINS_ALREADY" ]] && PLUGINS_ALREADY="$PLUGINS_ALREADY, "
1272
1264
  PLUGINS_ALREADY="${PLUGINS_ALREADY}${plugin}"
@@ -1275,61 +1267,18 @@ if [[ "${SPECWEAVE_DISABLE_AUTO_LOAD:-0}" != "1" ]] && [[ "${SPECWEAVE_DISABLE_H
1275
1267
  [[ -n "$PLUGINS_ALREADY" ]] && PLUGINS_ALREADY="$PLUGINS_ALREADY, "
1276
1268
  PLUGINS_ALREADY="${PLUGINS_ALREADY}${plugin}"
1277
1269
  else
1278
- # Not installed - install via direct copy
1279
- if install_plugin_direct "$plugin"; then
1270
+ # Install via vskill at project scope
1271
+ if install_plugin_via_vskill "$plugin"; then
1280
1272
  [[ -n "$PLUGINS_INSTALLED" ]] && PLUGINS_INSTALLED="$PLUGINS_INSTALLED, "
1281
1273
  PLUGINS_INSTALLED="${PLUGINS_INSTALLED}${plugin}"
1282
-
1283
- # Display scan result if available
1284
- if [[ -n "$VSKILL_INSTALL_OUTPUT" ]]; then
1285
- SCAN_RESULT=$(echo "$VSKILL_INSTALL_OUTPUT" | grep -oE "Score:[[:space:]]*[0-9]+/100[[:space:]]*Verdict:[[:space:]]*[A-Z]+" || true)
1286
- [[ -n "$SCAN_RESULT" ]] && echo "[$(date -Iseconds)] vskill | ${plugin} | ${SCAN_RESULT}" >> "$LAZY_LOAD_LOG"
1287
- fi
1274
+ echo "[$(date -Iseconds)] vskill | ${plugin} | ${VSKILL_INSTALL_OUTPUT:-ok}" >> "$LAZY_LOAD_LOG"
1288
1275
  else
1289
1276
  echo "[$(date -Iseconds)] vskill | ${plugin} | FAILED: ${VSKILL_INSTALL_OUTPUT:-unknown}" >> "$LAZY_LOAD_LOG"
1290
1277
  fi
1291
1278
  fi
1292
1279
  else
1293
- # ---- NON-SW PLUGINS: Install via claude CLI (unchanged) ----
1294
- MARKETPLACE="claude-plugins-official"
1295
- PLUGIN_SCOPE="$DEFAULT_PLUGIN_SCOPE"
1296
- FULL_PLUGIN_NAME="${plugin}@${MARKETPLACE}"
1297
- ALREADY_INSTALLED=false
1298
-
1299
- if check_plugin_installed_from_json "$plugin" "$MARKETPLACE"; then
1300
- ALREADY_INSTALLED=true
1301
- else
1302
- CURRENT_PLUGINS=""
1303
- if command -v timeout >/dev/null 2>&1; then
1304
- CURRENT_PLUGINS=$(timeout 10 claude plugin list 2>/dev/null | grep -E "^ ❯ " | sed 's/^ ❯ //' || true)
1305
- else
1306
- CURRENT_PLUGINS=$(claude plugin list 2>/dev/null | grep -E "^ ❯ " | sed 's/^ ❯ //' || true)
1307
- fi
1308
- if echo "$CURRENT_PLUGINS" | grep -q "^${FULL_PLUGIN_NAME}$"; then
1309
- ALREADY_INSTALLED=true
1310
- fi
1311
- fi
1312
-
1313
- if [[ "$ALREADY_INSTALLED" == "true" ]]; then
1314
- [[ -n "$PLUGINS_ALREADY" ]] && PLUGINS_ALREADY="$PLUGINS_ALREADY, "
1315
- PLUGINS_ALREADY="${PLUGINS_ALREADY}${plugin}"
1316
- else
1317
- if command -v timeout >/dev/null 2>&1; then
1318
- OUT=$(timeout 10 claude plugin install "${FULL_PLUGIN_NAME}" --scope "$PLUGIN_SCOPE" 2>&1) || true
1319
- else
1320
- OUT=$(claude plugin install "${FULL_PLUGIN_NAME}" --scope "$PLUGIN_SCOPE" 2>&1) || true
1321
- fi
1322
- if echo "$OUT" | grep -qiE "(success|installed)"; then
1323
- sleep 0.5
1324
- if check_plugin_installed_from_json "$plugin" "$MARKETPLACE"; then
1325
- [[ -n "$PLUGINS_INSTALLED" ]] && PLUGINS_INSTALLED="$PLUGINS_INSTALLED, "
1326
- PLUGINS_INSTALLED="${PLUGINS_INSTALLED}${plugin}"
1327
- else
1328
- [[ -n "$PLUGINS_ALREADY" ]] && PLUGINS_ALREADY="$PLUGINS_ALREADY, "
1329
- PLUGINS_ALREADY="${PLUGINS_ALREADY}${plugin}"
1330
- fi
1331
- fi
1332
- fi
1280
+ # Non-sw-* plugin detected skip (only @specweave plugins allowed)
1281
+ echo "[$(date -Iseconds)] plugins | SKIPPED non-specweave plugin: $plugin" >> "$LAZY_LOAD_LOG"
1333
1282
  fi
1334
1283
  done
1335
1284
 
@@ -249,21 +249,61 @@ if [ "$ALL_BACKLOG" = "true" ]; then
249
249
  fi
250
250
  fi
251
251
 
252
- # If no increments specified, find current in-progress increment
252
+ # If no increments specified, find current in-progress increment(s) via intent scoring
253
253
  if [ ${#INCREMENT_IDS[@]} -eq 0 ]; then
254
+ # Collect ALL active/in-progress increments (no blind first-match break)
255
+ _ACTIVE_DIRS=()
254
256
  for dir in "$INCREMENTS_DIR"/[0-9][0-9][0-9][0-9]-*/; do
255
257
  if [ -d "$dir" ]; then
256
258
  META_FILE="$dir/metadata.json"
257
259
  if [ -f "$META_FILE" ]; then
258
260
  STATUS=$(jq -r '.status' "$META_FILE" 2>/dev/null || echo "")
259
261
  if [ "$STATUS" = "active" ] || [ "$STATUS" = "in-progress" ]; then
260
- INCREMENT_ID=$(basename "$dir")
261
- INCREMENT_IDS+=("$INCREMENT_ID")
262
- break
262
+ _ACTIVE_DIRS+=("$dir")
263
263
  fi
264
264
  fi
265
265
  fi
266
266
  done
267
+
268
+ if [ ${#_ACTIVE_DIRS[@]} -eq 1 ]; then
269
+ # Fast path: single active increment, no scoring needed
270
+ INCREMENT_IDS+=("$(basename "${_ACTIVE_DIRS[0]}")")
271
+ elif [ ${#_ACTIVE_DIRS[@]} -gt 1 ]; then
272
+ select_best_increment() {
273
+ local prompt_text="$1"; shift; local dirs=("$@")
274
+ local _setup_dir; _setup_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
275
+ local _score_script="$_setup_dir/../hooks/lib/score-increment.sh"
276
+
277
+ if [ -n "$prompt_text" ] && [ -f "$_score_script" ]; then
278
+ # Score each increment against the prompt, pick best
279
+ local _best_score=-1 _best_dir=""
280
+ for _dir in "${dirs[@]}"; do
281
+ local _score; _score=$(bash "$_score_script" "$_dir" "$prompt_text" 2>/dev/null || echo "0")
282
+ if [ "$_score" -gt "$_best_score" ]; then
283
+ _best_score="$_score"; _best_dir="$_dir"
284
+ fi
285
+ done
286
+ local _sel; _sel=$(basename "$_best_dir")
287
+ echo "🎯 Selected '$_sel' by intent match (score: $_best_score/100)" >&2
288
+ echo "{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"event\":\"increment_selected\",\"increment\":\"$_sel\",\"method\":\"intent_scoring\",\"score\":$_best_score}" >> "$LOGS_DIR/auto-sessions.log"
289
+ echo "$_sel"
290
+ else
291
+ # No prompt: pick most-recently-modified increment
292
+ local _latest_mtime=0 _latest_dir=""
293
+ for _dir in "${dirs[@]}"; do
294
+ local _mtime; _mtime=$(stat -f%m "$_dir/metadata.json" 2>/dev/null || stat -c%Y "$_dir/metadata.json" 2>/dev/null || echo "0")
295
+ if [ "$_mtime" -gt "$_latest_mtime" ]; then
296
+ _latest_mtime="$_mtime"; _latest_dir="$_dir"
297
+ fi
298
+ done
299
+ local _sel; _sel=$(basename "${_latest_dir:-${dirs[0]}}")
300
+ echo "📅 Selected '$_sel' by most recent activity" >&2
301
+ echo "{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"event\":\"increment_selected\",\"increment\":\"$_sel\",\"method\":\"recent_activity\"}" >> "$LOGS_DIR/auto-sessions.log"
302
+ echo "$_sel"
303
+ fi
304
+ }
305
+ INCREMENT_IDS+=("$(select_best_increment "$PROMPT" "${_ACTIVE_DIRS[@]}")")
306
+ fi
267
307
  fi
268
308
 
269
309
  if [ ${#INCREMENT_IDS[@]} -eq 0 ]; then
@@ -472,6 +512,20 @@ echo "$SESSION_JSON" | jq . > "$SESSION_FILE"
472
512
  # Log session start
473
513
  echo "{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"event\":\"session_start\",\"sessionId\":\"$SESSION_ID\",\"increments\":${#INCREMENT_IDS[@]}}" >> "$LOGS_DIR/auto-sessions.log"
474
514
 
515
+ # Write userGoal to auto-mode.json BEFORE the session start banner
516
+ AUTO_MODE_FILE="$STATE_DIR/auto-mode.json"
517
+ if [ -f "$AUTO_MODE_FILE" ]; then
518
+ if [ -n "$PROMPT" ]; then
519
+ _UPDATED_AM=$(jq --arg g "$PROMPT" '.userGoal = $g' "$AUTO_MODE_FILE" 2>/dev/null)
520
+ else
521
+ _UPDATED_AM=$(jq '.userGoal = null' "$AUTO_MODE_FILE" 2>/dev/null)
522
+ fi
523
+ [ -n "$_UPDATED_AM" ] && echo "$_UPDATED_AM" > "$AUTO_MODE_FILE"
524
+ elif [ -n "$PROMPT" ]; then
525
+ # Create stub so stop hook can read userGoal even before LLM writes full auto-mode.json
526
+ jq -n --arg g "$PROMPT" '{"active":false,"userGoal":$g}' > "$AUTO_MODE_FILE"
527
+ fi
528
+
475
529
  # Output - Session Start Banner
476
530
  echo ""
477
531
  echo "╔══════════════════════════════════════════════════════════════════════════════╗"
@@ -0,0 +1,74 @@
1
+ #!/bin/bash
2
+ # Tests for scored increment selection in setup-auto.sh (AC-US1-01, AC-US1-03, AC-US1-04)
3
+ # Uses score-increment.sh directly to verify selection logic.
4
+
5
+ set -e
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ SCORE_SCRIPT="$SCRIPT_DIR/../../hooks/lib/score-increment.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_gt() {
23
+ if [ "$1" -gt "$2" ]; then
24
+ echo " ✓ $3 ($1 > $2)"; PASS=$((PASS+1))
25
+ else
26
+ echo " ✗ $3 (expected $1 > $2)"; FAIL=$((FAIL+1))
27
+ fi
28
+ }
29
+
30
+ make_increment() {
31
+ local dir="$TMPDIR_ROOT/$1"
32
+ mkdir -p "$dir"
33
+ echo "{\"title\":\"$2\",\"status\":\"active\",\"lastActivity\":\"$5\"}" > "$dir/metadata.json"
34
+ printf "# %s\n\n%s\n" "$2" "$3" > "$dir/spec.md"
35
+ printf "### T-001: %s\n**Status**: [ ] pending\n" "$4" > "$dir/tasks.md"
36
+ echo "$dir"
37
+ }
38
+
39
+ echo "scored increment selection tests"
40
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
41
+
42
+ # TC-007: Prompt-based selection picks best match
43
+ d1=$(make_increment "0100-user-auth" "Authentication Login Feature" "Implement user authentication and login" "Add login endpoint" "2026-02-18T10:00:00Z")
44
+ d2=$(make_increment "0101-deploy-pipeline" "CI/CD Deploy Pipeline" "Setup deployment pipeline with Docker" "Configure CI workflow" "2026-02-19T10:00:00Z")
45
+
46
+ s1=$(bash "$SCORE_SCRIPT" "$d1" "authentication login" 2>/dev/null)
47
+ s2=$(bash "$SCORE_SCRIPT" "$d2" "authentication login" 2>/dev/null)
48
+ assert_gt "$s1" "$s2" "TC-007: auth scores higher than deploy for 'authentication login'"
49
+
50
+ # TC-007b: Reverse -- deploy query should score deploy higher
51
+ s3=$(bash "$SCORE_SCRIPT" "$d1" "deploy pipeline docker" 2>/dev/null)
52
+ s4=$(bash "$SCORE_SCRIPT" "$d2" "deploy pipeline docker" 2>/dev/null)
53
+ assert_gt "$s4" "$s3" "TC-007b: deploy scores higher than auth for 'deploy pipeline docker'"
54
+
55
+ # TC-008: Single increment -- score still works (fast path skips scoring in setup-auto.sh)
56
+ d3=$(make_increment "0102-single" "Single Active Feature" "Only one active increment exists" "Implement feature" "2026-02-19T12:00:00Z")
57
+ result=$(bash "$SCORE_SCRIPT" "$d3" "single feature" 2>/dev/null)
58
+ echo "$result" | grep -qE '^[0-9]+$' && \
59
+ echo " ✓ TC-008: single increment scoring returns integer ($result)" && PASS=$((PASS+1)) || \
60
+ { echo " ✗ TC-008: non-integer output: '$result'"; FAIL=$((FAIL+1)); }
61
+
62
+ # TC-009: Empty query → 0 for all (triggers mtime fallback in setup-auto.sh)
63
+ s5=$(bash "$SCORE_SCRIPT" "$d1" "" 2>/dev/null)
64
+ s6=$(bash "$SCORE_SCRIPT" "$d2" "" 2>/dev/null)
65
+ assert_eq "$s5" "0" "TC-009: empty query → 0 for increment A"
66
+ assert_eq "$s6" "0" "TC-009b: empty query → 0 for increment B"
67
+
68
+ # TC-X: Scoring is deterministic (same inputs → same output)
69
+ s7=$(bash "$SCORE_SCRIPT" "$d1" "authentication login" 2>/dev/null)
70
+ assert_eq "$s7" "$s1" "TC-X: scoring is deterministic"
71
+
72
+ echo ""
73
+ echo "Results: $PASS passed, $FAIL failed"
74
+ [ "$FAIL" -eq 0 ] || exit 1
@@ -0,0 +1,83 @@
1
+ #!/bin/bash
2
+ # Tests for userGoal wiring in setup-auto.sh (AC-US2-01, AC-US2-02, AC-US2-04)
3
+ # Tests the logic directly without running the full setup-auto.sh.
4
+
5
+ set -e
6
+
7
+ TMPDIR_ROOT=$(mktemp -d)
8
+ trap 'rm -rf "$TMPDIR_ROOT"' EXIT
9
+
10
+ PASS=0; FAIL=0
11
+
12
+ assert_eq() {
13
+ if [ "$1" = "$2" ]; then
14
+ echo " ✓ $3"; PASS=$((PASS+1))
15
+ else
16
+ echo " ✗ $3 (expected '$2', got '$1')"; FAIL=$((FAIL+1))
17
+ fi
18
+ }
19
+
20
+ # Helper: run userGoal wiring logic (extracted from setup-auto.sh)
21
+ write_user_goal() {
22
+ local prompt="$1" file="$2"
23
+ if [ -f "$file" ]; then
24
+ if [ -n "$prompt" ]; then
25
+ local updated; updated=$(jq --arg g "$prompt" '.userGoal = $g' "$file" 2>/dev/null)
26
+ else
27
+ local updated; updated=$(jq '.userGoal = null' "$file" 2>/dev/null)
28
+ fi
29
+ [ -n "$updated" ] && echo "$updated" > "$file"
30
+ elif [ -n "$prompt" ]; then
31
+ jq -n --arg g "$prompt" '{"active":false,"userGoal":$g}' > "$file"
32
+ fi
33
+ }
34
+
35
+ echo "setup-auto.sh userGoal wiring tests"
36
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
37
+
38
+ # TC-005: Prompt sets userGoal on existing file
39
+ AM="$TMPDIR_ROOT/auto-mode.json"
40
+ echo '{"active":false}' > "$AM"
41
+ write_user_goal "fix auth bug" "$AM"
42
+ result=$(jq -r '.userGoal' "$AM")
43
+ assert_eq "$result" "fix auth bug" "TC-005: prompt sets userGoal"
44
+
45
+ # TC-006: No prompt sets userGoal to null on existing file
46
+ echo '{"active":false,"userGoal":"old goal"}' > "$AM"
47
+ write_user_goal "" "$AM"
48
+ result=$(jq -r '.userGoal' "$AM")
49
+ assert_eq "$result" "null" "TC-006: no prompt → userGoal null"
50
+
51
+ # TC-X: Prompt creates new file when absent
52
+ rm -f "$AM"
53
+ write_user_goal "add payment feature" "$AM"
54
+ result=$(jq -r '.userGoal' "$AM")
55
+ assert_eq "$result" "add payment feature" "TC-X1: creates file with userGoal"
56
+
57
+ # TC-X: No prompt does NOT create file when absent (userGoal null only on existing files)
58
+ rm -f "$AM"
59
+ write_user_goal "" "$AM"
60
+ if [ -f "$AM" ]; then
61
+ result=$(jq -r '.userGoal' "$AM")
62
+ assert_eq "$result" "null" "TC-X2: no-prompt no-file → file absent or null"
63
+ else
64
+ echo " ✓ TC-X2: no-prompt no-file → no file created"; PASS=$((PASS+1))
65
+ fi
66
+
67
+ # TC-X: Prompt preserves other fields in existing auto-mode.json
68
+ echo '{"active":true,"incrementIds":["0100-auth"],"tddMode":false}' > "$AM"
69
+ write_user_goal "fix login" "$AM"
70
+ active=$(jq -r '.active' "$AM")
71
+ goal=$(jq -r '.userGoal' "$AM")
72
+ assert_eq "$active" "true" "TC-X3: existing fields preserved after goal write"
73
+ assert_eq "$goal" "fix login" "TC-X3: userGoal set correctly"
74
+
75
+ # TC-X: Special characters in prompt are handled safely
76
+ echo '{"active":false}' > "$AM"
77
+ write_user_goal 'fix "tricky" bug & deploy' "$AM"
78
+ result=$(jq -r '.userGoal' "$AM")
79
+ assert_eq "$result" 'fix "tricky" bug & deploy' "TC-X4: special chars in prompt"
80
+
81
+ echo ""
82
+ echo "Results: $PASS passed, $FAIL failed"
83
+ [ "$FAIL" -eq 0 ] || exit 1
@@ -75,7 +75,7 @@ Use Read/Write/Edit/Glob tools directly (no CLI needed):
75
75
  "incrementIds": ["0001-feature"],
76
76
  "tddMode": false,
77
77
  "requireTests": false,
78
- "userGoal": "optional",
78
+ "userGoal": null,
79
79
  "successCriteria": [
80
80
  { "type": "tasks_complete", "description": "All tasks marked complete", "required": true },
81
81
  { "type": "acs_satisfied", "description": "All ACs satisfied", "required": true }
@@ -96,6 +96,8 @@ Map flags to extra `successCriteria` entries:
96
96
 
97
97
  Always include `tasks_complete` and `acs_satisfied` as base criteria. Ensure `.specweave/state/` dir exists.
98
98
 
99
+ **`userGoal` field**: Set to the user's stated intent from conversation context. If the user said "fix the auth bug", set `userGoal` to `"fix the auth bug"`. If no clear intent is expressed, set to `null`. This field is read by the stop hook to provide context-aware feedback and guide `/sw:do` to the correct increment.
100
+
99
101
  ### Step 1.5a: MANDATORY - Complexity Check for Team-Lead Routing
100
102
 
101
103
  **Before starting autonomous execution, check if this increment needs team-lead:**
@@ -49,6 +49,17 @@ When no ID provided, auto-select (NEVER ask user for ID):
49
49
  3. Select best candidate and auto-promote to in-progress if needed
50
50
  4. If no candidates, show status summary and offer: create new, close ready_for_review, resume backlog, or view status
51
51
 
52
+ ### Step 1.5: Auto-Mode Context Override
53
+
54
+ When running inside an active auto session (`.specweave/state/auto-mode.json` has `active: true`):
55
+
56
+ 1. **Explicit ID takes priority**: If an explicit increment ID was passed (e.g., `/sw:do 0252`), use it directly — skip this step
57
+ 2. **Stop hook guidance**: If the stop hook feedback in the current conversation mentions a specific increment ID (e.g., "Continue: /sw:do 0252"), use that ID
58
+ 3. **Read incrementIds**: If no ID from above, read `incrementIds` array from `auto-mode.json` and use the **first entry** — this is the increment prioritized by scoring at session start
59
+ 4. **Skip filesystem scanning**: When auto-mode context provides an increment ID via steps 2 or 3, skip Step 1's filesystem scanning entirely — auto-mode context takes priority
60
+
61
+ This ensures the execution loop stays focused on the contextually correct increment rather than re-scanning the filesystem each iteration.
62
+
52
63
  ### Step 2: Load Context
53
64
 
54
65
  1. **Find increment directory**: Normalize ID to 4-digit format, match `.specweave/increments/NNNN-*/`