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.
- package/bin/specweave.js +25 -1
- package/dist/src/cli/commands/auto.js +1 -0
- package/dist/src/cli/commands/auto.js.map +1 -1
- package/dist/src/cli/commands/scan-plugins.d.ts +12 -0
- package/dist/src/cli/commands/scan-plugins.d.ts.map +1 -0
- package/dist/src/cli/commands/scan-plugins.js +80 -0
- package/dist/src/cli/commands/scan-plugins.js.map +1 -0
- package/dist/src/core/doctor/checkers/installation-health-checker.js +6 -6
- package/dist/src/core/doctor/checkers/installation-health-checker.js.map +1 -1
- package/dist/src/core/lazy-loading/llm-plugin-detector.d.ts +8 -27
- package/dist/src/core/lazy-loading/llm-plugin-detector.d.ts.map +1 -1
- package/dist/src/core/lazy-loading/llm-plugin-detector.js +12 -90
- package/dist/src/core/lazy-loading/llm-plugin-detector.js.map +1 -1
- package/dist/src/core/skill-security/index.d.ts +9 -0
- package/dist/src/core/skill-security/index.d.ts.map +1 -0
- package/dist/src/core/skill-security/index.js +5 -0
- package/dist/src/core/skill-security/index.js.map +1 -0
- package/dist/src/core/skill-security/parser.d.ts +27 -0
- package/dist/src/core/skill-security/parser.d.ts.map +1 -0
- package/dist/src/core/skill-security/parser.js +55 -0
- package/dist/src/core/skill-security/parser.js.map +1 -0
- package/dist/src/core/skill-security/reporter.d.ts +21 -0
- package/dist/src/core/skill-security/reporter.d.ts.map +1 -0
- package/dist/src/core/skill-security/reporter.js +121 -0
- package/dist/src/core/skill-security/reporter.js.map +1 -0
- package/dist/src/core/skill-security/rules.d.ts +25 -0
- package/dist/src/core/skill-security/rules.d.ts.map +1 -0
- package/dist/src/core/skill-security/rules.js +137 -0
- package/dist/src/core/skill-security/rules.js.map +1 -0
- package/dist/src/core/skill-security/scanner.d.ts +41 -0
- package/dist/src/core/skill-security/scanner.d.ts.map +1 -0
- package/dist/src/core/skill-security/scanner.js +78 -0
- package/dist/src/core/skill-security/scanner.js.map +1 -0
- package/package.json +1 -1
- package/plugins/specweave/hooks/lib/score-increment.sh +87 -0
- package/plugins/specweave/hooks/stop-auto-v5.sh +55 -9
- package/plugins/specweave/hooks/tests/test-auto-context-integration.sh +126 -0
- package/plugins/specweave/hooks/tests/test-stop-auto-enriched.sh +128 -0
- package/plugins/specweave/hooks/user-prompt-submit.sh +99 -150
- package/plugins/specweave/scripts/setup-auto.sh +58 -4
- package/plugins/specweave/scripts/tests/test-setup-auto-selection.sh +74 -0
- package/plugins/specweave/scripts/tests/test-setup-auto-usergoal.sh +83 -0
- package/plugins/specweave/skills/auto/SKILL.md +3 -1
- package/plugins/specweave/skills/do/SKILL.md +11 -0
- package/plugins/specweave/skills/increment/SKILL.md +8 -2
- package/plugins/specweave/skills/team-lead/SKILL.md +69 -5
- package/plugins/specweave-jira/skills/jira-mapper/SKILL.md +13 -14
- package/plugins/specweave-jira/skills/jira-resource-validator/SKILL.md +74 -4
- 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.
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
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
|
-
|
|
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("-
|
|
280
|
+
(.key | test("@claude-plugins-official$"))
|
|
278
281
|
)
|
|
279
282
|
| .key
|
|
280
283
|
' "$USER_SETTINGS" 2>/dev/null)
|
|
281
284
|
|
|
282
|
-
if [[ -n "$
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
#
|
|
291
|
+
# Reinstall sw-* at project scope via vskill
|
|
290
292
|
_sw_name="${plugin_key%%@*}"
|
|
291
|
-
if
|
|
292
|
-
[[ -n "$
|
|
293
|
-
|
|
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 "$
|
|
306
|
-
echo "[$(date -Iseconds)] scope-guard |
|
|
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
|
-
#
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
476
|
-
#
|
|
477
|
-
#
|
|
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
|
-
|
|
492
|
+
install_plugin_via_vskill() {
|
|
482
493
|
local plugin="$1"
|
|
483
|
-
local
|
|
484
|
-
local
|
|
485
|
-
|
|
486
|
-
#
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
#
|
|
493
|
-
local
|
|
494
|
-
if command -v
|
|
495
|
-
|
|
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
|
-
#
|
|
498
|
-
|
|
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 "$
|
|
502
|
-
VSKILL_INSTALL_OUTPUT="
|
|
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
|
-
#
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
#
|
|
514
|
-
local
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
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="
|
|
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.
|
|
1264
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
1279
|
-
if
|
|
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
|
-
#
|
|
1294
|
-
|
|
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
|
-
|
|
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":
|
|
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-*/`
|