specweave 1.0.550 → 1.0.552

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 (166) hide show
  1. package/CLAUDE.md +1 -1
  2. package/bin/specweave.js +23 -1
  3. package/dist/src/cli/commands/hook.d.ts +15 -0
  4. package/dist/src/cli/commands/hook.d.ts.map +1 -0
  5. package/dist/src/cli/commands/hook.js +61 -0
  6. package/dist/src/cli/commands/hook.js.map +1 -0
  7. package/dist/src/cli/commands/init.d.ts.map +1 -1
  8. package/dist/src/cli/commands/init.js +5 -0
  9. package/dist/src/cli/commands/init.js.map +1 -1
  10. package/dist/src/cli/commands/refresh-plugins.d.ts.map +1 -1
  11. package/dist/src/cli/commands/refresh-plugins.js +11 -1
  12. package/dist/src/cli/commands/refresh-plugins.js.map +1 -1
  13. package/dist/src/cli/commands/sync-setup.d.ts.map +1 -1
  14. package/dist/src/cli/commands/sync-setup.js +7 -3
  15. package/dist/src/cli/commands/sync-setup.js.map +1 -1
  16. package/dist/src/cli/helpers/issue-tracker/project-mapping-wizard.d.ts +9 -0
  17. package/dist/src/cli/helpers/issue-tracker/project-mapping-wizard.d.ts.map +1 -1
  18. package/dist/src/cli/helpers/issue-tracker/project-mapping-wizard.js +9 -3
  19. package/dist/src/cli/helpers/issue-tracker/project-mapping-wizard.js.map +1 -1
  20. package/dist/src/config/types.d.ts +2 -2
  21. package/dist/src/core/config/types.d.ts +18 -2
  22. package/dist/src/core/config/types.d.ts.map +1 -1
  23. package/dist/src/core/config/types.js.map +1 -1
  24. package/dist/src/core/hooks/handlers/hook-router.d.ts +19 -0
  25. package/dist/src/core/hooks/handlers/hook-router.d.ts.map +1 -0
  26. package/dist/src/core/hooks/handlers/hook-router.js +75 -0
  27. package/dist/src/core/hooks/handlers/hook-router.js.map +1 -0
  28. package/dist/src/core/hooks/handlers/index.d.ts +10 -0
  29. package/dist/src/core/hooks/handlers/index.d.ts.map +1 -0
  30. package/dist/src/core/hooks/handlers/index.js +9 -0
  31. package/dist/src/core/hooks/handlers/index.js.map +1 -0
  32. package/dist/src/core/hooks/handlers/post-tool-use-analytics.d.ts +11 -0
  33. package/dist/src/core/hooks/handlers/post-tool-use-analytics.d.ts.map +1 -0
  34. package/dist/src/core/hooks/handlers/post-tool-use-analytics.js +73 -0
  35. package/dist/src/core/hooks/handlers/post-tool-use-analytics.js.map +1 -0
  36. package/dist/src/core/hooks/handlers/post-tool-use.d.ts +11 -0
  37. package/dist/src/core/hooks/handlers/post-tool-use.d.ts.map +1 -0
  38. package/dist/src/core/hooks/handlers/post-tool-use.js +76 -0
  39. package/dist/src/core/hooks/handlers/post-tool-use.js.map +1 -0
  40. package/dist/src/core/hooks/handlers/pre-compact.d.ts +11 -0
  41. package/dist/src/core/hooks/handlers/pre-compact.d.ts.map +1 -0
  42. package/dist/src/core/hooks/handlers/pre-compact.js +77 -0
  43. package/dist/src/core/hooks/handlers/pre-compact.js.map +1 -0
  44. package/dist/src/core/hooks/handlers/pre-tool-use.d.ts +11 -0
  45. package/dist/src/core/hooks/handlers/pre-tool-use.d.ts.map +1 -0
  46. package/dist/src/core/hooks/handlers/pre-tool-use.js +318 -0
  47. package/dist/src/core/hooks/handlers/pre-tool-use.js.map +1 -0
  48. package/dist/src/core/hooks/handlers/session-start.d.ts +9 -0
  49. package/dist/src/core/hooks/handlers/session-start.d.ts.map +1 -0
  50. package/dist/src/core/hooks/handlers/session-start.js +111 -0
  51. package/dist/src/core/hooks/handlers/session-start.js.map +1 -0
  52. package/dist/src/core/hooks/handlers/stop-auto.d.ts +16 -0
  53. package/dist/src/core/hooks/handlers/stop-auto.d.ts.map +1 -0
  54. package/dist/src/core/hooks/handlers/stop-auto.js +122 -0
  55. package/dist/src/core/hooks/handlers/stop-auto.js.map +1 -0
  56. package/dist/src/core/hooks/handlers/stop-reflect.d.ts +14 -0
  57. package/dist/src/core/hooks/handlers/stop-reflect.d.ts.map +1 -0
  58. package/dist/src/core/hooks/handlers/stop-reflect.js +43 -0
  59. package/dist/src/core/hooks/handlers/stop-reflect.js.map +1 -0
  60. package/dist/src/core/hooks/handlers/stop-sync.d.ts +15 -0
  61. package/dist/src/core/hooks/handlers/stop-sync.d.ts.map +1 -0
  62. package/dist/src/core/hooks/handlers/stop-sync.js +68 -0
  63. package/dist/src/core/hooks/handlers/stop-sync.js.map +1 -0
  64. package/dist/src/core/hooks/handlers/types.d.ts +63 -0
  65. package/dist/src/core/hooks/handlers/types.d.ts.map +1 -0
  66. package/dist/src/core/hooks/handlers/types.js +27 -0
  67. package/dist/src/core/hooks/handlers/types.js.map +1 -0
  68. package/dist/src/core/hooks/handlers/user-prompt-submit.d.ts +14 -0
  69. package/dist/src/core/hooks/handlers/user-prompt-submit.d.ts.map +1 -0
  70. package/dist/src/core/hooks/handlers/user-prompt-submit.js +173 -0
  71. package/dist/src/core/hooks/handlers/user-prompt-submit.js.map +1 -0
  72. package/dist/src/core/hooks/handlers/utils.d.ts +25 -0
  73. package/dist/src/core/hooks/handlers/utils.d.ts.map +1 -0
  74. package/dist/src/core/hooks/handlers/utils.js +64 -0
  75. package/dist/src/core/hooks/handlers/utils.js.map +1 -0
  76. package/dist/src/core/increment/completion-validator.d.ts.map +1 -1
  77. package/dist/src/core/increment/completion-validator.js +32 -0
  78. package/dist/src/core/increment/completion-validator.js.map +1 -1
  79. package/dist/src/init/research/types.d.ts +1 -1
  80. package/dist/src/sync/sync-target-resolver.js.map +1 -1
  81. package/dist/src/utils/lock-manager.d.ts.map +1 -1
  82. package/dist/src/utils/lock-manager.js +5 -0
  83. package/dist/src/utils/lock-manager.js.map +1 -1
  84. package/dist/src/utils/plugin-copier.d.ts +10 -0
  85. package/dist/src/utils/plugin-copier.d.ts.map +1 -1
  86. package/dist/src/utils/plugin-copier.js +63 -35
  87. package/dist/src/utils/plugin-copier.js.map +1 -1
  88. package/package.json +1 -1
  89. package/plugins/specweave/agents/sw-closer.md +3 -2
  90. package/plugins/specweave/hooks/hooks.json +10 -10
  91. package/plugins/specweave/skills/code-reviewer/SKILL.md +180 -16
  92. package/plugins/specweave/skills/code-reviewer/agents/reviewer-comments.md +83 -0
  93. package/plugins/specweave/skills/code-reviewer/agents/reviewer-silent-failures.md +19 -0
  94. package/plugins/specweave/skills/code-reviewer/agents/reviewer-spec-compliance.md +19 -0
  95. package/plugins/specweave/skills/code-reviewer/agents/reviewer-tests.md +101 -0
  96. package/plugins/specweave/skills/code-reviewer/agents/reviewer-types.md +20 -0
  97. package/plugins/specweave/skills/done/SKILL.md +56 -21
  98. package/plugins/specweave/skills/grill/SKILL.md +1 -1
  99. package/plugins/specweave/skills/team-lead/agents/reviewer-logic.md +19 -0
  100. package/plugins/specweave/skills/team-lead/agents/reviewer-performance.md +20 -0
  101. package/plugins/specweave/skills/team-lead/agents/reviewer-security.md +20 -0
  102. package/src/templates/CLAUDE.md.template +7 -4
  103. package/plugins/specweave/hooks/README.md +0 -493
  104. package/plugins/specweave/hooks/_archive/stop-auto-v4-legacy.sh +0 -1319
  105. package/plugins/specweave/hooks/lib/common-setup.sh +0 -144
  106. package/plugins/specweave/hooks/lib/hook-errors.sh +0 -414
  107. package/plugins/specweave/hooks/lib/migrate-increment-work.sh +0 -245
  108. package/plugins/specweave/hooks/lib/resolve-package.sh +0 -146
  109. package/plugins/specweave/hooks/lib/scheduler-startup.sh +0 -135
  110. package/plugins/specweave/hooks/lib/score-increment.sh +0 -87
  111. package/plugins/specweave/hooks/lib/sync-spec-content.sh +0 -193
  112. package/plugins/specweave/hooks/lib/update-active-increment.sh +0 -95
  113. package/plugins/specweave/hooks/lib/update-status-line.sh +0 -233
  114. package/plugins/specweave/hooks/lib/validate-spec-status.sh +0 -171
  115. package/plugins/specweave/hooks/llm-judge-validator.sh +0 -219
  116. package/plugins/specweave/hooks/log-decision.sh +0 -168
  117. package/plugins/specweave/hooks/pre-compact.sh +0 -64
  118. package/plugins/specweave/hooks/startup-health-check.sh +0 -64
  119. package/plugins/specweave/hooks/stop-auto-v5.sh +0 -276
  120. package/plugins/specweave/hooks/stop-reflect.sh +0 -336
  121. package/plugins/specweave/hooks/stop-sync.sh +0 -283
  122. package/plugins/specweave/hooks/tests/test-auto-context-integration.sh +0 -126
  123. package/plugins/specweave/hooks/tests/test-stop-auto-enriched.sh +0 -128
  124. package/plugins/specweave/hooks/universal/dispatcher.mjs +0 -336
  125. package/plugins/specweave/hooks/universal/fail-fast-wrapper.sh +0 -325
  126. package/plugins/specweave/hooks/universal/hook-wrapper.cmd +0 -26
  127. package/plugins/specweave/hooks/universal/hook-wrapper.sh +0 -69
  128. package/plugins/specweave/hooks/universal/run-hook.sh +0 -20
  129. package/plugins/specweave/hooks/universal/session-start.cmd +0 -16
  130. package/plugins/specweave/hooks/universal/session-start.ps1 +0 -16
  131. package/plugins/specweave/hooks/user-prompt-submit.sh +0 -2550
  132. package/plugins/specweave/hooks/v2/detectors/lifecycle-detector.sh +0 -87
  133. package/plugins/specweave/hooks/v2/detectors/us-completion-detector.sh +0 -186
  134. package/plugins/specweave/hooks/v2/dispatchers/post-tool-use-analytics.sh +0 -83
  135. package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +0 -447
  136. package/plugins/specweave/hooks/v2/dispatchers/pre-tool-use.sh +0 -104
  137. package/plugins/specweave/hooks/v2/dispatchers/session-start.sh +0 -270
  138. package/plugins/specweave/hooks/v2/guards/completion-guard.sh +0 -14
  139. package/plugins/specweave/hooks/v2/guards/increment-duplicate-guard.sh +0 -14
  140. package/plugins/specweave/hooks/v2/guards/increment-existence-guard.sh +0 -240
  141. package/plugins/specweave/hooks/v2/guards/interview-enforcement-guard.sh +0 -171
  142. package/plugins/specweave/hooks/v2/guards/metadata-json-guard.sh +0 -14
  143. package/plugins/specweave/hooks/v2/guards/skill-chain-enforcement-guard.sh +0 -222
  144. package/plugins/specweave/hooks/v2/guards/spec-template-enforcement-guard.sh +0 -21
  145. package/plugins/specweave/hooks/v2/guards/spec-validation-guard.sh +0 -14
  146. package/plugins/specweave/hooks/v2/guards/status-completion-guard.sh +0 -84
  147. package/plugins/specweave/hooks/v2/guards/task-ac-sync-guard.sh +0 -475
  148. package/plugins/specweave/hooks/v2/guards/tdd-enforcement-guard.sh +0 -268
  149. package/plugins/specweave/hooks/v2/handlers/ac-sync-dispatcher.sh +0 -332
  150. package/plugins/specweave/hooks/v2/handlers/ac-validation-handler.sh +0 -50
  151. package/plugins/specweave/hooks/v2/handlers/github-sync-handler.sh +0 -347
  152. package/plugins/specweave/hooks/v2/handlers/living-docs-handler.sh +0 -83
  153. package/plugins/specweave/hooks/v2/handlers/living-specs-handler.sh +0 -268
  154. package/plugins/specweave/hooks/v2/handlers/project-bridge-handler.sh +0 -104
  155. package/plugins/specweave/hooks/v2/handlers/status-line-handler.sh +0 -165
  156. package/plugins/specweave/hooks/v2/handlers/status-update.sh +0 -61
  157. package/plugins/specweave/hooks/v2/handlers/universal-auto-create-dispatcher.sh +0 -270
  158. package/plugins/specweave/hooks/v2/integrations/ado-post-living-docs-update.sh +0 -367
  159. package/plugins/specweave/hooks/v2/integrations/ado-post-task.sh +0 -179
  160. package/plugins/specweave/hooks/v2/integrations/github-auto-create-handler.sh +0 -553
  161. package/plugins/specweave/hooks/v2/integrations/github-post-task.sh +0 -345
  162. package/plugins/specweave/hooks/v2/integrations/jira-post-task.sh +0 -180
  163. package/plugins/specweave/hooks/v2/lib/check-provider-enabled.sh +0 -52
  164. package/plugins/specweave/hooks/v2/queue/enqueue.sh +0 -81
  165. package/plugins/specweave/hooks/v2/session-end.sh +0 -139
  166. package/plugins/specweave/hooks/validate-skill-activations.sh +0 -227
@@ -1,2550 +0,0 @@
1
- #!/bin/bash
2
-
3
- # SpecWeave UserPromptSubmit Hook (v1.0.533 - Native Plugin Installation)
4
- # Fires BEFORE user's command executes (prompt-based hook)
5
- # Purpose: Auto-load plugins, discipline validation, context injection, instant command execution
6
- #
7
- # FEATURES:
8
- # - v1.0.201: LSP CLI FALLBACK INSTRUCTIONS - When LSP requested, instruct Claude to use
9
- # `specweave lsp` commands instead of Grep. These use TsServerClient for REAL semantic
10
- # analysis. Key fix: "find references" now gets semantic refs, not text matches!
11
- # - v1.0.198: UNIFIED LLM LSP DETECTION - LLM decides if LSP is needed (replaces grep patterns)
12
- # * Single detect-intent call now returns: plugins + increment + skills + LSP recommendation
13
- # * LLM-based detection understands context ("find references to X" vs general coding)
14
- # * Returns: lsp.needed, lsp.operation (references/definition/hover/symbols), lsp.language
15
- # * Grep-based detection kept as fallback if LLM detection not available
16
- # * Key insight: LLM can distinguish "find references" request from "build feature" request
17
- # - v1.0.192: LSP AUTO-INSTALL ON PROJECT DETECTION - Critical fix for LSP plugin installation
18
- # * Previously: LSP plugins only installed when user explicitly asked for "find references"
19
- # * Now: LSP plugins auto-installed when working on TS/Py/Rust projects
20
- # * Project detection: tsconfig.json, package.json, requirements.txt, Cargo.toml, etc.
21
- # * Prompt detection: mentions of typescript, react, python, django, rust, etc.
22
- # * Supported: vtsls (TS), pyright (Python), rust-analyzer (Rust)
23
- # * Key insight: Users shouldn't need to know about LSP to benefit from it
24
- # - v1.0.177: SKILL CHAINING REMINDER - Add explicit guidance in SKILL FIRST message
25
- # * "SKILL FIRST" does NOT mean "only one skill"
26
- # * Shows domain skills to use after sw:increment
27
- # * Points to CLAUDE.md "MANDATORY: Skill Chaining" section
28
- # - v1.0.175: CRITICAL FIX - Use installed_plugins.json as SOURCE OF TRUTH
29
- # * Reads ~/.claude/plugins/installed_plugins.json directly (eliminates false restart warnings)
30
- # * `claude plugin list` can have timing/buffering issues → unreliable for detection
31
- # * Primary: check_plugin_installed_from_json() using jq (fast, accurate)
32
- # * Fallback: `claude plugin list` only if jq unavailable
33
- # * Post-install verification: re-checks registry after install to confirm success
34
- # * Increased timeouts: 5s → 10s for CLI operations (reduces timing issues)
35
- # * Guard against false positives: if install says "success" but not in registry → treat as already installed
36
- # - v1.0.169: DIRECT SKILL INVOCATION - Call sw:increment skill directly
37
- # * Originally skipped wrapper indirection; increment-planner merged into increment in v1.0.261
38
- # * Passes FULL user prompt as args (not just extracted name)
39
- # * Uses <system><rules> tags (Claude-trained) instead of custom <mandatory_instruction>
40
- # * More concise, imperative instruction text
41
- # - v1.0.167: FIX PLUGIN RESTART WARNING - Use `claude plugin list` BEFORE install (DEPRECATED - had timing issues)
42
- # * Claude CLI always outputs "Successfully installed" even when already installed
43
- # * Called `claude plugin list` once to get current plugins, then checked against that
44
- # * Skipped install for already-installed plugins (faster + no false restart warnings)
45
- # * ISSUE: `claude plugin list` output unreliable → false positives → fixed in v1.0.175
46
- # - v1.0.147: SYNC PLUGIN INSTALL - Plugins available for CURRENT prompt!
47
- # * Replaced 20s async LLM detection with ~200ms sync `claude plugin install`
48
- # * Claude Code hot-reload picks up plugins immediately
49
- # * Keywords: react, vue, kubernetes, docker, terraform, github, jira, etc.
50
- # * Controlled by SPECWEAVE_DISABLE_AUTO_LOAD env var
51
- # - v1.0.127: (DEPRECATED) Background async plugin detection - too slow, plugins only for next prompt
52
- # - v1.0.166: CRITICAL FIX - Use hookSpecificOutput.additionalContext (NOT systemMessage!)
53
- # * Claude Code ignores systemMessage field in UserPromptSubmit hooks
54
- # * Use {"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"..."}}
55
- # * See: https://docs.claude.com/en/docs/claude-code/hooks
56
- # - v1.0.106: Use approve decision for info commands (not block)
57
- # * "block" erases command from context and stops execution
58
- # * Info commands (/sw:progress, /sw:status, /sw:jobs, etc.) now use "approve"
59
- # * Validation errors (task limit) still use "block" correctly
60
- # - v1.0.105: Fix ARGS extraction for prompts with IDE metadata prefix (<ide_opened_file>)
61
- # - v0.34.0: Unified "block" decision for both CLI and VSCode (WRONG - fixed in v1.0.106)
62
- # - v0.33.1: Unified instant execution - scripts run in hook for both CLI and VSCode
63
- # - v0.33.0: Script delegation pattern (now deprecated in favor of block decision)
64
- # - v0.26.13: jq for JSON parsing (10x faster than node -e)
65
- # - Single active increment detection (cached, not 4x!)
66
- # - Deferred heavy checks (SpecSyncManager only when needed)
67
- # - Ultra-fast early exits
68
- #
69
- # Performance: Status commands <100ms (was 3+ min), other prompts <10ms
70
- #
71
- # ARCHITECTURE:
72
- # - Both CLI and VSCode: Uses "block" decision with "reason" field to display output and stop execution
73
- # - Scripts execute in hook - NO LLM involvement for instant commands
74
- # - Use hookSpecificOutput.additionalContext for UserPromptSubmit (systemMessage is ignored!)
75
- # - See output_approve_with_context() helper function
76
-
77
- set +e
78
-
79
- # ==============================================================================
80
- # ULTRA-FAST EARLY EXIT (before ANY processing)
81
- # ==============================================================================
82
- INPUT=$(cat 2>/dev/null || echo '{}')
83
-
84
- # Use jq if available (10x faster than node), fallback to portable sed
85
- if command -v jq >/dev/null 2>&1; then
86
- PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""' 2>/dev/null || echo "")
87
- else
88
- # Fallback: extract prompt with sed (portable - works on macOS BSD and Linux GNU)
89
- # Note: grep -oP (PCRE) is NOT available on macOS default BSD grep
90
- PROMPT=$(echo "$INPUT" | sed -n 's/.*"prompt"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' 2>/dev/null || echo "")
91
- fi
92
-
93
- # ==============================================================================
94
- # EARLY EXIT FOR BUILT-IN SLASH COMMANDS (v1.0.280, fixed v1.0.305)
95
- # ==============================================================================
96
- # Claude Code has built-in slash commands (/context, /help, /clear, /compact,
97
- # /memory, /permissions, /cost, /doctor, /login, /logout, /config, etc.)
98
- # SpecWeave MUST NOT intercept, delay, or inject context into these commands.
99
- # Only /sw: and /sw-*: prefixed commands belong to SpecWeave.
100
- #
101
- # Without this guard, built-in commands like /context go through the LLM
102
- # detect-intent pipeline (5-15s delay) and may get incorrect additionalContext
103
- # injected, causing them to fail or behave unexpectedly.
104
- #
105
- # v1.0.305: Strip IDE metadata tags before checking. In VSCode, the prompt may
106
- # have <ide_opened_file>...</ide_opened_file> or <ide_selection>...</ide_selection>
107
- # prefixed on the same line as the command, causing the ^-anchored regex to fail.
108
- # Uses sed to strip everything up to the last closing </ide_*> tag (handles content
109
- # with < chars like code selections), then trims leading whitespace.
110
- CLEAN_PROMPT=$(echo "$PROMPT" | sed 's/.*<\/ide_[a-z_]*>//; s/^[[:space:]]*//')
111
- if echo "$CLEAN_PROMPT" | grep -qE "^[[:space:]]*/[a-zA-Z][a-zA-Z0-9_-]*($|[[:space:]])" && \
112
- ! echo "$CLEAN_PROMPT" | grep -qiE "^[[:space:]]*/sw[-:]"; then
113
- echo '{"decision":"approve"}'
114
- exit 0
115
- fi
116
-
117
- # ==============================================================================
118
- # PROJECT-SCOPE INITIALIZATION GUARD (v1.0.235)
119
- # ==============================================================================
120
- # Prevents SpecWeave skills from running in non-initialized projects.
121
- # Provides user-friendly prompt to initialize or disable plugins.
122
- #
123
- # Control:
124
- # - SPECWEAVE_DISABLE_GUARD=1 environment variable
125
- # - guard.enabled: false in .specweave/config.json
126
- #
127
- # Why: Skills are globally visible due to Claude Code's plugin system,
128
- # but they require project-specific initialization to work correctly.
129
-
130
- # Helper function: Find .specweave/config.json by walking up directory tree
131
- find_specweave_config() {
132
- local dir="$PWD"
133
- while [[ "$dir" != "/" ]]; do
134
- if [[ -f "$dir/.specweave/config.json" ]]; then
135
- # Validate it's valid JSON
136
- if command -v jq >/dev/null 2>&1; then
137
- if jq empty "$dir/.specweave/config.json" 2>/dev/null; then
138
- echo "$dir/.specweave/config.json"
139
- return 0
140
- fi
141
- else
142
- # No jq available - assume valid if file exists
143
- echo "$dir/.specweave/config.json"
144
- return 0
145
- fi
146
- fi
147
- dir=$(dirname "$dir")
148
- done
149
- return 1
150
- }
151
-
152
- # Check if this is a SpecWeave skill invocation
153
- # Matches: /sw:*, /sw-github:*, /backend:*, /testing:*, etc.
154
- DOMAIN_PLUGIN_PATTERN="^[[:space:]]*/([Bb]ackend|[Tt]esting|[Mm]obile|[Ii]nfra|[Kk]8s|[Mm]l|[Pp]ayments|[Kk]afka|[Cc]onfluent|[Cc]ost|[Dd]ocs|[Ss]ecurity|[Ss]kills|[Bb]lockchain):[a-zA-Z-]+"
155
- if [[ "$PROMPT" =~ ^[[:space:]]*/[Ss][Ww](-[a-zA-Z0-9-]+)?:[a-zA-Z-]+ ]] || [[ "$PROMPT" =~ $DOMAIN_PLUGIN_PATTERN ]]; then
156
- # Check if guard is disabled via environment variable
157
- if [[ "${SPECWEAVE_DISABLE_GUARD:-0}" != "1" ]]; then
158
- # Check if project is initialized (walk up tree to find .specweave/config.json)
159
- FOUND_CONFIG=$(find_specweave_config)
160
- if [[ -z "$FOUND_CONFIG" ]]; then
161
- # Check if guard is disabled in config (would fail since no config exists yet)
162
- # Extract skill name for error message
163
- SKILL_NAME=$(echo "$PROMPT" | grep -oiE '^[[:space:]]*/[a-z0-9-]+:[a-zA-Z-]+' | tr '[:upper:]' '[:lower:]' | sed 's/^[[:space:]]*//')
164
-
165
- # Generate helpful error message
166
- cat <<EOF
167
- {
168
- "decision": "block",
169
- "reason": "⚠️ **SpecWeave Not Initialized**
170
-
171
- You invoked \`${SKILL_NAME}\`, but this project hasn't been initialized with SpecWeave yet.
172
-
173
- **Options:**
174
-
175
- 1. **Initialize SpecWeave in this project:**
176
- \`\`\`bash
177
- specweave init
178
- # or
179
- npx specweave init
180
- \`\`\`
181
-
182
- 2. **Use SpecWeave in a different directory:**
183
- Navigate to a SpecWeave project first:
184
- \`\`\`bash
185
- cd /path/to/your/specweave-project
186
- \`\`\`
187
-
188
- 3. **Disable SpecWeave plugins globally:**
189
- If you don't need SpecWeave in most projects, disable it in:
190
- \`~/.claude/settings.json\`
191
- \`\`\`json
192
- {
193
- \"enabledPlugins\": {
194
- \"sw@specweave\": false,
195
- \"backend@vskill\": false
196
- }
197
- }
198
- \`\`\`
199
-
200
- 4. **Disable this guard (not recommended):**
201
- \`\`\`bash
202
- export SPECWEAVE_DISABLE_GUARD=1
203
- \`\`\`
204
-
205
- **Note:** SpecWeave skills are globally installed but require project-specific initialization to work correctly."
206
- }
207
- EOF
208
- exit 0
209
- fi
210
-
211
- # Check if guard is disabled in config (using found config path)
212
- if command -v jq >/dev/null 2>&1; then
213
- GUARD_ENABLED=$(jq -r '.guard.enabled // true' "$FOUND_CONFIG" 2>/dev/null)
214
- if [[ "$GUARD_ENABLED" == "false" ]]; then
215
- # Guard disabled in config - allow through
216
- :
217
- fi
218
- fi
219
- fi
220
- fi
221
-
222
- # ==============================================================================
223
- # PROJECT ROOT DETECTION (walk up to find .specweave/config.json)
224
- # ==============================================================================
225
- # MUST run BEFORE any code that uses SW_PROJECT_ROOT (scope guard, logging, etc.)
226
- # Checks for config.json to distinguish real projects from stale folders.
227
- SW_PROJECT_ROOT=""
228
- _swdir="$PWD"
229
- while [[ "$_swdir" != "/" ]]; do
230
- if [[ -f "$_swdir/.specweave/config.json" ]]; then
231
- SW_PROJECT_ROOT="$_swdir"
232
- break
233
- fi
234
- _swdir=$(dirname "$_swdir")
235
- done
236
-
237
- # ==============================================================================
238
- # USER-LEVEL PLUGIN SCOPE GUARD (v1.0.249)
239
- # ==============================================================================
240
- # Prevents SpecWeave domain plugins (sw-*) and LSP plugins (*-lsp) from
241
- # polluting ~/.claude/settings.json (user scope). These should ALWAYS be
242
- # project-scoped (.claude/settings.json) to avoid leaking across projects.
243
- #
244
- # Sources of user-level pollution:
245
- # - Claude Code's own plugin discovery (installs at user scope by default)
246
- # - Older SpecWeave versions (pre-v1.0.210) that didn't enforce project scope
247
- # - Manual `claude plugin install` without --scope flag
248
- #
249
- # What this guard does:
250
- # 1. Reads installed plugins via `claude plugin list` (fast, <200ms)
251
- # 2. Identifies sw-*@specweave or *-lsp@* at user scope
252
- # 3. Uninstalls from user scope and reinstalls at project scope
253
- # 4. Uses a daily marker file to avoid running on every prompt
254
- #
255
- # Rate limit: runs at most once per day per project (marker in .specweave/state/)
256
- # GUARD: Only run if inside a valid SpecWeave project (prevents stale folder creation)
257
- SCOPE_GUARD_RUN=false
258
- SCOPE_GUARD_MARKER=""
259
-
260
- if [[ -n "$SW_PROJECT_ROOT" ]]; then
261
- SCOPE_GUARD_MARKER="$SW_PROJECT_ROOT/.specweave/state/scope-guard.marker"
262
-
263
- if [[ -f "$SCOPE_GUARD_MARKER" ]]; then
264
- # Check if marker is from today (skip if already ran today)
265
- MARKER_DATE=$(cat "$SCOPE_GUARD_MARKER" 2>/dev/null)
266
- TODAY=$(date +%Y-%m-%d)
267
- [[ "$MARKER_DATE" != "$TODAY" ]] && SCOPE_GUARD_RUN=true
268
- else
269
- SCOPE_GUARD_RUN=true
270
- fi
271
- fi
272
-
273
- if [[ "$SCOPE_GUARD_RUN" == "true" ]] && command -v jq >/dev/null 2>&1 && command -v claude >/dev/null 2>&1; then
274
- USER_SETTINGS="$HOME/.claude/settings.json"
275
-
276
- if [[ -f "$USER_SETTINGS" ]]; then
277
- # Find SpecWeave domain plugins and LSP plugins at user level
278
- # Exempt: sw@specweave (core plugin, intentionally user-scoped)
279
- POLLUTED_PLUGINS=$(jq -r '
280
- .enabledPlugins // {} | to_entries[]
281
- | select(
282
- (.key | test("^sw-.*@specweave$")) or
283
- (.key | test("-lsp@"))
284
- )
285
- | .key
286
- ' "$USER_SETTINGS" 2>/dev/null)
287
-
288
- if [[ -n "$POLLUTED_PLUGINS" ]]; then
289
- MIGRATED=""
290
- for plugin_key in $POLLUTED_PLUGINS; do
291
- # Uninstall from user scope
292
- if timeout 5 claude plugin uninstall "$plugin_key" >/dev/null 2>&1; then
293
- if [[ "$plugin_key" == sw-*@specweave ]]; then
294
- # Reinstall at project scope via native Claude plugin system (v1.0.533)
295
- if timeout 10 claude plugin install "$plugin_key" --scope project >/dev/null 2>&1; then
296
- [[ -n "$MIGRATED" ]] && MIGRATED="$MIGRATED, "
297
- MIGRATED="${MIGRATED}${plugin_key}"
298
- fi
299
- else
300
- # LSP and other plugins: reinstall via claude CLI at project scope
301
- if timeout 10 claude plugin install "$plugin_key" --scope project >/dev/null 2>&1; then
302
- [[ -n "$MIGRATED" ]] && MIGRATED="$MIGRATED, "
303
- MIGRATED="${MIGRATED}${plugin_key}"
304
- fi
305
- fi
306
- fi
307
- done
308
-
309
- if [[ -n "$MIGRATED" ]]; then
310
- echo "[$(date -Iseconds)] scope-guard | migrated user→project: $MIGRATED" >> "$SW_PROJECT_ROOT/.specweave/state/hook.log" 2>/dev/null || true
311
- fi
312
-
313
- # CRITICAL FIX: Restore sw@specweave enabled state after uninstall operations
314
- # The `claude plugin uninstall` commands above may corrupt ~/.claude/settings.json
315
- # and disable sw@specweave as collateral damage. Re-enable it explicitly.
316
- if [[ -f "$USER_SETTINGS" ]]; then
317
- SW_ENABLED=$(jq -r '.enabledPlugins."sw@specweave" // "not_set"' "$USER_SETTINGS" 2>/dev/null)
318
- if [[ "$SW_ENABLED" != "true" ]]; then
319
- # Re-enable core plugin (preserves all other settings)
320
- jq '.enabledPlugins."sw@specweave" = true' "$USER_SETTINGS" > "${USER_SETTINGS}.tmp" 2>/dev/null && \
321
- mv "${USER_SETTINGS}.tmp" "$USER_SETTINGS" 2>/dev/null || true
322
- echo "[$(date -Iseconds)] scope-guard | restored sw@specweave enabled state" >> "$SW_PROJECT_ROOT/.specweave/state/hook.log" 2>/dev/null || true
323
- fi
324
- fi
325
- fi
326
- fi
327
-
328
- # v1.0.583: Clean up stale plugins from non-specweave marketplaces (e.g., frontend@vskill).
329
- # For each non-exempt marketplace in enabledPlugins, check if it has a valid manifest.
330
- # If not, remove all its plugins from settings. Runs daily alongside scope guard.
331
- if [[ -f "$USER_SETTINGS" ]]; then
332
- STALE_CLEANED=""
333
- # Get all unique marketplace names from enabledPlugins (exclude specweave and well-known externals)
334
- STALE_MARKETPLACES=$(jq -r '
335
- .enabledPlugins // {} | keys[]
336
- | split("@")[1] // empty
337
- | select(. != "specweave" and . != "claude-plugins-official" and . != "claude-code-lsps")
338
- ' "$USER_SETTINGS" 2>/dev/null | sort -u)
339
-
340
- for mkt in $STALE_MARKETPLACES; do
341
- MKT_MANIFEST="$HOME/.claude/plugins/marketplaces/$mkt/.claude-plugin/marketplace.json"
342
- if [[ ! -f "$MKT_MANIFEST" ]]; then
343
- # No manifest — marketplace is dead. Remove ALL its plugins from settings.
344
- STALE_KEYS=$(jq -r --arg mkt "$mkt" '
345
- .enabledPlugins // {} | keys[] | select(endswith("@" + $mkt))
346
- ' "$USER_SETTINGS" 2>/dev/null)
347
- for stale_key in $STALE_KEYS; do
348
- jq --arg k "$stale_key" 'del(.enabledPlugins[$k])' "$USER_SETTINGS" > "${USER_SETTINGS}.tmp" 2>/dev/null && \
349
- mv "${USER_SETTINGS}.tmp" "$USER_SETTINGS" 2>/dev/null || true
350
- [[ -n "$STALE_CLEANED" ]] && STALE_CLEANED="$STALE_CLEANED, "
351
- STALE_CLEANED="${STALE_CLEANED}${stale_key}"
352
- done
353
- else
354
- # Manifest exists — remove plugins not in the manifest
355
- VALID_NAMES=$(jq -r '.plugins[].name' "$MKT_MANIFEST" 2>/dev/null)
356
- ENABLED_KEYS=$(jq -r --arg mkt "$mkt" '
357
- .enabledPlugins // {} | keys[] | select(endswith("@" + $mkt))
358
- ' "$USER_SETTINGS" 2>/dev/null)
359
- for key in $ENABLED_KEYS; do
360
- plugin_name="${key%%@*}"
361
- if ! echo "$VALID_NAMES" | grep -qx "$plugin_name"; then
362
- jq --arg k "$key" 'del(.enabledPlugins[$k])' "$USER_SETTINGS" > "${USER_SETTINGS}.tmp" 2>/dev/null && \
363
- mv "${USER_SETTINGS}.tmp" "$USER_SETTINGS" 2>/dev/null || true
364
- [[ -n "$STALE_CLEANED" ]] && STALE_CLEANED="$STALE_CLEANED, "
365
- STALE_CLEANED="${STALE_CLEANED}${key}"
366
- fi
367
- done
368
- fi
369
- done
370
-
371
- if [[ -n "$STALE_CLEANED" ]]; then
372
- echo "[$(date -Iseconds)] scope-guard | cleaned stale plugins: $STALE_CLEANED" >> "$SW_PROJECT_ROOT/.specweave/state/hook.log" 2>/dev/null || true
373
- fi
374
- fi
375
-
376
- # Write today's marker
377
- mkdir -p "$(dirname "$SCOPE_GUARD_MARKER")" 2>/dev/null
378
- date +%Y-%m-%d > "$SCOPE_GUARD_MARKER" 2>/dev/null || true
379
- fi
380
-
381
- # ==============================================================================
382
- # AUTO-LOAD PLUGIN DETECTION + INCREMENT ASSIST (v1.0.141)
383
- # ==============================================================================
384
- # Detect plugin needs AND increment creation suggestions using LLM (Claude Haiku)
385
- # This runs BEFORE the SpecWeave keyword check to catch plugin-specific prompts
386
- #
387
- # Plugin Auto-Load Control:
388
- # - SPECWEAVE_DISABLE_AUTO_LOAD=1 environment variable
389
- # - pluginAutoLoad.enabled: false in .specweave/config.json
390
- #
391
- # Increment Assist Control:
392
- # - incrementAssist.enabled: false in .specweave/config.json (disables suggestions)
393
- # - incrementAssist.confidenceThreshold: 0.7 (minimum confidence to show suggestion)
394
- #
395
- # When both disabled: NO detection, NO LLM calls, fastest response time (~5-7s saved)
396
-
397
- # PROJECT ROOT DETECTION was moved earlier (before scope guard) to prevent stale folder creation
398
-
399
- # Check config for pluginAutoLoad.enabled, suggestOnly and incrementAssist.enabled settings
400
- PLUGIN_AUTOLOAD_ENABLED=true
401
- PLUGIN_SUGGEST_ONLY=true
402
- INCREMENT_ASSIST_ENABLED=true
403
- INCREMENT_CONFIDENCE_THRESHOLD=0.7
404
- INCREMENT_MANDATORY_CONFIG=true
405
- DEEP_INTERVIEW_ENABLED=false
406
- if [[ -n "$SW_PROJECT_ROOT" ]]; then
407
- CONFIG_PATH="$SW_PROJECT_ROOT/.specweave/config.json"
408
- else
409
- CONFIG_PATH=".specweave/config.json"
410
- fi
411
- if [[ -f "$CONFIG_PATH" ]]; then
412
- if command -v jq >/dev/null 2>&1; then
413
- AUTOLOAD_VALUE=$(jq -r '.pluginAutoLoad.enabled // true' "$CONFIG_PATH" 2>/dev/null)
414
- [[ "$AUTOLOAD_VALUE" == "false" ]] && PLUGIN_AUTOLOAD_ENABLED=false
415
-
416
- # Check suggestOnly mode (v1.0.158, default flipped to true in v1.0.397 — consent-first)
417
- SUGGEST_VALUE=$(jq -r '.pluginAutoLoad.suggestOnly // true' "$CONFIG_PATH" 2>/dev/null)
418
- [[ "$SUGGEST_VALUE" == "false" ]] && PLUGIN_SUGGEST_ONLY=false
419
-
420
- INCREMENT_VALUE=$(jq -r '.incrementAssist.enabled // true' "$CONFIG_PATH" 2>/dev/null)
421
- [[ "$INCREMENT_VALUE" == "false" ]] && INCREMENT_ASSIST_ENABLED=false
422
-
423
- THRESHOLD_VALUE=$(jq -r '.incrementAssist.confidenceThreshold // 0.7' "$CONFIG_PATH" 2>/dev/null)
424
- [[ "$THRESHOLD_VALUE" =~ ^[0-9.]+$ ]] && INCREMENT_CONFIDENCE_THRESHOLD="$THRESHOLD_VALUE"
425
-
426
- # Read incrementAssist.mandatory from config (config-based override for blocking)
427
- MANDATORY_CONFIG_VALUE=$(jq -r '.incrementAssist.mandatory // true' "$CONFIG_PATH" 2>/dev/null)
428
- [[ "$MANDATORY_CONFIG_VALUE" == "false" ]] && INCREMENT_MANDATORY_CONFIG=false
429
-
430
- # Deep Interview Mode detection (v1.0.195)
431
- DEEP_INTERVIEW_VALUE=$(jq -r '.planning.deepInterview.enabled // false' "$CONFIG_PATH" 2>/dev/null)
432
- [[ "$DEEP_INTERVIEW_VALUE" == "true" ]] && DEEP_INTERVIEW_ENABLED=true
433
- else
434
- # Fallback: grep for explicit false settings
435
- if grep -q '"pluginAutoLoad"' "$CONFIG_PATH" 2>/dev/null && grep -q '"enabled"[[:space:]]*:[[:space:]]*false' "$CONFIG_PATH" 2>/dev/null; then
436
- PLUGIN_AUTOLOAD_ENABLED=false
437
- fi
438
- # Fallback: grep for explicit suggestOnly=false (opt-in to auto-install)
439
- if grep -q '"pluginAutoLoad"' "$CONFIG_PATH" 2>/dev/null && grep -A5 '"pluginAutoLoad"' "$CONFIG_PATH" 2>/dev/null | grep -q '"suggestOnly"[[:space:]]*:[[:space:]]*false'; then
440
- PLUGIN_SUGGEST_ONLY=false
441
- fi
442
- if grep -q '"incrementAssist"' "$CONFIG_PATH" 2>/dev/null && grep -A5 '"incrementAssist"' "$CONFIG_PATH" 2>/dev/null | grep -q '"enabled"[[:space:]]*:[[:space:]]*false'; then
443
- INCREMENT_ASSIST_ENABLED=false
444
- fi
445
- # Fallback: grep for deep interview mode
446
- if grep -q '"deepInterview"' "$CONFIG_PATH" 2>/dev/null && grep -A5 '"deepInterview"' "$CONFIG_PATH" 2>/dev/null | grep -q '"enabled"[[:space:]]*:[[:space:]]*true'; then
447
- DEEP_INTERVIEW_ENABLED=true
448
- fi
449
- fi
450
- fi
451
-
452
- # ==============================================================================
453
- # HELPER FUNCTIONS (must be defined before use)
454
- # ==============================================================================
455
-
456
- # Helper: Escape output for JSON (handles newlines, quotes, backslashes)
457
- escape_json_early() {
458
- local input="$1"
459
- if command -v jq >/dev/null 2>&1; then
460
- printf '%s' "$input" | jq -Rs '.' | sed 's/^"//; s/"$//'
461
- else
462
- # Fallback: basic escaping without jq
463
- printf '%s' "$input" | sed 's/\\/\\\\/g; s/"/\\"/g' | awk '{printf "%s\\n", $0}' | sed 's/\\n$//'
464
- fi
465
- }
466
-
467
- # v1.0.254: Prompt safety limits to prevent "Prompt is too long" errors
468
- # Must match MAX_ADDITIONAL_CONTEXT_LENGTH in src/core/lazy-loading/llm-plugin-detector.ts
469
- MAX_ADDITIONAL_CONTEXT_LENGTH=3000
470
-
471
- # Helper: Output approve response with context (Claude Code hook format v1.0.166)
472
- # CRITICAL: systemMessage is NOT a valid field for UserPromptSubmit hooks!
473
- # Use hookSpecificOutput.additionalContext instead.
474
- # See: https://docs.claude.com/en/docs/claude-code/hooks
475
- #
476
- # v1.0.254: Added size guard — truncates additionalContext if it exceeds
477
- # MAX_ADDITIONAL_CONTEXT_LENGTH to prevent "Prompt is too long" errors.
478
- output_approve_with_context() {
479
- local context="$1"
480
- # v1.0.254: Safety truncation to prevent prompt overflow
481
- # v1.0.260: Added overflow logging for debugging context budget issues
482
- if [[ ${#context} -gt $MAX_ADDITIONAL_CONTEXT_LENGTH ]]; then
483
- local overflow_by=$(( ${#context} - MAX_ADDITIONAL_CONTEXT_LENGTH ))
484
- echo "[$(date -Iseconds)] CONTEXT OVERFLOW | size=${#context} | max=$MAX_ADDITIONAL_CONTEXT_LENGTH | overflow_by=$overflow_by | truncating" >> "${LAZY_LOAD_LOG:-/dev/null}" 2>/dev/null
485
- context="${context:0:$MAX_ADDITIONAL_CONTEXT_LENGTH}... [context truncated for safety]"
486
- fi
487
- local escaped
488
- escaped=$(escape_json_early "$context")
489
- printf '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"%s"}}\n' "$escaped"
490
- }
491
-
492
- # v1.0.260: truncate_and_escape_prompt() removed — prompt embedding in SKILL FIRST
493
- # was eliminated to save ~800 chars of context budget per turn. The skill reads
494
- # the user's prompt from conversation context (it's already there).
495
-
496
- # Helper: Check if plugin is in vskill.lock (fast-path skip) (v1.0.272)
497
- # vskill.lock is the SOURCE OF TRUTH for vskill-installed plugins.
498
- # Args: $1=plugin name (e.g., "backend")
499
- # Returns: 0 if in lockfile, 1 if not
500
- check_plugin_in_vskill_lock() {
501
- local plugin="$1"
502
- local lockfile="vskill.lock"
503
-
504
- # Check project-local lockfile first
505
- [[ ! -f "$lockfile" ]] && return 1
506
-
507
- # Must have jq for reliable JSON parsing
508
- if ! command -v jq >/dev/null 2>&1; then
509
- # Fallback: grep for plugin name in lockfile
510
- grep -q "\"${plugin}\"" "$lockfile" 2>/dev/null && return 0
511
- return 1
512
- fi
513
-
514
- # Check if plugin exists in lockfile skills
515
- local has_skill
516
- has_skill=$(jq -r --arg key "$plugin" '.skills[$key] // null' "$lockfile" 2>/dev/null)
517
-
518
- if [[ "$has_skill" != "null" ]] && [[ -n "$has_skill" ]]; then
519
- return 0 # Already installed via vskill
520
- else
521
- return 1 # Not in lockfile
522
- fi
523
- }
524
-
525
- # ==============================================================================
526
- # check_plugin_installed_from_json REMOVED (v1.0.535)
527
- # Plugins are now pre-installed at init time. No runtime installation checks needed.
528
- # ==============================================================================
529
-
530
- # ==============================================================================
531
- # KEYWORD-BASED PLUGIN DETECTION REMOVED (v1.0.159)
532
- # ==============================================================================
533
- # Keyword fallback was removed because it was too aggressive.
534
- # Example: "run tests" would install testing even for simple test runs.
535
- #
536
- # Plugin detection now happens ONLY via LLM analysis (specweave detect-intent).
537
- # The LLM understands user INTENT - it only recommends plugins when user
538
- # explicitly asks to BUILD/IMPLEMENT something, not for questions or discussions.
539
-
540
- # ==============================================================================
541
- # UNIFIED LLM DETECTION (v1.0.147) - ONE call for BOTH plugins AND increments
542
- # ==============================================================================
543
- # Single `specweave detect-intent` call returns:
544
- # - plugins: which plugins to install (for CURRENT prompt via hot-reload)
545
- # - increment: whether to suggest creating an increment
546
- #
547
- # DEFAULT: SUGGEST INCREMENT for ~95% of implementation work.
548
- # LLM decides action: "new" (most cases), "small_fix" (trivial edits),
549
- # "hotfix" (urgent), "reopen" (related to previous), "none" (skip).
550
- #
551
- # WHEN NOT TO CREATE INCREMENT (action: "none" ONLY):
552
- # ┌─────────────────────────────────────────────────────────────────────────────┐
553
- # │ • Pure questions: "what is X?", "explain Y", "tell me about Z" │
554
- # │ • Exploration: "show me", "list", "search for" │
555
- # │ • Commands: "run tests", "build", "deploy", "commit" │
556
- # │ • Already in workflow: prompt starts with /sw: │
557
- # │ • Chat/greetings: "hello", "thanks", general conversation │
558
- # │ • EXPLICIT OPT-OUT: "don't create an increment", "skip workflow" │
559
- # │ │
560
- # │ NOTE: These are NOT questions — they are WORK requiring increments: │
561
- # │ "investigate", "debug", "troubleshoot", "why does X fail", │
562
- # │ "optimize", "improve", "secure", "audit", "solve", "resolve", │
563
- # │ "analyze", "root cause", "X is broken", "X keeps failing" │
564
- # └─────────────────────────────────────────────────────────────────────────────┘
565
- #
566
- # STILL SUGGEST INCREMENT (action: "small_fix"):
567
- # │ • Typo fixes, version bumps, single config value changes │
568
- # │ • These get a non-mandatory suggestion so user can opt in │
569
-
570
- # Initialize message variable
571
- AUTOLOAD_PLUGINS_MSG=""
572
-
573
- # ==============================================================================
574
- # EXTERNAL FOLDER DETECTION (v1.0.166) - Quick recommendation
575
- # ==============================================================================
576
- # If user wants to create project in EXTERNAL folder, recommend new session
577
- EXTERNAL_FOLDER_DETECTED=""
578
- if echo "$PROMPT" | grep -qiE "(in|to|at|create)[[:space:]]+(external[[:space:]]+folder|separate[[:space:]]+folder|new[[:space:]]+folder|~/|/Users/|/home/|/tmp/|outside[[:space:]]+(this|current|the)[[:space:]]+project)"; then
579
- # Extract the target path if mentioned
580
- TARGET_PATH=$(echo "$PROMPT" | grep -oE "~/[a-zA-Z0-9_/-]+" | head -1)
581
- [[ -z "$TARGET_PATH" ]] && TARGET_PATH=$(echo "$PROMPT" | grep -oE "/Users/[a-zA-Z0-9_/-]+" | head -1)
582
- [[ -z "$TARGET_PATH" ]] && TARGET_PATH=$(echo "$PROMPT" | grep -oE "/home/[a-zA-Z0-9_/-]+" | head -1)
583
-
584
- EXTERNAL_FOLDER_DETECTED="📁 **EXTERNAL PROJECT DETECTED**
585
-
586
- You're requesting work in a folder outside this project.
587
- ${TARGET_PATH:+Target: $TARGET_PATH}
588
-
589
- 💡 **Recommended**: Start a NEW Claude Code session in that folder:
590
- 1. Open terminal: \`cd ${TARGET_PATH:-<target-folder>}\`
591
- 2. Start Claude Code: \`claude\`
592
- 3. Run \`specweave init\` to set up SpecWeave there
593
-
594
- This ensures plugins and context are properly scoped to the new project.
595
-
596
- ---
597
-
598
- "
599
- fi
600
-
601
- # ==============================================================================
602
- # LSP LANGUAGE SERVER CHECK (v1.0.179) - One-time warning for missing servers
603
- # ==============================================================================
604
- # Reads results from background lsp-check.sh (spawned by session-start.sh)
605
- # Shows warning ONCE per session if language servers are missing
606
- LSP_WARNING_MSG=""
607
- if [[ -n "$SW_PROJECT_ROOT" ]]; then
608
- LSP_STATE_FILE="$SW_PROJECT_ROOT/.specweave/state/lsp-check.json"
609
- else
610
- LSP_STATE_FILE=""
611
- fi
612
- if [[ -f "$LSP_STATE_FILE" ]] && command -v jq >/dev/null 2>&1; then
613
- LSP_STATUS=$(jq -r '.status // "ok"' "$LSP_STATE_FILE" 2>/dev/null)
614
- LSP_WARNED=$(jq -r '.warned // false' "$LSP_STATE_FILE" 2>/dev/null)
615
-
616
- if [[ "$LSP_STATUS" == "missing" ]] && [[ "$LSP_WARNED" != "true" ]]; then
617
- # Build warning message from missing servers
618
- MISSING_SERVERS=$(jq -r '.missing[] | "- **\(.language)**: `\(.install)`"' "$LSP_STATE_FILE" 2>/dev/null)
619
-
620
- if [[ -n "$MISSING_SERVERS" ]]; then
621
- LSP_WARNING_MSG="LSP missing: ${MISSING_SERVERS}. Install for semantic code intelligence. Guide: https://verified-skill.com/docs/guides/lsp-integration
622
-
623
- "
624
- # Mark as warned so we don't show again this session
625
- # Use a temp file to avoid jq in-place issues
626
- TMP_FILE=$(mktemp)
627
- jq '.warned = true' "$LSP_STATE_FILE" > "$TMP_FILE" 2>/dev/null && mv "$TMP_FILE" "$LSP_STATE_FILE" 2>/dev/null
628
- fi
629
- fi
630
- fi
631
-
632
- # ==============================================================================
633
- # LSP PROJECT CONFIG (v1.0.192) - Project-level LSP configuration
634
- # ==============================================================================
635
- # Reads LSP settings from .specweave/config.json instead of requiring env vars
636
- # Config schema:
637
- # lsp.enabled: true/false - Enable LSP features for this project
638
- # lsp.autoInstallPlugins: true/false - Auto-install marketplace and plugins
639
- # lsp.marketplace: "boostvolt/claude-code-lsps" - Which marketplace to use
640
- # lsp.plugins.typescript: "vtsls" - TypeScript LSP plugin
641
- # lsp.plugins.python: "pyright" - Python LSP plugin
642
-
643
- LSP_CONFIG_ENABLED="false"
644
- LSP_AUTO_INSTALL="false"
645
- LSP_MARKETPLACE="boostvolt/claude-code-lsps"
646
- LSP_MARKETPLACE_URL="https://github.com/boostvolt/claude-code-lsps"
647
- LSP_ENV_WARNED="false"
648
-
649
- if [[ -f "$CONFIG_PATH" ]] && command -v jq >/dev/null 2>&1; then
650
- LSP_CONFIG_ENABLED=$(jq -r '.lsp.enabled // false' "$CONFIG_PATH" 2>/dev/null)
651
- LSP_AUTO_INSTALL=$(jq -r '.lsp.autoInstallPlugins // false' "$CONFIG_PATH" 2>/dev/null)
652
- LSP_MARKETPLACE=$(jq -r '.lsp.marketplace // "boostvolt/claude-code-lsps"' "$CONFIG_PATH" 2>/dev/null)
653
- fi
654
-
655
- # Check for LSP request keywords (find references, go to definition, etc.)
656
- # v1.0.198: This is now a FALLBACK - LLM detection (lsp.needed field) takes precedence
657
- # The LLM-based detection in detect-intent is more accurate and understands context
658
- LSP_REQUEST_DETECTED="false"
659
- if echo "$PROMPT" | grep -qiE "(find|get|show)[[:space:]]+(all[[:space:]]+)?references|go[[:space:]]?to[[:space:]]?definition|goto[[:space:]]?definition|LSP|findReferences|goToDefinition|hover|documentSymbol|workspaceSymbol"; then
660
- LSP_REQUEST_DETECTED="true"
661
- fi
662
-
663
- # ==============================================================================
664
- # LSP PROJECT LANGUAGE DETECTION (v1.0.192) - Auto-detect project languages
665
- # ==============================================================================
666
- # Detects project languages from file system to auto-install LSP plugins
667
- # LAZY LOADING: Only installs when project/prompt actually needs that language
668
- # Available plugins in boostvolt/claude-code-lsps marketplace:
669
- # - vtsls: TypeScript/JavaScript (most common)
670
- # - pyright: Python
671
- # - rust-analyzer: Rust
672
- # Key insight: User working on TS project should get LSP without asking for it
673
- LSP_PROJECT_NEEDS_TS="false"
674
- LSP_PROJECT_NEEDS_PY="false"
675
- LSP_PROJECT_NEEDS_RUST="false"
676
- LSP_PROJECT_NEEDS_CSHARP="false"
677
- LSP_PROJECT_NEEDS_GO="false"
678
- LSP_PROJECT_NEEDS_JAVA="false"
679
- LSP_PROMPT_NEEDS_TS="false"
680
- LSP_PROMPT_NEEDS_PY="false"
681
- LSP_PROMPT_NEEDS_RUST="false"
682
- LSP_PROMPT_NEEDS_CSHARP="false"
683
- LSP_PROMPT_NEEDS_GO="false"
684
- LSP_PROMPT_NEEDS_JAVA="false"
685
-
686
- # Detect TypeScript/JavaScript project from file system
687
- # Check for: tsconfig.json, package.json with typescript, *.ts/*.tsx files
688
- if [[ -f "tsconfig.json" ]] || [[ -f "tsconfig.base.json" ]] || [[ -f "jsconfig.json" ]]; then
689
- LSP_PROJECT_NEEDS_TS="true"
690
- elif [[ -f "package.json" ]]; then
691
- # Check if package.json mentions typescript
692
- if grep -qE '"typescript"|"@types/|"tsx"|"ts-node"' package.json 2>/dev/null; then
693
- LSP_PROJECT_NEEDS_TS="true"
694
- fi
695
- fi
696
- # Also check for .ts/.tsx files in src/ or root (fast check, max 1 level deep)
697
- if [[ "$LSP_PROJECT_NEEDS_TS" != "true" ]]; then
698
- if ls *.ts *.tsx src/*.ts src/*.tsx 2>/dev/null | head -1 | grep -q .; then
699
- LSP_PROJECT_NEEDS_TS="true"
700
- fi
701
- fi
702
-
703
- # Detect Python project from file system
704
- # Check for: requirements.txt, pyproject.toml, setup.py, *.py files
705
- if [[ -f "requirements.txt" ]] || [[ -f "pyproject.toml" ]] || [[ -f "setup.py" ]] || [[ -f "Pipfile" ]]; then
706
- LSP_PROJECT_NEEDS_PY="true"
707
- fi
708
- if [[ "$LSP_PROJECT_NEEDS_PY" != "true" ]]; then
709
- if ls *.py src/*.py 2>/dev/null | head -1 | grep -q .; then
710
- LSP_PROJECT_NEEDS_PY="true"
711
- fi
712
- fi
713
-
714
- # Detect Rust project from file system
715
- # Check for: Cargo.toml, Cargo.lock, *.rs files
716
- if [[ -f "Cargo.toml" ]] || [[ -f "Cargo.lock" ]]; then
717
- LSP_PROJECT_NEEDS_RUST="true"
718
- fi
719
- if [[ "$LSP_PROJECT_NEEDS_RUST" != "true" ]]; then
720
- if ls *.rs src/*.rs 2>/dev/null | head -1 | grep -q .; then
721
- LSP_PROJECT_NEEDS_RUST="true"
722
- fi
723
- fi
724
-
725
- # v1.0.235: Detect C#/.NET project from file system
726
- # Check for: *.csproj, *.sln, Directory.Build.props, *.cs files
727
- if ls *.csproj *.sln 2>/dev/null | head -1 | grep -q . || [[ -f "Directory.Build.props" ]]; then
728
- LSP_PROJECT_NEEDS_CSHARP="true"
729
- fi
730
- if [[ "$LSP_PROJECT_NEEDS_CSHARP" != "true" ]]; then
731
- # Check one level of subdirectories (common C# project structure: src/MyApp/MyApp.csproj)
732
- if ls */*.csproj */*.sln src/*/*.csproj 2>/dev/null | head -1 | grep -q .; then
733
- LSP_PROJECT_NEEDS_CSHARP="true"
734
- elif ls *.cs src/*.cs 2>/dev/null | head -1 | grep -q .; then
735
- LSP_PROJECT_NEEDS_CSHARP="true"
736
- fi
737
- fi
738
-
739
- # v1.0.235: Detect Go project from file system
740
- if [[ -f "go.mod" ]] || [[ -f "go.sum" ]]; then
741
- LSP_PROJECT_NEEDS_GO="true"
742
- fi
743
- if [[ "$LSP_PROJECT_NEEDS_GO" != "true" ]]; then
744
- if ls *.go cmd/*.go pkg/*.go 2>/dev/null | head -1 | grep -q .; then
745
- LSP_PROJECT_NEEDS_GO="true"
746
- fi
747
- fi
748
-
749
- # v1.0.235: Detect Java project from file system
750
- if [[ -f "pom.xml" ]] || [[ -f "build.gradle" ]] || [[ -f "build.gradle.kts" ]] || [[ -f "settings.gradle" ]]; then
751
- LSP_PROJECT_NEEDS_JAVA="true"
752
- fi
753
- if [[ "$LSP_PROJECT_NEEDS_JAVA" != "true" ]]; then
754
- if ls *.java src/*.java src/main/java/*.java 2>/dev/null | head -1 | grep -q .; then
755
- LSP_PROJECT_NEEDS_JAVA="true"
756
- fi
757
- fi
758
-
759
- # Detect from prompt keywords (TypeScript/React/Vue/Angular/Node)
760
- if echo "$PROMPT" | grep -qiE "\.tsx?|typescript|react|vue|angular|next\.?js|node\.?js|express|nestjs"; then
761
- LSP_PROMPT_NEEDS_TS="true"
762
- fi
763
-
764
- # Detect from prompt keywords (Python/Django/Flask/FastAPI)
765
- if echo "$PROMPT" | grep -qiE "\.py|python|django|flask|fastapi|pytorch|tensorflow|pandas|numpy"; then
766
- LSP_PROMPT_NEEDS_PY="true"
767
- fi
768
-
769
- # Detect from prompt keywords (Rust/Cargo)
770
- if echo "$PROMPT" | grep -qiE "\.rs|rust|cargo|rustc|tokio|actix|axum"; then
771
- LSP_PROMPT_NEEDS_RUST="true"
772
- fi
773
-
774
- # v1.0.235: Detect from prompt keywords (C#/.NET)
775
- if echo "$PROMPT" | grep -qiE "\.cs\b|c#|csharp|dotnet|\.net|asp\.net|blazor|entity.?framework|nuget|\.csproj|\.sln"; then
776
- LSP_PROMPT_NEEDS_CSHARP="true"
777
- fi
778
-
779
- # v1.0.235: Detect from prompt keywords (Go)
780
- if echo "$PROMPT" | grep -qiE "\.go\b|golang|go\.mod|goroutine|gin|echo|fiber"; then
781
- LSP_PROMPT_NEEDS_GO="true"
782
- fi
783
-
784
- # v1.0.235: Detect from prompt keywords (Java/Spring/Kotlin)
785
- if echo "$PROMPT" | grep -qiE "\.java\b|java\b|spring|maven|gradle|kotlin|\.kt\b|jvm"; then
786
- LSP_PROMPT_NEEDS_JAVA="true"
787
- fi
788
-
789
- # LSP setup is handled by `specweave init` and `specweave lsp status`
790
- # No per-prompt warnings needed - avoids state file pollution
791
- LSP_ENV_SETUP_MSG=""
792
-
793
- # ==============================================================================
794
- # LSP AUTO-INSTALL (v1.0.196) - Install LSP plugins with PROJECT SCOPE
795
- # ==============================================================================
796
- # Triggers on ANY of:
797
- # - Explicit LSP request (findReferences, goToDefinition, etc.)
798
- # - Project language detection (tsconfig.json, package.json, requirements.txt, Cargo.toml)
799
- # - Prompt language detection (mentions typescript, react, python, django, etc.)
800
- # This ensures LSP plugins are installed when working on TS/Py/Rust projects
801
- # WITHOUT requiring user to explicitly ask for "find references"
802
- #
803
- # v1.0.196: LSP plugins now install with PROJECT SCOPE by default
804
- # - Reads plugins.scope.lspScope from config (default: "project")
805
- # - Project scope keeps LSP plugins specific to the project
806
- # - Avoids polluting global plugin list with project-specific language support
807
- LSP_INSTALL_MSG=""
808
- LSP_NEEDS_INSTALL="false"
809
-
810
- # Read LSP scope from config.json (v1.0.196)
811
- # Default: "project" - LSP plugins should be project-scoped
812
- LSP_PLUGIN_SCOPE="project"
813
- if [[ -f "$CONFIG_PATH" ]] && command -v jq >/dev/null 2>&1; then
814
- SCOPE_VALUE=$(jq -r '.plugins.scope.lspScope // "project"' "$CONFIG_PATH" 2>/dev/null)
815
- if [[ "$SCOPE_VALUE" == "user" ]] || [[ "$SCOPE_VALUE" == "project" ]] || [[ "$SCOPE_VALUE" == "local" ]]; then
816
- LSP_PLUGIN_SCOPE="$SCOPE_VALUE"
817
- fi
818
- fi
819
-
820
- # Check if ANY language detection triggered
821
- if [[ "$LSP_REQUEST_DETECTED" == "true" ]] || \
822
- [[ "$LSP_PROJECT_NEEDS_TS" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_TS" == "true" ]] || \
823
- [[ "$LSP_PROJECT_NEEDS_PY" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_PY" == "true" ]] || \
824
- [[ "$LSP_PROJECT_NEEDS_RUST" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_RUST" == "true" ]] || \
825
- [[ "$LSP_PROJECT_NEEDS_CSHARP" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_CSHARP" == "true" ]] || \
826
- [[ "$LSP_PROJECT_NEEDS_GO" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_GO" == "true" ]] || \
827
- [[ "$LSP_PROJECT_NEEDS_JAVA" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_JAVA" == "true" ]]; then
828
- LSP_NEEDS_INSTALL="true"
829
- fi
830
-
831
- # ==============================================================================
832
- # LSP SETUP SUGGESTION (v1.0.203) - Suggest setup instead of auto-installing
833
- # ==============================================================================
834
- # When languages are detected but auto-install is disabled, suggest running
835
- # specweave lsp setup which does multi-repo scanning and asks user approval
836
- LSP_SETUP_SUGGESTION_MSG=""
837
- if [[ -n "$SW_PROJECT_ROOT" ]]; then
838
- LSP_SETUP_STATE_FILE="$SW_PROJECT_ROOT/.specweave/state/lsp-setup-suggested.flag"
839
- else
840
- LSP_SETUP_STATE_FILE=""
841
- fi
842
-
843
- if [[ "$LSP_NEEDS_INSTALL" == "true" ]] && [[ "$LSP_AUTO_INSTALL" != "true" ]]; then
844
- # Check if we've already suggested setup in this session
845
- if [[ ! -f "$LSP_SETUP_STATE_FILE" ]] && [[ -n "${ENABLE_LSP_TOOL:-}" ]]; then
846
- # Build list of detected languages
847
- DETECTED_LANGS=""
848
- if [[ "$LSP_PROJECT_NEEDS_TS" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_TS" == "true" ]]; then
849
- DETECTED_LANGS="TypeScript"
850
- fi
851
- if [[ "$LSP_PROJECT_NEEDS_PY" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_PY" == "true" ]]; then
852
- [[ -n "$DETECTED_LANGS" ]] && DETECTED_LANGS="$DETECTED_LANGS, "
853
- DETECTED_LANGS="${DETECTED_LANGS}Python"
854
- fi
855
- if [[ "$LSP_PROJECT_NEEDS_RUST" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_RUST" == "true" ]]; then
856
- [[ -n "$DETECTED_LANGS" ]] && DETECTED_LANGS="$DETECTED_LANGS, "
857
- DETECTED_LANGS="${DETECTED_LANGS}Rust"
858
- fi
859
- if [[ "$LSP_PROJECT_NEEDS_CSHARP" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_CSHARP" == "true" ]]; then
860
- [[ -n "$DETECTED_LANGS" ]] && DETECTED_LANGS="$DETECTED_LANGS, "
861
- DETECTED_LANGS="${DETECTED_LANGS}C#"
862
- fi
863
- if [[ "$LSP_PROJECT_NEEDS_GO" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_GO" == "true" ]]; then
864
- [[ -n "$DETECTED_LANGS" ]] && DETECTED_LANGS="$DETECTED_LANGS, "
865
- DETECTED_LANGS="${DETECTED_LANGS}Go"
866
- fi
867
- if [[ "$LSP_PROJECT_NEEDS_JAVA" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_JAVA" == "true" ]]; then
868
- [[ -n "$DETECTED_LANGS" ]] && DETECTED_LANGS="$DETECTED_LANGS, "
869
- DETECTED_LANGS="${DETECTED_LANGS}Java"
870
- fi
871
-
872
- # v1.0.235: Check if plugins are missing (expanded language support)
873
- MISSING_PLUGINS="false"
874
- INSTALLED_PLUGINS_FILE="$HOME/.claude/plugins/installed_plugins.json"
875
- _check_plugin() {
876
- local plugin_name="$1"
877
- if [[ -f "$INSTALLED_PLUGINS_FILE" ]]; then
878
- if ! grep -q "\"${plugin_name}@" "$INSTALLED_PLUGINS_FILE" 2>/dev/null; then
879
- MISSING_PLUGINS="true"
880
- fi
881
- else
882
- MISSING_PLUGINS="true"
883
- fi
884
- }
885
- if [[ "$LSP_PROJECT_NEEDS_TS" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_TS" == "true" ]]; then
886
- _check_plugin "vtsls"
887
- fi
888
- if [[ "$LSP_PROJECT_NEEDS_PY" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_PY" == "true" ]]; then
889
- _check_plugin "pyright"
890
- fi
891
- if [[ "$LSP_PROJECT_NEEDS_RUST" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_RUST" == "true" ]]; then
892
- _check_plugin "rust-analyzer"
893
- fi
894
- if [[ "$LSP_PROJECT_NEEDS_CSHARP" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_CSHARP" == "true" ]]; then
895
- _check_plugin "csharp-lsp"
896
- fi
897
- if [[ "$LSP_PROJECT_NEEDS_GO" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_GO" == "true" ]]; then
898
- _check_plugin "gopls"
899
- fi
900
- if [[ "$LSP_PROJECT_NEEDS_JAVA" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_JAVA" == "true" ]]; then
901
- _check_plugin "jdtls"
902
- fi
903
-
904
- if [[ "$MISSING_PLUGINS" == "true" ]] && [[ -n "$DETECTED_LANGS" ]]; then
905
- LSP_SETUP_SUGGESTION_MSG="💡 **LSP Setup Available**
906
-
907
- Detected languages: **${DETECTED_LANGS}**
908
-
909
- For enhanced code intelligence (find references, go to definition, hover), run:
910
- \`\`\`bash
911
- specweave lsp setup
912
- \`\`\`
913
-
914
- This will scan your project (including nested repos) and let you choose which LSP plugins to install.
915
-
916
- ---
917
-
918
- "
919
- # Mark as suggested (only in initialized SpecWeave projects)
920
- if [[ -n "$LSP_SETUP_STATE_FILE" ]] && [[ -n "$SW_PROJECT_ROOT" ]]; then
921
- mkdir -p "$(dirname "$LSP_SETUP_STATE_FILE")" 2>/dev/null
922
- touch "$LSP_SETUP_STATE_FILE" 2>/dev/null
923
- fi
924
- fi
925
- fi
926
- fi
927
-
928
- if [[ "$LSP_NEEDS_INSTALL" == "true" ]] && [[ "$LSP_AUTO_INSTALL" == "true" ]]; then
929
- # CRITICAL: Skip LSP plugin installation if ENABLE_LSP_TOOL is not set (v1.0.195)
930
- # Installing plugins without this env var is useless - they won't work
931
- if [[ -z "${ENABLE_LSP_TOOL:-}" ]]; then
932
- # Don't install - just show the setup message (already handled by LSP_ENV_SETUP_MSG)
933
- LSP_NEEDS_INSTALL="false"
934
- fi
935
- fi
936
-
937
- if [[ "$LSP_NEEDS_INSTALL" == "true" ]] && [[ "$LSP_AUTO_INSTALL" == "true" ]]; then
938
- # v1.0.397: Respect global suggest-only mode for LSP plugins too (consent-first)
939
- if [[ "$PLUGIN_SUGGEST_ONLY" == "true" ]]; then
940
- LSP_SUGGEST_CMDS=""
941
- if [[ "$LSP_PROJECT_NEEDS_TS" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_TS" == "true" ]]; then
942
- VTSLS_INSTALLED=$(jq -r '."vtsls@claude-code-lsps" // false' "$HOME/.claude/plugins/installed_plugins.json" 2>/dev/null)
943
- [[ "$VTSLS_INSTALLED" != "true" ]] && LSP_SUGGEST_CMDS="${LSP_SUGGEST_CMDS} - **TypeScript**: \`claude plugin install vtsls@claude-code-lsps --scope ${LSP_PLUGIN_SCOPE}\`\n"
944
- fi
945
- if [[ "$LSP_PROJECT_NEEDS_PY" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_PY" == "true" ]]; then
946
- PYRIGHT_INSTALLED=$(jq -r '."pyright@claude-code-lsps" // false' "$HOME/.claude/plugins/installed_plugins.json" 2>/dev/null)
947
- [[ "$PYRIGHT_INSTALLED" != "true" ]] && LSP_SUGGEST_CMDS="${LSP_SUGGEST_CMDS} - **Python**: \`claude plugin install pyright@claude-code-lsps --scope ${LSP_PLUGIN_SCOPE}\`\n"
948
- fi
949
- if [[ "$LSP_PROJECT_NEEDS_RUST" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_RUST" == "true" ]]; then
950
- RUST_ANALYZER_INSTALLED=$(jq -r '."rust-analyzer@claude-code-lsps" // false' "$HOME/.claude/plugins/installed_plugins.json" 2>/dev/null)
951
- [[ "$RUST_ANALYZER_INSTALLED" != "true" ]] && LSP_SUGGEST_CMDS="${LSP_SUGGEST_CMDS} - **Rust**: \`claude plugin install rust-analyzer@claude-code-lsps --scope ${LSP_PLUGIN_SCOPE}\`\n"
952
- fi
953
- if [[ -n "$LSP_SUGGEST_CMDS" ]]; then
954
- LSP_INSTALL_MSG="**Suggested LSP plugins**:\n${LSP_SUGGEST_CMDS}After installing, **restart Claude Code** to use LSP features.\n\n---\n\n"
955
- fi
956
- else
957
- # NORMAL MODE (user opted in with suggestOnly: false) - Actually install LSP plugins
958
- # Check if marketplace is already installed
959
- MARKETPLACE_DIR="$HOME/.claude/plugins/marketplaces/claude-code-lsps"
960
- if [[ ! -d "$MARKETPLACE_DIR" ]] && command -v claude >/dev/null 2>&1; then
961
- # Install the marketplace
962
- if timeout 15 claude plugin marketplace add "$LSP_MARKETPLACE_URL" >/dev/null 2>&1; then
963
- LSP_INSTALL_MSG="✅ **LSP marketplace installed**: \`$LSP_MARKETPLACE\`
964
- "
965
- fi
966
- fi
967
-
968
- # Auto-install TypeScript LSP plugin (vtsls) when TypeScript project/prompt detected
969
- # v1.0.196: Uses --scope $LSP_PLUGIN_SCOPE (default: project)
970
- if [[ "$LSP_PROJECT_NEEDS_TS" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_TS" == "true" ]]; then
971
- VTSLS_INSTALLED=$(jq -r '."vtsls@claude-code-lsps" // false' "$HOME/.claude/plugins/installed_plugins.json" 2>/dev/null)
972
- if [[ "$VTSLS_INSTALLED" != "true" ]] && command -v claude >/dev/null 2>&1; then
973
- if timeout 15 claude plugin install vtsls@claude-code-lsps --scope $LSP_PLUGIN_SCOPE >/dev/null 2>&1; then
974
- LSP_INSTALL_MSG="${LSP_INSTALL_MSG}✅ **TypeScript LSP installed**: \`vtsls@claude-code-lsps\` (scope: $LSP_PLUGIN_SCOPE)
975
- "
976
- fi
977
- fi
978
- fi
979
-
980
- # Auto-install Python LSP plugin (pyright) when Python project/prompt detected
981
- # v1.0.196: Uses --scope $LSP_PLUGIN_SCOPE (default: project)
982
- if [[ "$LSP_PROJECT_NEEDS_PY" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_PY" == "true" ]]; then
983
- PYRIGHT_INSTALLED=$(jq -r '."pyright@claude-code-lsps" // false' "$HOME/.claude/plugins/installed_plugins.json" 2>/dev/null)
984
- if [[ "$PYRIGHT_INSTALLED" != "true" ]] && command -v claude >/dev/null 2>&1; then
985
- if timeout 15 claude plugin install pyright@claude-code-lsps --scope $LSP_PLUGIN_SCOPE >/dev/null 2>&1; then
986
- LSP_INSTALL_MSG="${LSP_INSTALL_MSG}✅ **Python LSP installed**: \`pyright@claude-code-lsps\` (scope: $LSP_PLUGIN_SCOPE)
987
- "
988
- fi
989
- fi
990
- fi
991
-
992
- # Auto-install Rust LSP plugin (rust-analyzer) when Rust project/prompt detected
993
- # v1.0.196: Uses --scope $LSP_PLUGIN_SCOPE (default: project)
994
- if [[ "$LSP_PROJECT_NEEDS_RUST" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_RUST" == "true" ]]; then
995
- RUST_ANALYZER_INSTALLED=$(jq -r '."rust-analyzer@claude-code-lsps" // false' "$HOME/.claude/plugins/installed_plugins.json" 2>/dev/null)
996
- if [[ "$RUST_ANALYZER_INSTALLED" != "true" ]] && command -v claude >/dev/null 2>&1; then
997
- if timeout 15 claude plugin install rust-analyzer@claude-code-lsps --scope $LSP_PLUGIN_SCOPE >/dev/null 2>&1; then
998
- LSP_INSTALL_MSG="${LSP_INSTALL_MSG}✅ **Rust LSP installed**: \`rust-analyzer@claude-code-lsps\` (scope: $LSP_PLUGIN_SCOPE)
999
- "
1000
- fi
1001
- fi
1002
- fi
1003
-
1004
- if [[ -n "$LSP_INSTALL_MSG" ]]; then
1005
- LSP_INSTALL_MSG="${LSP_INSTALL_MSG}
1006
- ---
1007
-
1008
- "
1009
- fi
1010
- fi
1011
- fi
1012
-
1013
- # ==============================================================================
1014
- # VSKILL MARKETPLACE PLUGIN RECOMMENDATION (v1.0.542)
1015
- # ==============================================================================
1016
- VSKILL_SUGGEST_MSG=""
1017
-
1018
- detect_vskill_recommendations() {
1019
- # Testing: Keyword matching logic is tested via TypeScript mirror (llm-plugin-detector.vskill.test.ts).
1020
- # Bash-specific behavior (grep -qwi word boundary, case/esac, TMPDIR markers) is validated manually.
1021
- local prompt
1022
- prompt=$(echo "$1" | tr '[:upper:]' '[:lower:]')
1023
- local suggestions=""
1024
- local match_count=0
1025
- local max_suggestions=3
1026
-
1027
- # Note: On macOS, TMPDIR is per-session (/var/folders/...) so markers auto-reset.
1028
- # On Linux, TMPDIR often defaults to /tmp (shared), so markers may persist across sessions.
1029
- # This is an accepted trade-off — worst case, suggestions appear only once per reboot on Linux.
1030
-
1031
- # --- mobile ---
1032
- # shortKeywords (word-boundary): ios, apk
1033
- # keywords (substring): testflight, app store, play store, react native, react-native, expo, mobile app, flutter, xcode, cocoapods, fastlane, app-store-connect, app deployment
1034
- if [[ $match_count -lt $max_suggestions ]]; then
1035
- local mobile_match="false"
1036
- if echo "$prompt" | grep -qwi 'ios'; then mobile_match="true"; fi
1037
- if [[ "$mobile_match" == "false" ]] && echo "$prompt" | grep -qwi 'apk'; then mobile_match="true"; fi
1038
- if [[ "$mobile_match" == "false" ]]; then
1039
- case "$prompt" in
1040
- *testflight*|*"app store"*|*"play store"*|*"react native"*|*react-native*|*expo*|*"mobile app"*|*flutter*|*xcode*|*cocoapods*|*fastlane*|*app-store-connect*|*"app deployment"*) mobile_match="true" ;;
1041
- esac
1042
- fi
1043
- if [[ "$mobile_match" == "true" ]]; then
1044
- if ! check_plugin_in_vskill_lock "mobile"; then
1045
- if [[ ! -f "${TMPDIR:-/tmp}/specweave-vskill-suggested-mobile" ]]; then
1046
- touch "${TMPDIR:-/tmp}/specweave-vskill-suggested-mobile" 2>/dev/null
1047
- suggestions="${suggestions}- **mobile** (App Store, TestFlight, mobile CI/CD): \`vskill i anton-abyzov/vskill/appstore\`\n"
1048
- match_count=$((match_count + 1))
1049
- fi
1050
- fi
1051
- fi
1052
- fi
1053
-
1054
- # --- google-workspace ---
1055
- # shortKeywords: (none)
1056
- # keywords (substring): google sheets, google docs, google drive, google slides, google calendar, google workspace, gmail api, sheets api
1057
- if [[ $match_count -lt $max_suggestions ]]; then
1058
- local gws_match="false"
1059
- case "$prompt" in
1060
- *"google sheets"*|*"google docs"*|*"google drive"*|*"google slides"*|*"google calendar"*|*"google workspace"*|*"gmail api"*|*"sheets api"*) gws_match="true" ;;
1061
- esac
1062
- if [[ "$gws_match" == "true" ]]; then
1063
- if ! check_plugin_in_vskill_lock "google-workspace"; then
1064
- if [[ ! -f "${TMPDIR:-/tmp}/specweave-vskill-suggested-google-workspace" ]]; then
1065
- touch "${TMPDIR:-/tmp}/specweave-vskill-suggested-google-workspace" 2>/dev/null
1066
- suggestions="${suggestions}- **google-workspace** (Gmail, Drive, Sheets, Docs, Calendar): \`vskill i anton-abyzov/vskill/gws\`\n"
1067
- match_count=$((match_count + 1))
1068
- fi
1069
- fi
1070
- fi
1071
- fi
1072
-
1073
- # --- marketing ---
1074
- # shortKeywords: (none)
1075
- # keywords (substring): linkedin, instagram, social media, twitter, facebook, tiktok, content marketing, social post, blog post, newsletter, slack messaging
1076
- if [[ $match_count -lt $max_suggestions ]]; then
1077
- local mktg_match="false"
1078
- case "$prompt" in
1079
- *linkedin*|*instagram*|*"social media"*|*twitter*|*facebook*|*tiktok*|*"content marketing"*|*"social post"*|*"blog post"*|*newsletter*|*"slack messaging"*) mktg_match="true" ;;
1080
- esac
1081
- if [[ "$mktg_match" == "true" ]]; then
1082
- if ! check_plugin_in_vskill_lock "marketing"; then
1083
- if [[ ! -f "${TMPDIR:-/tmp}/specweave-vskill-suggested-marketing" ]]; then
1084
- touch "${TMPDIR:-/tmp}/specweave-vskill-suggested-marketing" 2>/dev/null
1085
- suggestions="${suggestions}- **marketing** (slack-messaging, social-media-posting): \`vskill i anton-abyzov/vskill/slack-messaging\`\n"
1086
- match_count=$((match_count + 1))
1087
- fi
1088
- fi
1089
- fi
1090
- fi
1091
-
1092
- # --- productivity ---
1093
- # shortKeywords: (none)
1094
- # keywords (substring): notion, todoist, trello, asana, monday.com, obsidian, time tracking, project management, task management
1095
- if [[ $match_count -lt $max_suggestions ]]; then
1096
- local prod_match="false"
1097
- case "$prompt" in
1098
- *notion*|*todoist*|*trello*|*asana*|*monday.com*|*obsidian*|*"time tracking"*|*"project management"*|*"task management"*) prod_match="true" ;;
1099
- esac
1100
- if [[ "$prod_match" == "true" ]]; then
1101
- if ! check_plugin_in_vskill_lock "productivity"; then
1102
- if [[ ! -f "${TMPDIR:-/tmp}/specweave-vskill-suggested-productivity" ]]; then
1103
- touch "${TMPDIR:-/tmp}/specweave-vskill-suggested-productivity" 2>/dev/null
1104
- suggestions="${suggestions}- **productivity** (Notion, Todoist, Trello, Asana, Obsidian): \`vskill i anton-abyzov/vskill/survey-passing\`\n"
1105
- match_count=$((match_count + 1))
1106
- fi
1107
- fi
1108
- fi
1109
- fi
1110
-
1111
- # --- skills ---
1112
- # shortKeywords: (none)
1113
- # keywords (substring): find plugin, search plugin, discover skill, browse marketplace, vskill search, plugin search
1114
- if [[ $match_count -lt $max_suggestions ]]; then
1115
- local skills_match="false"
1116
- case "$prompt" in
1117
- *"find plugin"*|*"search plugin"*|*"discover skill"*|*"browse marketplace"*|*"vskill search"*|*"plugin search"*) skills_match="true" ;;
1118
- esac
1119
- if [[ "$skills_match" == "true" ]]; then
1120
- if ! check_plugin_in_vskill_lock "skills"; then
1121
- if [[ ! -f "${TMPDIR:-/tmp}/specweave-vskill-suggested-skills" ]]; then
1122
- touch "${TMPDIR:-/tmp}/specweave-vskill-suggested-skills" 2>/dev/null
1123
- suggestions="${suggestions}- **skills** (skill discovery and installation): \`vskill i anton-abyzov/vskill/scout\`\n"
1124
- match_count=$((match_count + 1))
1125
- fi
1126
- fi
1127
- fi
1128
- fi
1129
-
1130
- if [[ -n "$suggestions" ]]; then
1131
- printf '**Suggested vskill plugins** (not yet installed):\n%b' "$suggestions"
1132
- fi
1133
- }
1134
-
1135
- if [[ -n "$PROMPT" ]]; then
1136
- VSKILL_SUGGEST_MSG=$(detect_vskill_recommendations "$PROMPT")
1137
- fi
1138
-
1139
- # ==============================================================================
1140
- # EXPLICIT LSP REQUEST DETECTION (v1.0.181)
1141
- # ==============================================================================
1142
- # Detects when users explicitly ask to "use LSP" for tasks like "find references"
1143
- # and explains that LSP in Claude Code provides background enhancement, not explicit tools.
1144
- # v1.0.201: SEMANTIC LSP VIA CLI FALLBACK
1145
- # When user asks for LSP features (find references, go to definition, etc.),
1146
- # instruct Claude to use `specweave lsp` commands which provide REAL semantic analysis
1147
- # via TsServerClient - NOT grep text matching!
1148
- # ==============================================================================
1149
- # v1.0.235: LSP AUTO-INSTALL + CLI INSTRUCTIONS
1150
- # ==============================================================================
1151
- # When user requests LSP operations (find references, go to definition, etc.):
1152
- # 1. Detect which language from prompt/project context
1153
- # 2. Check if that language's LSP binary + plugin are installed
1154
- # 3. If missing, return AUTO-INSTALL commands for Claude to run silently
1155
- # 4. Then provide CLI instructions for the actual LSP operation
1156
- LSP_EXPLICIT_REQUEST_MSG=""
1157
- if echo "$PROMPT" | grep -qiE "(use|with|via)[[:space:]]+(the[[:space:]]+)?LSP|LSP[[:space:]]+(find|get|show|goto|hover|definition|references|implementations)|(find|get|show)[[:space:]]+(all[[:space:]]+)?references|go[[:space:]]?to[[:space:]]?definition|where[[:space:]]+(is|are).*defined|what[[:space:]]+(uses|calls)|who[[:space:]]+(uses|calls)"; then
1158
-
1159
- # Build auto-install commands for missing LSP servers
1160
- LSP_AUTO_INSTALL_CMDS=""
1161
- _lsp_check_and_install() {
1162
- local lang="$1" binary="$2" binary_install="$3" plugin="$4" marketplace="${LSP_MARKETPLACE:-claude-code-lsps}"
1163
- # Check if binary is installed
1164
- if ! command -v "$binary" >/dev/null 2>&1; then
1165
- LSP_AUTO_INSTALL_CMDS="${LSP_AUTO_INSTALL_CMDS}
1166
- - **${lang}** binary missing: \`${binary_install}\`"
1167
- fi
1168
- # Check if plugin is installed
1169
- local plugins_file="$HOME/.claude/plugins/installed_plugins.json"
1170
- if [[ -f "$plugins_file" ]]; then
1171
- if ! grep -q "\"${plugin}@" "$plugins_file" 2>/dev/null; then
1172
- LSP_AUTO_INSTALL_CMDS="${LSP_AUTO_INSTALL_CMDS}
1173
- - **${lang}** plugin missing: \`claude plugin install ${plugin}@${marketplace} --scope ${LSP_PLUGIN_SCOPE:-project}\`"
1174
- fi
1175
- else
1176
- LSP_AUTO_INSTALL_CMDS="${LSP_AUTO_INSTALL_CMDS}
1177
- - **${lang}** plugin missing: \`claude plugin install ${plugin}@${marketplace} --scope ${LSP_PLUGIN_SCOPE:-project}\`"
1178
- fi
1179
- }
1180
-
1181
- # Check each detected language
1182
- if [[ "$LSP_PROJECT_NEEDS_TS" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_TS" == "true" ]]; then
1183
- _lsp_check_and_install "TypeScript" "typescript-language-server" "npm install -g typescript-language-server typescript" "vtsls"
1184
- fi
1185
- if [[ "$LSP_PROJECT_NEEDS_PY" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_PY" == "true" ]]; then
1186
- _lsp_check_and_install "Python" "pyright-langserver" "pip install pyright" "pyright"
1187
- fi
1188
- if [[ "$LSP_PROJECT_NEEDS_RUST" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_RUST" == "true" ]]; then
1189
- _lsp_check_and_install "Rust" "rust-analyzer" "rustup component add rust-analyzer" "rust-analyzer"
1190
- fi
1191
- if [[ "$LSP_PROJECT_NEEDS_CSHARP" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_CSHARP" == "true" ]]; then
1192
- _lsp_check_and_install "C#" "csharp-ls" "dotnet tool install -g csharp-ls" "csharp-lsp"
1193
- fi
1194
- if [[ "$LSP_PROJECT_NEEDS_GO" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_GO" == "true" ]]; then
1195
- _lsp_check_and_install "Go" "gopls" "go install golang.org/x/tools/gopls@latest" "gopls"
1196
- fi
1197
- if [[ "$LSP_PROJECT_NEEDS_JAVA" == "true" ]] || [[ "$LSP_PROMPT_NEEDS_JAVA" == "true" ]]; then
1198
- _lsp_check_and_install "Java" "jdtls" "brew install jdtls" "jdtls"
1199
- fi
1200
-
1201
- # Build the message
1202
- if [[ -n "$LSP_AUTO_INSTALL_CMDS" ]]; then
1203
- LSP_EXPLICIT_REQUEST_MSG="⚡ **LSP AUTO-INSTALL REQUIRED**
1204
-
1205
- The following LSP components are missing for this request. **Install them NOW before proceeding:**
1206
- ${LSP_AUTO_INSTALL_CMDS}
1207
-
1208
- **MANDATORY**: Run ALL missing install commands above using Bash tool, then use \`specweave lsp\` CLI:
1209
-
1210
- \`\`\`bash
1211
- specweave lsp refs <file> <symbol> # Find semantic references
1212
- specweave lsp def <file> <symbol> # Go to definition
1213
- specweave lsp hover <file> <symbol> # Type information
1214
- specweave lsp symbols <file> # List symbols
1215
- \`\`\`
1216
-
1217
- ⚠️ Do NOT use Grep for \"find references\" - use LSP for semantic accuracy.
1218
-
1219
- ---
1220
-
1221
- "
1222
- else
1223
- LSP_EXPLICIT_REQUEST_MSG="🎯 **LSP Semantic Analysis Available**
1224
-
1225
- Use SpecWeave LSP CLI for **semantic code intelligence** (not grep text matching):
1226
-
1227
- \`\`\`bash
1228
- specweave lsp refs <file> <symbol> # Find semantic references
1229
- specweave lsp def <file> <symbol> # Go to definition
1230
- specweave lsp hover <file> <symbol> # Type information
1231
- specweave lsp symbols <file> # List symbols
1232
- specweave lsp search <query> # Workspace symbol search
1233
- \`\`\`
1234
-
1235
- ⚠️ Do NOT use Grep for \"find references\" - use LSP for semantic accuracy.
1236
-
1237
- ---
1238
-
1239
- "
1240
- fi
1241
- fi
1242
-
1243
- # Only run if features are enabled and not disabled via env
1244
- if [[ "${SPECWEAVE_DISABLE_AUTO_LOAD:-0}" != "1" ]] && [[ "${SPECWEAVE_DISABLE_HOOKS:-0}" != "1" ]]; then
1245
- if [[ "$PLUGIN_AUTOLOAD_ENABLED" == "true" ]] || [[ "$INCREMENT_ASSIST_ENABLED" == "true" ]]; then
1246
-
1247
- # Quick skip: already using /sw: commands (user is in workflow)
1248
- if ! echo "$PROMPT" | grep -qE "^[[:space:]]*/sw:"; then
1249
-
1250
- # BYPASS: Native Claude Code slash commands (e.g., /context, /help, /doctor)
1251
- # Prevents 15s detect-intent timeout → LLM_DETECTION_FAILED → keyword fallback
1252
- # that falsely matches "test" as substring inside "/context". Pattern matches
1253
- # /word or /word-word prompts that don't mention specweave.
1254
- if echo "$PROMPT" | grep -qE "^[[:space:]]*/[a-z][a-z0-9-]*([[:space:]]|$)" && \
1255
- ! echo "$PROMPT" | grep -qiE "specweave"; then
1256
- echo '{"decision":"approve"}'
1257
- exit 0
1258
- fi
1259
-
1260
- # Check if specweave CLI is available
1261
- if command -v specweave >/dev/null 2>&1; then
1262
- # Setup logging (use project root, never create dirs at $HOME)
1263
- if [[ -n "$SW_PROJECT_ROOT" ]]; then
1264
- LAZY_LOAD_LOG="$SW_PROJECT_ROOT/.specweave/logs/lazy-loading.log"
1265
- else
1266
- LAZY_LOAD_LOG="/dev/null"
1267
- fi
1268
-
1269
- # Per-session cache to avoid redundant LLM calls (30 min TTL)
1270
- if [[ -n "$SW_PROJECT_ROOT" ]]; then
1271
- PROMPT_CACHE_DIR="$SW_PROJECT_ROOT/.specweave/state/prompt-cache"
1272
- mkdir -p "$PROMPT_CACHE_DIR" 2>/dev/null
1273
- else
1274
- PROMPT_CACHE_DIR="${TMPDIR:-/tmp}/specweave-prompt-cache"
1275
- mkdir -p "$PROMPT_CACHE_DIR" 2>/dev/null
1276
- fi
1277
- PROMPT_HASH=$(echo "$PROMPT" | md5sum 2>/dev/null | cut -c1-16 || md5 -qs "$PROMPT" 2>/dev/null | cut -c1-16 || echo "nohash")
1278
- CACHE_FILE="$PROMPT_CACHE_DIR/${PROMPT_HASH}.json"
1279
-
1280
- DETECT_OUTPUT=""
1281
- SHOULD_CALL_LLM=true
1282
-
1283
- # Check cache
1284
- if [[ -f "$CACHE_FILE" ]]; then
1285
- CACHE_AGE=$(($(date +%s) - $(stat -f%m "$CACHE_FILE" 2>/dev/null || stat -c%Y "$CACHE_FILE" 2>/dev/null || echo 0)))
1286
- if [[ "$CACHE_AGE" -lt 1800 ]]; then
1287
- DETECT_OUTPUT=$(cat "$CACHE_FILE" 2>/dev/null)
1288
- SHOULD_CALL_LLM=false
1289
- echo "[$(date -Iseconds)] detect-intent | cached=true | age=${CACHE_AGE}s" >> "$LAZY_LOAD_LOG"
1290
- fi
1291
- fi
1292
-
1293
- # Call LLM if not cached
1294
- if [[ "$SHOULD_CALL_LLM" == "true" ]]; then
1295
- START_TIME=$(perl -MTime::HiRes=time -e 'printf "%.0f", time*1000' 2>/dev/null || echo $(($(date +%s) * 1000)))
1296
-
1297
- # Write prompt to temp file to avoid all escaping issues (v1.0.153)
1298
- PROMPT_TMP_FILE=$(mktemp 2>/dev/null || echo "/tmp/specweave-prompt-$$")
1299
- printf '%s' "$PROMPT" > "$PROMPT_TMP_FILE"
1300
-
1301
- # ONE LLM call for BOTH plugins and increment (using --file flag)
1302
- if command -v timeout >/dev/null 2>&1; then
1303
- # v1.0.159: Reduced timeout to 15s with --setting-sources "" optimization
1304
- DETECT_OUTPUT=$(timeout 15 specweave detect-intent --file "$PROMPT_TMP_FILE" 2>/dev/null)
1305
- else
1306
- DETECT_OUTPUT=$(specweave detect-intent --file "$PROMPT_TMP_FILE" 2>/dev/null)
1307
- fi
1308
-
1309
- # Clean up temp file
1310
- rm -f "$PROMPT_TMP_FILE" 2>/dev/null
1311
-
1312
- END_TIME=$(perl -MTime::HiRes=time -e 'printf "%.0f", time*1000' 2>/dev/null || echo $(($(date +%s) * 1000)))
1313
- DURATION=$((END_TIME - START_TIME))
1314
-
1315
- # Cache result
1316
- [[ -n "$DETECT_OUTPUT" ]] && echo "$DETECT_OUTPUT" > "$CACHE_FILE" 2>/dev/null
1317
- echo "[$(date -Iseconds)] detect-intent | duration=${DURATION}ms | cached=false" >> "$LAZY_LOAD_LOG"
1318
- fi
1319
-
1320
- # Parse JSON response (extract complete JSON object from multi-line output)
1321
- LLM_DETECTION_FAILED=false
1322
- if [[ -n "$DETECT_OUTPUT" ]] && command -v jq >/dev/null 2>&1; then
1323
- # Extract JSON using awk: from first { to last } (handles multi-line JSON)
1324
- JSON_OUTPUT=$(echo "$DETECT_OUTPUT" | awk '/^\{/{found=1} found{print} /^\}/{if(found) exit}')
1325
-
1326
- if [[ -n "$JSON_OUTPUT" ]]; then
1327
- # ==================================================================
1328
- # ERROR RESPONSE DETECTION (v1.0.337)
1329
- # ==================================================================
1330
- # When detect-intent returns valid JSON but with error data (e.g.,
1331
- # nested session error, timeout, auth failure), the JSON has
1332
- # installMessage with error text, confidence=0, and no increment
1333
- # field. Without this check, the hook treats error responses as
1334
- # "LLM worked, nothing needed" and skips keyword fallback entirely.
1335
- INSTALL_MSG=$(echo "$JSON_OUTPUT" | jq -r '.installMessage // empty' 2>/dev/null)
1336
- HAS_INCREMENT=$(echo "$JSON_OUTPUT" | jq -r 'has("increment")' 2>/dev/null)
1337
- RESP_CONFIDENCE=$(echo "$JSON_OUTPUT" | jq -r '.confidence // 0' 2>/dev/null)
1338
- if [[ -n "$INSTALL_MSG" && "$HAS_INCREMENT" != "true" && "$RESP_CONFIDENCE" == "0" ]]; then
1339
- LLM_DETECTION_FAILED=true
1340
- echo "[$(date -Iseconds)] LLM detection failed | reason=error_response | msg=${INSTALL_MSG:0:100}" >> "$LAZY_LOAD_LOG"
1341
- fi
1342
-
1343
- # ==================================================================
1344
- # ON-DEMAND PLUGIN INSTALL (v1.0.540 — restored)
1345
- # ==================================================================
1346
- # When LLM detect-intent identifies plugins needed for this prompt,
1347
- # install any that aren't already present. Uses specweave CLI's
1348
- # --plugin flag for targeted install. Session markers prevent
1349
- # duplicate installs within the same Claude Code session.
1350
- # ==================================================================
1351
- DETECTED_PLUGINS=$(echo "$JSON_OUTPUT" | jq -r '.plugins[]? // empty' 2>/dev/null)
1352
- if [[ -n "$DETECTED_PLUGINS" ]]; then
1353
- CACHE_BASE="${HOME}/.claude/plugins/cache/specweave"
1354
- SKILLS_BASE="${SW_PROJECT_ROOT:-.}/.claude/skills"
1355
- # Use PPID (parent process = Claude Code) for session-level uniqueness
1356
- SESSION_MARKER_DIR="${TMPDIR:-/tmp}/specweave-ondemand-${PPID:-$$}"
1357
- mkdir -p "$SESSION_MARKER_DIR" 2>/dev/null
1358
- ONDEMAND_PIDS=()
1359
-
1360
- while IFS= read -r PLUGIN_NAME; do
1361
- [[ -z "$PLUGIN_NAME" ]] && continue
1362
- [[ "$PLUGIN_NAME" == "sw" ]] && continue # core always installed
1363
- # Sanitize: only allow alphanumeric, dash, underscore (prevent injection)
1364
- if [[ ! "$PLUGIN_NAME" =~ ^[a-zA-Z0-9_-]+$ ]]; then
1365
- continue
1366
- fi
1367
-
1368
- # Idempotency: skip if already installed or already attempted this session
1369
- if [[ -d "$CACHE_BASE/$PLUGIN_NAME" ]] || \
1370
- [[ -d "$SKILLS_BASE/$PLUGIN_NAME" ]] || \
1371
- [[ -f "$SESSION_MARKER_DIR/$PLUGIN_NAME" ]]; then
1372
- continue
1373
- fi
1374
-
1375
- # Install via CLI (handles both native and direct-copy modes)
1376
- specweave refresh-plugins --plugin "$PLUGIN_NAME" --quiet 2>/dev/null &
1377
- ONDEMAND_PIDS+=($!)
1378
- touch "$SESSION_MARKER_DIR/$PLUGIN_NAME" 2>/dev/null
1379
-
1380
- AUTOLOAD_PLUGINS_MSG="${AUTOLOAD_PLUGINS_MSG}Installed plugin: ${PLUGIN_NAME} (on-demand)."$'\n'
1381
- done <<< "$DETECTED_PLUGINS"
1382
-
1383
- # Wait for all background installs with 5s timeout
1384
- if [[ ${#ONDEMAND_PIDS[@]} -gt 0 ]]; then
1385
- for pid in "${ONDEMAND_PIDS[@]}"; do
1386
- ( sleep 5 && kill "$pid" 2>/dev/null ) &
1387
- local timeout_pid=$!
1388
- wait "$pid" 2>/dev/null
1389
- kill "$timeout_pid" 2>/dev/null 2>&1
1390
- done
1391
- fi
1392
- fi
1393
-
1394
- # ==================================================================
1395
- # EXTRACT ROUTING INFO EARLY (v1.0.155 - needed for agent directives)
1396
- # ==================================================================
1397
- ROUTING_SKILLS_COUNT=$(echo "$JSON_OUTPUT" | jq -r '.routing.skills // [] | length' 2>/dev/null)
1398
-
1399
- # ==================================================================
1400
- # INCREMENT SUGGESTION (from LLM response)
1401
- # ==================================================================
1402
- if [[ "$INCREMENT_ASSIST_ENABLED" == "true" ]]; then
1403
- INC_ACTION=$(echo "$JSON_OUTPUT" | jq -r '.increment.action // "none"' 2>/dev/null)
1404
- INC_CONF=$(echo "$JSON_OUTPUT" | jq -r '.increment.confidence // 0' 2>/dev/null)
1405
- INC_NAME=$(echo "$JSON_OUTPUT" | jq -r '.increment.suggestedName // empty' 2>/dev/null)
1406
- INC_REASON=$(echo "$JSON_OUTPUT" | jq -r '.increment.reasoning // empty' 2>/dev/null)
1407
- INC_KEYWORD=$(echo "$JSON_OUTPUT" | jq -r '.increment.relatedKeyword // empty' 2>/dev/null)
1408
- # v1.0.168: LLM decides if mandatory (not config-based)
1409
- INC_MANDATORY=$(echo "$JSON_OUTPUT" | jq -r '.increment.mandatory // false' 2>/dev/null)
1410
-
1411
- # Config-based override: if incrementAssist.mandatory=true in config,
1412
- # force mandatory for ALL detected implementation work (action != "none")
1413
- if [[ "$INCREMENT_MANDATORY_CONFIG" == "true" && "$INC_ACTION" != "none" ]]; then
1414
- INC_MANDATORY="true"
1415
- fi
1416
-
1417
- # v1.0.168: Parse skill invocation recommendation
1418
- SKILL_INVOCATION=$(echo "$JSON_OUTPUT" | jq -r '.skillInvocation.skill // empty' 2>/dev/null)
1419
- SKILL_REASON=$(echo "$JSON_OUTPUT" | jq -r '.skillInvocation.reason // empty' 2>/dev/null)
1420
- SKILL_MANDATORY=$(echo "$JSON_OUTPUT" | jq -r '.skillInvocation.mandatory // false' 2>/dev/null)
1421
-
1422
- # v1.0.198: Parse LSP recommendation from unified LLM detection
1423
- LSP_LLM_NEEDED=$(echo "$JSON_OUTPUT" | jq -r '.lsp.needed // false' 2>/dev/null)
1424
- LSP_LLM_OPERATION=$(echo "$JSON_OUTPUT" | jq -r '.lsp.operation // empty' 2>/dev/null)
1425
- LSP_LLM_LANGUAGE=$(echo "$JSON_OUTPUT" | jq -r '.lsp.language // empty' 2>/dev/null)
1426
- LSP_LLM_WARMUP=$(echo "$JSON_OUTPUT" | jq -r '.lsp.warmupRequired // false' 2>/dev/null)
1427
-
1428
- # Override grep-based detection with LLM decision
1429
- if [[ "$LSP_LLM_NEEDED" == "true" ]]; then
1430
- LSP_REQUEST_DETECTED="true"
1431
- # v1.0.235: Use LLM language detection for all supported languages
1432
- case "$LSP_LLM_LANGUAGE" in
1433
- typescript|javascript) LSP_PROMPT_NEEDS_TS="true" ;;
1434
- python) LSP_PROMPT_NEEDS_PY="true" ;;
1435
- rust) LSP_PROMPT_NEEDS_RUST="true" ;;
1436
- csharp|c#) LSP_PROMPT_NEEDS_CSHARP="true" ;;
1437
- go|golang) LSP_PROMPT_NEEDS_GO="true" ;;
1438
- java|kotlin) LSP_PROMPT_NEEDS_JAVA="true" ;;
1439
- esac
1440
- fi
1441
-
1442
- # Check confidence threshold
1443
- ABOVE=$(echo "$INC_CONF >= $INCREMENT_CONFIDENCE_THRESHOLD" | bc -l 2>/dev/null || echo 0)
1444
-
1445
- if [[ "$ABOVE" == "1" ]]; then
1446
- AUTOLOAD_PREFIX=""
1447
- # v1.0.166: Prepend external folder warning if detected
1448
- [[ -n "$EXTERNAL_FOLDER_DETECTED" ]] && AUTOLOAD_PREFIX="${EXTERNAL_FOLDER_DETECTED}"
1449
- # v1.0.179: Prepend LSP warning if language servers missing
1450
- [[ -n "$LSP_WARNING_MSG" ]] && AUTOLOAD_PREFIX="${AUTOLOAD_PREFIX}${LSP_WARNING_MSG}"
1451
- # v1.0.180: Prepend explicit LSP request explanation
1452
- [[ -n "$LSP_EXPLICIT_REQUEST_MSG" ]] && AUTOLOAD_PREFIX="${AUTOLOAD_PREFIX}${LSP_EXPLICIT_REQUEST_MSG}"
1453
- # v1.0.542: Prepend vskill plugin suggestions
1454
- [[ -n "$VSKILL_SUGGEST_MSG" ]] && AUTOLOAD_PREFIX="${AUTOLOAD_PREFIX}${VSKILL_SUGGEST_MSG}
1455
- "
1456
- [[ -n "$AUTOLOAD_PLUGINS_MSG" ]] && AUTOLOAD_PREFIX="${AUTOLOAD_PREFIX}${AUTOLOAD_PLUGINS_MSG}
1457
-
1458
- "
1459
- # Build agent spawn directive if routing skills available (v1.0.155)
1460
- AGENT_DIRECTIVE=""
1461
- # v1.0.168: Skill invocation directive (takes precedence over routing)
1462
- # Skill memories now loaded via DCI in SKILL.md (no hook injection)
1463
- # v1.0.260: Compacted AGENT_DIRECTIVE to save context budget
1464
- if [[ -n "$SKILL_INVOCATION" ]]; then
1465
- if [[ "$SKILL_MANDATORY" == "true" ]]; then
1466
- AGENT_DIRECTIVE="
1467
- MANDATORY: Also call \`Skill({ skill: \"${SKILL_INVOCATION}\" })\` — ${SKILL_REASON:-specialized support needed}."
1468
- else
1469
- AGENT_DIRECTIVE="
1470
- Recommended: \`Skill({ skill: \"${SKILL_INVOCATION}\" })\` — ${SKILL_REASON:-specialized support for this task}."
1471
- fi
1472
- elif [[ "$ROUTING_SKILLS_COUNT" -gt 0 ]]; then
1473
- PRIMARY_PLUGIN=$(echo "$JSON_OUTPUT" | jq -r '.routing.skills[] | select(.priority == "primary") | .plugin // empty' 2>/dev/null | head -1)
1474
- PRIMARY_SKILL_NAME=$(echo "$JSON_OUTPUT" | jq -r '.routing.skills[] | select(.priority == "primary") | .name // empty' 2>/dev/null | head -1)
1475
- PRIMARY_REASON=$(echo "$JSON_OUTPUT" | jq -r '.routing.skills[] | select(.priority == "primary") | .reason // empty' 2>/dev/null | head -1)
1476
- if [[ -n "$PRIMARY_PLUGIN" && -n "$PRIMARY_SKILL_NAME" ]]; then
1477
- # v1.0.260: Compacted routing directive to save context budget
1478
- AGENT_DIRECTIVE="
1479
- Then spawn agent: \`Task({ subagent_type: \"${PRIMARY_PLUGIN}:${PRIMARY_SKILL_NAME}\", description: \"${PRIMARY_REASON:-Implementation}\" })\`"
1480
- fi
1481
- fi
1482
-
1483
- case "$INC_ACTION" in
1484
- new)
1485
- CMD="/sw:increment"
1486
- [[ -n "$INC_NAME" ]] && CMD="/sw:increment \"$INC_NAME\""
1487
-
1488
- # v1.0.169+: Call sw:increment skill directly
1489
- # Pass user prompt so skill can extract context
1490
- # INC_MANDATORY comes from detect-intent LLM response
1491
- if [[ "$INC_MANDATORY" == "true" ]]; then
1492
- # v1.0.260: Removed prompt embedding from SKILL FIRST args to save ~800 chars.
1493
- # The skill reads the user's prompt from conversation context (it's already there).
1494
-
1495
- # v1.0.243: Smart interview gate — LLM assesses prompt completeness
1496
- DEEP_INTERVIEW_MSG=""
1497
- if [[ "$DEEP_INTERVIEW_ENABLED" == "true" ]]; then
1498
- DEEP_INTERVIEW_MSG=" Assess prompt completeness first — sw:pm skill will assess complexity and determine question count. If gaps exist, ask targeted questions (count depends on complexity) before calling."
1499
- fi
1500
-
1501
- MSG="${WIP_WARNING}${AUTOLOAD_PREFIX}SKILL FIRST: \`Skill({ skill: \"sw:increment\" })\` — call BEFORE implementation.
1502
- Detection: ${INC_REASON} (confidence: ${INC_CONF}).${AGENT_DIRECTIVE}${DEEP_INTERVIEW_MSG}
1503
- After increment, chain domain skills per tech stack (see CLAUDE.md Skill Chaining)."
1504
- output_approve_with_context "$MSG"
1505
- exit 0
1506
- else
1507
- # v1.0.260: Removed prompt embedding to save context budget
1508
- MSG="${WIP_WARNING}${AUTOLOAD_PREFIX}Increment suggested: \`Skill({ skill: \"sw:increment\" })\` or \`$CMD\`. Reason: $INC_REASON${AGENT_DIRECTIVE}"
1509
- output_approve_with_context "$MSG"
1510
- exit 0
1511
- fi
1512
- ;;
1513
-
1514
- hotfix)
1515
- # v1.0.260: Removed prompt embedding to save context budget
1516
- MSG="${WIP_WARNING}${AUTOLOAD_PREFIX}Hotfix detected: \`Skill({ skill: \"sw:increment\", args: \"--type=hotfix\" })\`. Reason: $INC_REASON"
1517
- output_approve_with_context "$MSG"
1518
- exit 0
1519
- ;;
1520
-
1521
- reopen)
1522
- HINT=""
1523
- [[ -n "$INC_KEYWORD" ]] && HINT=" (look for: *$INC_KEYWORD*)"
1524
- MSG="${AUTOLOAD_PREFIX}Related to previous work$HINT. Consider: \`/sw:status\` then \`specweave resume <id>\`. Reason: $INC_REASON"
1525
- output_approve_with_context "$MSG"
1526
- exit 0
1527
- ;;
1528
-
1529
- small_fix)
1530
- # v1.0.260: Removed prompt embedding to save context budget
1531
- CMD_SMALLFIX="/sw:increment"
1532
- [[ -n "$INC_NAME" ]] && CMD_SMALLFIX="/sw:increment \"$INC_NAME\""
1533
-
1534
- MSG="${WIP_WARNING}${AUTOLOAD_PREFIX}Small change — consider tracking: \`Skill({ skill: \"sw:increment\" })\` or \`$CMD_SMALLFIX\`. Reason: $INC_REASON${AGENT_DIRECTIVE}"
1535
- output_approve_with_context "$MSG"
1536
- exit 0
1537
- ;;
1538
- esac
1539
- fi
1540
-
1541
- # ==================================================================
1542
- # SKILL-ONLY INVOCATION (v1.0.196)
1543
- # Handle skill invocation when NO increment is suggested but LLM
1544
- # recommends a skill (e.g., LSP skill for "find references" prompts)
1545
- # ==================================================================
1546
- if [[ "$ABOVE" != "1" && -n "$SKILL_INVOCATION" ]]; then
1547
- # Build prefix messages
1548
- SKILL_ONLY_PREFIX=""
1549
- [[ -n "$LSP_WARNING_MSG" ]] && SKILL_ONLY_PREFIX="${LSP_WARNING_MSG}"
1550
- [[ -n "$LSP_EXPLICIT_REQUEST_MSG" ]] && SKILL_ONLY_PREFIX="${SKILL_ONLY_PREFIX}${LSP_EXPLICIT_REQUEST_MSG}"
1551
-
1552
- if [[ "$SKILL_MANDATORY" == "true" ]]; then
1553
- MSG="${SKILL_ONLY_PREFIX}SKILL REQUIRED: \`Skill({ skill: \"${SKILL_INVOCATION}\" })\` — call before proceeding. ${SKILL_REASON:-Specialized support needed.}"
1554
- output_approve_with_context "$MSG"
1555
- exit 0
1556
- else
1557
- MSG="${SKILL_ONLY_PREFIX}Skill recommended: \`Skill({ skill: \"${SKILL_INVOCATION}\" })\`. ${SKILL_REASON:-Specialized support for this task.}"
1558
- output_approve_with_context "$MSG"
1559
- exit 0
1560
- fi
1561
- fi
1562
- fi
1563
-
1564
- # ==================================================================
1565
- # SKILL ROUTING (from LLM response) - v1.0.150+
1566
- # ==================================================================
1567
- # Extract skill routing for brain message
1568
- ROUTING_SKILLS_COUNT=$(echo "$JSON_OUTPUT" | jq -r '.routing.skills // [] | length' 2>/dev/null)
1569
- ROUTING_MSG=""
1570
-
1571
- if [[ "$ROUTING_SKILLS_COUNT" -gt 0 ]]; then
1572
- # Extract primary skill
1573
- PRIMARY_SKILL=$(echo "$JSON_OUTPUT" | jq -r '.routing.skills[] | select(.priority == "primary") | .fullName // empty' 2>/dev/null | head -1)
1574
- PRIMARY_INVOKE=$(echo "$JSON_OUTPUT" | jq -r '.routing.skills[] | select(.priority == "primary") | .invokeWhen // "after_increment"' 2>/dev/null | head -1)
1575
- PRIMARY_REASON=$(echo "$JSON_OUTPUT" | jq -r '.routing.skills[] | select(.priority == "primary") | .reason // empty' 2>/dev/null | head -1)
1576
-
1577
- # Extract secondary skills
1578
- SECONDARY_SKILLS=$(echo "$JSON_OUTPUT" | jq -r '.routing.skills[] | select(.priority == "secondary") | .fullName' 2>/dev/null | tr '\n' ', ' | sed 's/,$//')
1579
-
1580
- # Extract workflow info
1581
- SUGGEST_PLAN=$(echo "$JSON_OUTPUT" | jq -r '.routing.workflow.suggestPlanMode // false' 2>/dev/null)
1582
- WORKFLOW_PHASES=$(echo "$JSON_OUTPUT" | jq -r '.routing.workflow.phases[]?' 2>/dev/null | tr '\n' ' ')
1583
- ROUTING_REASON=$(echo "$JSON_OUTPUT" | jq -r '.routing.reasoning // empty' 2>/dev/null)
1584
-
1585
- echo "[$(date -Iseconds)] routing | primary=${PRIMARY_SKILL:-none} | secondary=${SECONDARY_SKILLS:-none} | invokeWhen=${PRIMARY_INVOKE}" >> "$LAZY_LOAD_LOG"
1586
- fi
1587
-
1588
- # ==================================================================
1589
- # BUILD BRAIN MESSAGE (combining all analysis)
1590
- # ==================================================================
1591
- # Only show brain message if we have meaningful routing info
1592
- if [[ "$ROUTING_SKILLS_COUNT" -gt 0 || -n "$AUTOLOAD_PLUGINS_MSG" ]]; then
1593
- BRAIN_MSG=""
1594
-
1595
- # Compact router output — one line per decision
1596
- if [[ "$INC_ACTION" == "new" || "$INC_ACTION" == "hotfix" ]]; then
1597
- BRAIN_MSG+="Increment: create \\\"${INC_NAME:-new-feature}\\\" (${INC_ACTION}). "
1598
- elif [[ "$INC_ACTION" == "reopen" ]]; then
1599
- BRAIN_MSG+="Increment: reopen existing"
1600
- [[ -n "$INC_KEYWORD" ]] && BRAIN_MSG+=" (${INC_KEYWORD})"
1601
- BRAIN_MSG+=". "
1602
- fi
1603
-
1604
- if [[ -n "$PRIMARY_SKILL" ]]; then
1605
- # Extract agent type for Task tool
1606
- PRIMARY_PLUGIN=$(echo "$JSON_OUTPUT" | jq -r '.routing.skills[] | select(.priority == "primary") | .plugin // empty' 2>/dev/null | head -1)
1607
- PRIMARY_SKILL_NAME=$(echo "$JSON_OUTPUT" | jq -r '.routing.skills[] | select(.priority == "primary") | .name // empty' 2>/dev/null | head -1)
1608
- BRAIN_MSG+="Primary skill: ${PRIMARY_SKILL}"
1609
- # v2.1.0: Use plugin:skill format for agent type (e.g., backend:dotnet)
1610
- [[ -n "$PRIMARY_PLUGIN" && -n "$PRIMARY_SKILL_NAME" ]] && BRAIN_MSG+=" (agent: ${PRIMARY_PLUGIN}:${PRIMARY_SKILL_NAME})"
1611
- BRAIN_MSG+=", invoke: ${PRIMARY_INVOKE:-after_increment}. "
1612
- [[ -n "$SECONDARY_SKILLS" ]] && BRAIN_MSG+="Also: ${SECONDARY_SKILLS}. "
1613
- fi
1614
-
1615
- [[ "$SUGGEST_PLAN" == "true" ]] && BRAIN_MSG+="Suggest plan mode. "
1616
-
1617
- output_approve_with_context "$BRAIN_MSG"
1618
- exit 0
1619
- fi
1620
-
1621
- # If plugins loaded but no routing, show just plugins (fallback)
1622
- if [[ -n "$AUTOLOAD_PLUGINS_MSG" ]]; then
1623
- output_approve_with_context "$AUTOLOAD_PLUGINS_MSG"
1624
- exit 0
1625
- fi
1626
- else
1627
- # JSON_OUTPUT was empty - LLM detection failed
1628
- LLM_DETECTION_FAILED=true
1629
- echo "[$(date -Iseconds)] LLM detection failed | reason=empty_json_output" >> "$LAZY_LOAD_LOG"
1630
- fi
1631
- else
1632
- # DETECT_OUTPUT was empty or jq not available
1633
- LLM_DETECTION_FAILED=true
1634
- echo "[$(date -Iseconds)] LLM detection failed | reason=empty_detect_output_or_no_jq" >> "$LAZY_LOAD_LOG"
1635
- fi
1636
-
1637
- # ==================================================================
1638
- # KEYWORD FALLBACK: PLUGIN INSTALLATION (v1.0.540 — note)
1639
- # ==================================================================
1640
- # On-demand plugin installation restored in v1.0.540 via the LLM
1641
- # detection path above. Keyword-based fallback is not needed here
1642
- # because the LLM detect-intent already identifies needed plugins.
1643
- # ==================================================================
1644
-
1645
- # ==================================================================
1646
- # KEYWORD FALLBACK FOR INCREMENT DISCIPLINE (v1.0.257)
1647
- # ==================================================================
1648
- # When LLM detection fails/times out, use keyword matching for
1649
- # increment suggestions.
1650
- if [[ "$LLM_DETECTION_FAILED" == "true" && "$INCREMENT_ASSIST_ENABLED" == "true" ]]; then
1651
- # Check for implementation-intent keywords
1652
- # v1.0.261: Expanded from 20 to 65+ keywords across 9 categories:
1653
- # Original, Investigation, Analysis, Problem-solving, Optimization,
1654
- # Security, Documentation, DevOps/Data, Structural
1655
- if echo "$PROMPT" | grep -qiE "(test|component|feature|fix|refactor|setup|configure|integrate|migrate|upgrade|write|style|design|add|create|implement|build|develop|deploy|scaffold|generate|investigate|debug|troubleshoot|diagnose|trace|profile|examine|inspect|reproduce|replicate|analyze|assess|audit|evaluate|benchmark|measure|validate|solve|resolve|address|tackle|determine|optimize|improve|reduce|minimize|eliminate|simplify|streamline|secure|harden|patch|sanitize|encrypt|document|provision|containerize|dockerize|seed|populate|import|export|transform|sync|batch|remove|delete|replace|convert|extract|merge|split|wrap|unwrap|decouple|modularize)"; then
1656
- # Exclude PURE questions but NOT investigation/work prompts
1657
- # v1.0.261: "why" and "how" removed — they almost always imply work intent
1658
- # ("why does X fail" = investigation, "how do I fix X" = work)
1659
- # Patterns made more specific to avoid false negatives
1660
- if ! echo "$PROMPT" | grep -qiE "^[[:space:]]*(what is|what are|explain|tell me about|can you explain|does .* support|should I use|is there a|where is|when did|which one)" && \
1661
- ! echo "$PROMPT" | grep -qE "\?[[:space:]]*$"; then
1662
- # v1.0.260: Removed prompt embedding to save context budget
1663
- if [[ "$INCREMENT_MANDATORY_CONFIG" == "true" ]]; then
1664
- FALLBACK_MSG="SKILL FIRST: \`Skill({ skill: \"sw:increment\" })\` — call BEFORE implementation.
1665
- Detection: Implementation keywords detected (LLM unavailable, keyword fallback).
1666
- After increment, chain domain skills per tech stack (see CLAUDE.md Skill Chaining)."
1667
- else
1668
- FALLBACK_MSG="Increment suggested: \`Skill({ skill: \"sw:increment\" })\`. Reason: Implementation keywords detected (LLM unavailable, keyword fallback)."
1669
- fi
1670
- # v1.0.542: Prepend vskill suggestions to fallback message
1671
- [[ -n "$VSKILL_SUGGEST_MSG" ]] && FALLBACK_MSG="${VSKILL_SUGGEST_MSG}
1672
- ${FALLBACK_MSG}"
1673
- echo "[$(date -Iseconds)] keyword-fallback | prompt_keywords_matched=true | mandatory=$INCREMENT_MANDATORY_CONFIG" >> "$LAZY_LOAD_LOG"
1674
- output_approve_with_context "$FALLBACK_MSG"
1675
- exit 0
1676
- fi
1677
- fi
1678
-
1679
- # ==================================================================
1680
- # ERROR-STATE DETECTION: symptom-based prompts (v1.0.261)
1681
- # ==================================================================
1682
- # Catches prompts describing failure states without action verbs:
1683
- # "the dashboard is broken", "login keeps failing", "app crashes on mobile"
1684
- # Only runs when LLM failed AND primary keyword regex didn't match.
1685
- if echo "$PROMPT" | grep -qiE "(is broken|keeps? failing|crash(es|ing)|hang(s|ing)|times? out|is slow|memory leak|performance issue|not working|throwing error|exception|stack trace|segfault|deadlock|race condition)"; then
1686
- # Require 3+ words for context (not just bare "is broken")
1687
- WORD_COUNT=$(echo "$PROMPT" | wc -w | tr -d ' ')
1688
- if [[ "$WORD_COUNT" -ge 3 ]]; then
1689
- if [[ "$INCREMENT_MANDATORY_CONFIG" == "true" ]]; then
1690
- FALLBACK_MSG="SKILL FIRST: \`Skill({ skill: \"sw:increment\" })\` — call BEFORE implementation.
1691
- Detection: Error/failure state detected (LLM unavailable, symptom fallback).
1692
- After increment, chain domain skills per tech stack (see CLAUDE.md Skill Chaining)."
1693
- else
1694
- FALLBACK_MSG="Increment suggested: \`Skill({ skill: \"sw:increment\" })\`. Reason: Error/failure state detected (LLM unavailable, symptom fallback)."
1695
- fi
1696
- echo "[$(date -Iseconds)] symptom-fallback | prompt_symptom_matched=true | mandatory=$INCREMENT_MANDATORY_CONFIG" >> "$LAZY_LOAD_LOG"
1697
- output_approve_with_context "$FALLBACK_MSG"
1698
- exit 0
1699
- fi
1700
- fi
1701
- fi
1702
- fi
1703
- fi
1704
- fi
1705
- fi
1706
-
1707
- # ==============================================================================
1708
- # TDD MODE CONTEXT INJECTION (v1.0.148) - Ensure Claude ALWAYS knows TDD status
1709
- # ==============================================================================
1710
- # Priority (highest to lowest):
1711
- # 1. Command flag: --tdd or --strict in prompt
1712
- # 2. Increment metadata: .specweave/increments/<active>/metadata.json
1713
- # 3. Global config: .specweave/config.json
1714
- #
1715
- # When TDD is enabled, injects context into systemMessage so Claude:
1716
- # - Always knows to follow RED→GREEN→REFACTOR discipline
1717
- # - Uses /sw:tdd-cycle for guided workflow
1718
- # - Blocks or warns on out-of-order task completion (based on enforcement)
1719
-
1720
- TDD_MODE="off"
1721
- TDD_ENFORCEMENT="warn"
1722
- TDD_SOURCE=""
1723
- TDD_MSG=""
1724
-
1725
- # Only check TDD if we're in a SpecWeave project (use resolved project root)
1726
- if [[ -n "$SW_PROJECT_ROOT" ]] && [[ -f "$SW_PROJECT_ROOT/.specweave/config.json" ]]; then
1727
-
1728
- # Step 1: Check global config (LOWEST priority)
1729
- if [[ -f "$SW_PROJECT_ROOT/.specweave/config.json" ]] && command -v jq >/dev/null 2>&1; then
1730
- GLOBAL_TDD=$(jq -r '.testing.defaultTestMode // "test-after"' "$SW_PROJECT_ROOT/.specweave/config.json" 2>/dev/null)
1731
- GLOBAL_ENFORCEMENT=$(jq -r '.testing.tddEnforcement // "warn"' "$SW_PROJECT_ROOT/.specweave/config.json" 2>/dev/null)
1732
- if [[ "$GLOBAL_TDD" == "TDD" || "$GLOBAL_TDD" == "tdd" ]]; then
1733
- TDD_MODE="TDD"
1734
- TDD_ENFORCEMENT="$GLOBAL_ENFORCEMENT"
1735
- TDD_SOURCE="global config"
1736
- fi
1737
- fi
1738
-
1739
- # Step 2: Check active increment metadata (MEDIUM priority - overrides global)
1740
- ACTIVE_INCREMENT=""
1741
- for meta in "$SW_PROJECT_ROOT/.specweave/increments/"/*/metadata.json; do
1742
- [[ -f "$meta" ]] || continue
1743
- if jq -e '.status == "in-progress" or .status == "active"' "$meta" >/dev/null 2>&1; then
1744
- ACTIVE_INCREMENT="$meta"
1745
- break
1746
- fi
1747
- done
1748
-
1749
- if [[ -n "$ACTIVE_INCREMENT" ]] && command -v jq >/dev/null 2>&1; then
1750
- INC_TDD=$(jq -r '.testMode // .tddMode // ""' "$ACTIVE_INCREMENT" 2>/dev/null)
1751
- INC_ENFORCEMENT=$(jq -r '.tddEnforcement // ""' "$ACTIVE_INCREMENT" 2>/dev/null)
1752
- if [[ "$INC_TDD" == "TDD" || "$INC_TDD" == "tdd" || "$INC_TDD" == "true" ]]; then
1753
- TDD_MODE="TDD"
1754
- [[ -n "$INC_ENFORCEMENT" ]] && TDD_ENFORCEMENT="$INC_ENFORCEMENT"
1755
- TDD_SOURCE="increment metadata"
1756
- elif [[ "$INC_TDD" == "test-after" || "$INC_TDD" == "false" ]]; then
1757
- # Increment explicitly disables TDD (overrides global)
1758
- TDD_MODE="off"
1759
- TDD_SOURCE="increment metadata (disabled)"
1760
- fi
1761
- fi
1762
-
1763
- # Step 3: Check command flags in prompt (HIGHEST priority)
1764
- if echo "$PROMPT" | grep -qE '\-\-tdd|\-\-strict'; then
1765
- TDD_MODE="TDD"
1766
- TDD_ENFORCEMENT="strict"
1767
- TDD_SOURCE="command flag"
1768
- elif echo "$PROMPT" | grep -qE '\-\-no-tdd'; then
1769
- TDD_MODE="off"
1770
- TDD_SOURCE="command flag (disabled)"
1771
- fi
1772
-
1773
- # Inject TDD context if enabled
1774
- if [[ "$TDD_MODE" == "TDD" ]]; then
1775
- ENFORCEMENT_DESC="warns but allows"
1776
- [[ "$TDD_ENFORCEMENT" == "strict" ]] && ENFORCEMENT_DESC="BLOCKS violations"
1777
-
1778
- # v1.0.160: STRICT TDD adds mandatory blocking directive
1779
- if [[ "$TDD_ENFORCEMENT" == "strict" ]]; then
1780
- TDD_MSG="STRICT TDD ACTIVE (source: ${TDD_SOURCE}). RED->GREEN->REFACTOR enforced. No implementation before failing test. Use /sw:tdd-cycle."
1781
- fi
1782
-
1783
- # v1.0.201: Include LSP instructions BEFORE TDD message
1784
- # This ensures LSP guidance is included in ALL early exits
1785
- if [[ -n "$LSP_EXPLICIT_REQUEST_MSG" ]]; then
1786
- if [[ -n "$AUTOLOAD_PLUGINS_MSG" ]]; then
1787
- AUTOLOAD_PLUGINS_MSG="${AUTOLOAD_PLUGINS_MSG}${LSP_EXPLICIT_REQUEST_MSG}"
1788
- else
1789
- AUTOLOAD_PLUGINS_MSG="$LSP_EXPLICIT_REQUEST_MSG"
1790
- fi
1791
- fi
1792
-
1793
- # Append TDD message
1794
- if [[ -n "$AUTOLOAD_PLUGINS_MSG" ]]; then
1795
- AUTOLOAD_PLUGINS_MSG="${AUTOLOAD_PLUGINS_MSG}${TDD_MSG}"
1796
- else
1797
- AUTOLOAD_PLUGINS_MSG="$TDD_MSG"
1798
- fi
1799
-
1800
- # Log TDD activation (use project root, never create dirs at $HOME)
1801
- if [[ -n "$SW_PROJECT_ROOT" ]]; then
1802
- TDD_LOG="$SW_PROJECT_ROOT/.specweave/logs/tdd-enforcement.log"
1803
- echo "[$(date -Iseconds)] TDD_MODE=$TDD_MODE | enforcement=$TDD_ENFORCEMENT | source=$TDD_SOURCE" >> "$TDD_LOG" 2>/dev/null
1804
- fi
1805
- fi
1806
- fi
1807
-
1808
- # CRITICAL: Exit immediately for non-SpecWeave prompts
1809
- # This covers 90%+ of prompts with <5ms overhead
1810
- # v1.0.144: Still show plugin autoload message if plugins are being loaded
1811
- # v1.0.155: AUTOLOAD_PLUGINS_MSG now includes new plugin warnings from helper function
1812
- # v1.0.257: Expanded keywords to catch implementation prompts that bypass LLM detection
1813
- if ! echo "$PROMPT" | grep -qiE "(specweave|/sw:|increment|add|create|implement|build|develop|test|component|feature|fix|refactor|write|style|setup|configure|migrate|deploy|scaffold)"; then
1814
- if [[ -n "$AUTOLOAD_PLUGINS_MSG" ]]; then
1815
- # Show plugin loading feedback even for non-SpecWeave prompts
1816
- output_approve_with_context "$AUTOLOAD_PLUGINS_MSG"
1817
- else
1818
- echo '{"decision":"approve"}'
1819
- fi
1820
- exit 0
1821
- fi
1822
-
1823
- # ==============================================================================
1824
- # EARLY EXIT FOR NON-SPECWEAVE PROJECTS (T-006 - v0.26.15)
1825
- # ==============================================================================
1826
- # Even if prompt contains SpecWeave keywords, exit if no .specweave directory
1827
- # v1.0.144: Still show plugin autoload message for non-SpecWeave projects
1828
- # Use SW_PROJECT_ROOT to avoid creating dirs relative to CWD
1829
- if [[ -n "$SW_PROJECT_ROOT" ]]; then
1830
- SPECWEAVE_DIR="$SW_PROJECT_ROOT/.specweave"
1831
- else
1832
- SPECWEAVE_DIR=".specweave"
1833
- fi
1834
- if [[ ! -d "$SPECWEAVE_DIR" ]]; then
1835
- if [[ -n "$AUTOLOAD_PLUGINS_MSG" ]]; then
1836
- # Show plugin loading feedback even for non-SpecWeave projects
1837
- output_approve_with_context "$AUTOLOAD_PLUGINS_MSG"
1838
- else
1839
- echo '{"decision":"approve"}'
1840
- fi
1841
- exit 0
1842
- fi
1843
-
1844
- # ==============================================================================
1845
- # INSTANT SCRIPT EXECUTION: Status commands bypass LLM entirely (v0.33.0)
1846
- # ==============================================================================
1847
- # These commands need NO LLM reasoning - execute scripts directly for <1s response
1848
- # Pattern: Detect command → Execute script → Return output via "block" → Exit
1849
-
1850
- PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
1851
- SCRIPTS_DIR="$PLUGIN_ROOT/scripts"
1852
-
1853
- # Helper: Escape output for JSON (handles newlines, quotes, backslashes)
1854
- # Uses jq for proper JSON string escaping (required dependency for instant commands)
1855
- escape_json() {
1856
- local input="$1"
1857
- # jq -Rs properly escapes all special characters including newlines
1858
- # We strip the surrounding quotes since we add them in the printf
1859
- printf '%s' "$input" | jq -Rs '.' | sed 's/^"//; s/"$//'
1860
- }
1861
-
1862
- # NOTE: output_approve_with_context() is defined earlier in the file (line ~105)
1863
- # It uses hookSpecificOutput.additionalContext (NOT systemMessage!)
1864
-
1865
- # Helper: Check if running in VSCode extension
1866
- # VSCode sets CLAUDE_CODE_ENTRYPOINT=claude-vscode
1867
- # Returns 0 (true) if VSCode, 1 (false) if CLI
1868
- is_vscode() {
1869
- [[ -n "${CLAUDE_CODE_ENTRYPOINT}" ]] && [[ "${CLAUDE_CODE_ENTRYPOINT}" == "claude-vscode" ]]
1870
- }
1871
-
1872
- # Helper: Extract command line and args from multi-line prompt (v1.0.105+)
1873
- # When prompts contain IDE metadata (e.g., <ide_opened_file>...</ide_opened_file>)
1874
- # the command may be on a subsequent line. This function:
1875
- # 1. Finds the line containing the command
1876
- # 2. Extracts args from that specific line
1877
- # Usage: extract_command_args "PROMPT" "command_pattern" (e.g., "/sw:progress")
1878
- # Returns args on stdout, or empty string if no args
1879
- extract_command_args() {
1880
- local prompt="$1"
1881
- local cmd_pattern="$2"
1882
-
1883
- # Find the line containing the command and extract args from it
1884
- # The grep -oE gets just the matching line, sed removes the command prefix
1885
- local cmd_line
1886
- cmd_line=$(echo "$prompt" | grep -E "^${cmd_pattern}($| )" | head -1)
1887
-
1888
- if [[ -n "$cmd_line" ]]; then
1889
- # Remove the command pattern from the line to get args
1890
- echo "$cmd_line" | sed "s|^${cmd_pattern}[[:space:]]*||"
1891
- fi
1892
- }
1893
-
1894
- # /sw:jobs → Execute read-jobs.sh (pure bash, ~2ms)
1895
- if echo "$PROMPT" | grep -qE "^/sw:jobs($| )"; then
1896
- ARGS=$(extract_command_args "$PROMPT" "/sw:jobs")
1897
-
1898
- # Execute command and get output
1899
- if [[ -f "$SCRIPTS_DIR/read-jobs.sh" ]]; then
1900
- OUTPUT=$(cd "$(pwd)" && bash "$SCRIPTS_DIR/read-jobs.sh" "$ARGS" 2>&1)
1901
- elif [[ -f "$SCRIPTS_DIR/jobs.js" ]] && command -v node >/dev/null 2>&1; then
1902
- OUTPUT=$(cd "$(pwd)" && node "$SCRIPTS_DIR/jobs.js" "$ARGS" 2>&1)
1903
- else
1904
- OUTPUT="❌ No jobs script available"
1905
- fi
1906
-
1907
- # Unified response for both CLI and VSCode (v1.0.166)
1908
- # Uses additionalContext (NOT systemMessage) to inject output into Claude's context
1909
- output_approve_with_context "$OUTPUT"
1910
- exit 0
1911
- fi
1912
-
1913
- # /sw:progress → Execute read-progress.sh (pure bash, ~30ms)
1914
- if echo "$PROMPT" | grep -qE "^/sw:progress($| )"; then
1915
- ARGS=$(extract_command_args "$PROMPT" "/sw:progress")
1916
-
1917
- # Execute command and get output
1918
- if [[ -f "$SCRIPTS_DIR/read-progress.sh" ]]; then
1919
- OUTPUT=$(cd "$(pwd)" && bash "$SCRIPTS_DIR/read-progress.sh" "$ARGS" 2>&1)
1920
- elif [[ -f "$SCRIPTS_DIR/progress.js" ]] && command -v node >/dev/null 2>&1; then
1921
- OUTPUT=$(cd "$(pwd)" && node "$SCRIPTS_DIR/progress.js" "$ARGS" 2>&1)
1922
- else
1923
- OUTPUT="❌ No progress script available"
1924
- fi
1925
-
1926
- # Unified response for both CLI and VSCode (v1.0.166)
1927
- # Uses additionalContext (NOT systemMessage) to inject output into Claude's context
1928
- output_approve_with_context "$OUTPUT"
1929
- exit 0
1930
- fi
1931
-
1932
- # /sw:grill → Load increment context for code review (pure bash, ~50ms)
1933
- # Unlike /sw:progress which displays data, this injects CONTEXT + INSTRUCTIONS
1934
- # so the LLM performs a structured code review with full increment awareness.
1935
- if echo "$PROMPT" | grep -qE "^/sw:grill($| )"; then
1936
- ARGS=$(extract_command_args "$PROMPT" "/sw:grill")
1937
-
1938
- if [[ -f "$SCRIPTS_DIR/read-grill-context.sh" ]]; then
1939
- OUTPUT=$(cd "$(pwd)" && bash "$SCRIPTS_DIR/read-grill-context.sh" $ARGS 2>&1)
1940
- else
1941
- OUTPUT="GRILL MODE ACTIVATED — Run a thorough code review of the active increment. Check correctness, security, performance, and maintainability. Categorize issues as BLOCKER, CRITICAL, MAJOR, MINOR, or SUGGESTION. End with VERDICT: PASS or FAIL."
1942
- fi
1943
-
1944
- output_approve_with_context "$OUTPUT"
1945
- exit 0
1946
- fi
1947
-
1948
- # /sw:status → Execute read-status.sh (pure bash, ~150ms)
1949
- if echo "$PROMPT" | grep -qE "^/sw:status($| )"; then
1950
- ARGS=$(extract_command_args "$PROMPT" "/sw:status")
1951
-
1952
- # Execute command and get output
1953
- if [[ -f "$SCRIPTS_DIR/read-status.sh" ]]; then
1954
- OUTPUT=$(cd "$(pwd)" && bash "$SCRIPTS_DIR/read-status.sh" "$ARGS" 2>&1)
1955
- elif [[ -f "$SCRIPTS_DIR/status.js" ]] && command -v node >/dev/null 2>&1; then
1956
- OUTPUT=$(cd "$(pwd)" && node "$SCRIPTS_DIR/status.js" "$ARGS" 2>&1)
1957
- else
1958
- OUTPUT="❌ No status script available"
1959
- fi
1960
-
1961
- # Unified response for both CLI and VSCode (v1.0.166)
1962
- # Uses additionalContext (NOT systemMessage) to inject output into Claude's context
1963
- output_approve_with_context "$OUTPUT"
1964
- exit 0
1965
- fi
1966
-
1967
- # /sw:workflow → Execute read-workflow.sh (pure bash, ~100ms)
1968
- if echo "$PROMPT" | grep -qE "^/sw:workflow($| )"; then
1969
- ARGS=$(extract_command_args "$PROMPT" "/sw:workflow")
1970
-
1971
- # Execute command and get output
1972
- if [[ -f "$SCRIPTS_DIR/read-workflow.sh" ]]; then
1973
- OUTPUT=$(cd "$(pwd)" && bash "$SCRIPTS_DIR/read-workflow.sh" "$ARGS" 2>&1)
1974
- else
1975
- OUTPUT="❌ No workflow script available"
1976
- fi
1977
-
1978
- # Unified response for both CLI and VSCode (v1.0.166)
1979
- # Uses additionalContext (NOT systemMessage) to inject output into Claude's context
1980
- output_approve_with_context "$OUTPUT"
1981
- exit 0
1982
- fi
1983
-
1984
- # /sw:costs → Execute read-costs.sh (pure bash, ~50ms)
1985
- if echo "$PROMPT" | grep -qE "^/sw:costs($| )"; then
1986
- ARGS=$(extract_command_args "$PROMPT" "/sw:costs")
1987
-
1988
- # Execute command and get output
1989
- if [[ -f "$SCRIPTS_DIR/read-costs.sh" ]]; then
1990
- OUTPUT=$(cd "$(pwd)" && bash "$SCRIPTS_DIR/read-costs.sh" "$ARGS" 2>&1)
1991
- else
1992
- OUTPUT="❌ No costs script available"
1993
- fi
1994
-
1995
- # Unified response for both CLI and VSCode (v1.0.166)
1996
- # Uses additionalContext (NOT systemMessage) to inject output into Claude's context
1997
- output_approve_with_context "$OUTPUT"
1998
- exit 0
1999
- fi
2000
-
2001
- # /sw:analytics → Execute read-analytics.sh (pure bash, ~50ms)
2002
- if echo "$PROMPT" | grep -qE "^/sw:analytics($| )"; then
2003
- ARGS=$(extract_command_args "$PROMPT" "/sw:analytics")
2004
-
2005
- # Execute command and get output
2006
- if [[ -f "$SCRIPTS_DIR/read-analytics.sh" ]]; then
2007
- OUTPUT=$(cd "$(pwd)" && bash "$SCRIPTS_DIR/read-analytics.sh" "$ARGS" 2>&1)
2008
- else
2009
- OUTPUT="❌ No analytics script available"
2010
- fi
2011
-
2012
- # Unified response for both CLI and VSCode (v1.0.166)
2013
- # Uses additionalContext (NOT systemMessage) to inject output into Claude's context
2014
- output_approve_with_context "$OUTPUT"
2015
- exit 0
2016
- fi
2017
-
2018
- # ==============================================================================
2019
- # TASK COUNT GUARD: Block /sw:do for oversized increments (v0.32.2+)
2020
- # ==============================================================================
2021
- # >8 tasks = context explosion = CRASH (per CLAUDE.md)
2022
- MAX_TASKS=8
2023
-
2024
- if echo "$PROMPT" | grep -qE "^/sw:do($| )"; then
2025
- # Extract increment ID from prompt
2026
- DO_INCREMENT_ID=$(echo "$PROMPT" | grep -oE "[0-9]{4}[a-zA-Z0-9-]*" | head -1)
2027
-
2028
- # If no ID provided, find active increment
2029
- if [[ -z "$DO_INCREMENT_ID" ]]; then
2030
- for meta in "$SPECWEAVE_DIR/increments"/*/metadata.json; do
2031
- [[ -f "$meta" ]] || continue
2032
- if command -v jq >/dev/null 2>&1; then
2033
- status=$(jq -r '.status // "unknown"' "$meta" 2>/dev/null)
2034
- else
2035
- status=$(sed -n 's/.*"status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$meta" 2>/dev/null || echo "unknown")
2036
- fi
2037
- if [[ "$status" == "active" || "$status" == "planning" || "$status" == "backlog" || "$status" == "ready_for_review" ]]; then
2038
- DO_INCREMENT_ID=$(basename "$(dirname "$meta")")
2039
- break
2040
- fi
2041
- done
2042
- fi
2043
-
2044
- if [[ -n "$DO_INCREMENT_ID" ]]; then
2045
- TASKS_FILE="$SPECWEAVE_DIR/increments/$DO_INCREMENT_ID/tasks.md"
2046
- if [[ -f "$TASKS_FILE" ]]; then
2047
- TASK_COUNT=$(grep -c "^### T-" "$TASKS_FILE" 2>/dev/null || echo "0")
2048
- if [[ "$TASK_COUNT" -gt "$MAX_TASKS" ]]; then
2049
- printf '{"decision":"block","reason":"❌ TASK COUNT EXCEEDS LIMIT\\n\\nIncrement %s has %s tasks (maximum: %s)\\n\\n>8 tasks = context explosion = CRASH\\n\\n💡 REQUIRED: Split into smaller increments:\\n\\n Pattern: %s/ → Split into:\\n • %s-part1/ (T-001 to T-004)\\n • Next increment (T-005 to T-008)\\n • Next increment (T-009+)\\n\\n⚠️ DO NOT PROCEED until tasks.md has ≤8 tasks!"}\n' "$DO_INCREMENT_ID" "$TASK_COUNT" "$MAX_TASKS" "$DO_INCREMENT_ID" "$DO_INCREMENT_ID"
2050
- exit 0
2051
- fi
2052
- fi
2053
- fi
2054
- fi
2055
-
2056
- # ==============================================================================
2057
- # CACHED ACTIVE INCREMENT DETECTION (ONCE - reused throughout!)
2058
- # ==============================================================================
2059
- ACTIVE_INCREMENT=""
2060
- ACTIVE_COUNT=0
2061
- ACTIVE_LIST=""
2062
-
2063
- if [[ -d "$SPECWEAVE_DIR/increments" ]]; then
2064
- # Single find + jq pass to get ALL active increment info
2065
- while IFS= read -r metadata_file; do
2066
- [[ -z "$metadata_file" ]] && continue
2067
-
2068
- # Use jq (fast) to extract status and id
2069
- if command -v jq >/dev/null 2>&1; then
2070
- read -r status inc_type < <(jq -r '"\(.status // "unknown") \(.type // "feature")"' "$metadata_file" 2>/dev/null || echo "unknown feature")
2071
- else
2072
- # Fallback: grep (no node!)
2073
- status=$(sed -n 's/.*"status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$metadata_file" 2>/dev/null || echo "unknown")
2074
- inc_type=$(sed -n 's/.*"type"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$metadata_file" 2>/dev/null || echo "feature")
2075
- fi
2076
-
2077
- if [[ "$status" == "active" || "$status" == "planning" || "$status" == "ready_for_review" ]]; then
2078
- inc_id=$(basename "$(dirname "$metadata_file")")
2079
- ACTIVE_COUNT=$((ACTIVE_COUNT + 1))
2080
- ACTIVE_LIST="${ACTIVE_LIST} - $inc_id [$inc_type]\n"
2081
- [[ -z "$ACTIVE_INCREMENT" ]] && ACTIVE_INCREMENT="$inc_id"
2082
- fi
2083
- done < <(find "$SPECWEAVE_DIR/increments" -maxdepth 2 -name "metadata.json" -not -path "*/_archive/*" 2>/dev/null)
2084
- fi
2085
-
2086
- # ==============================================================================
2087
- # WIP WARNING BUILDER (reusable across all increment suggestion paths)
2088
- # ==============================================================================
2089
- # Builds a soft warning string when active increments >= configured limit.
2090
- # Never blocks — just informs. Prepended to increment suggestion messages.
2091
- WIP_WARNING=""
2092
- if [[ "$ACTIVE_COUNT" -gt 0 ]]; then
2093
- _WIP_CONFIG="${SPECWEAVE_DIR}/config.json"
2094
- _WIP_LIMIT=3
2095
- if [[ -f "$_WIP_CONFIG" ]] && command -v jq >/dev/null 2>&1; then
2096
- _WIP_LIMIT=$(jq -r '.limits.maxActiveIncrements // 3' "$_WIP_CONFIG" 2>/dev/null || echo "3")
2097
- fi
2098
- [[ ! "$_WIP_LIMIT" =~ ^[0-9]+$ ]] && _WIP_LIMIT=3
2099
-
2100
- if [[ "$ACTIVE_COUNT" -ge "$_WIP_LIMIT" ]]; then
2101
- WIP_WARNING="⚠️ **WIP Notice** (${ACTIVE_COUNT}/${_WIP_LIMIT} active)\\n\\nActive increments:\\n${ACTIVE_LIST}\\nConsider completing existing work first (\`/sw:done <id>\`) or pausing (\`specweave pause <id>\`).\\n\\n---\\n\\n"
2102
- fi
2103
- fi
2104
-
2105
- # ==============================================================================
2106
- # ARCHIVE SUGGESTION (v1.0.257 - Auto-archive when too many increments)
2107
- # ==============================================================================
2108
- # When total numbered increment directories exceed threshold, suggest archiving.
2109
- # In auto mode: archive silently. Interactive: inject suggestion for LLM.
2110
- # Rate-limited: once per day via marker file.
2111
- ARCHIVE_SUGGESTION_MSG=""
2112
- if [[ -d "$SPECWEAVE_DIR/increments" ]]; then
2113
- TOTAL_INCREMENT_DIRS=$(ls -d "$SPECWEAVE_DIR/increments"/[0-9]* 2>/dev/null | wc -l | tr -d ' ')
2114
-
2115
- # Read threshold from config (default: 10)
2116
- _ARCHIVE_THRESHOLD=10
2117
- if [[ -f "$CONFIG_PATH" ]] && command -v jq >/dev/null 2>&1; then
2118
- _ARCHIVE_THRESHOLD=$(jq -r '.archiving.autoArchiveThreshold // 10' "$CONFIG_PATH" 2>/dev/null || echo "10")
2119
- fi
2120
- [[ ! "$_ARCHIVE_THRESHOLD" =~ ^[0-9]+$ ]] && _ARCHIVE_THRESHOLD=10
2121
-
2122
- if [[ "$TOTAL_INCREMENT_DIRS" -ge "$_ARCHIVE_THRESHOLD" ]]; then
2123
- # Rate limit: once per day via marker file
2124
- ARCHIVE_MARKER="$SPECWEAVE_DIR/state/archive-suggestion.marker"
2125
- SHOULD_SUGGEST=true
2126
- if [[ -f "$ARCHIVE_MARKER" ]]; then
2127
- MARKER_DATE=$(cat "$ARCHIVE_MARKER" 2>/dev/null | head -1)
2128
- TODAY=$(date +%Y-%m-%d)
2129
- [[ "$MARKER_DATE" == "$TODAY" ]] && SHOULD_SUGGEST=false
2130
- fi
2131
-
2132
- if [[ "$SHOULD_SUGGEST" == "true" ]]; then
2133
- # Check if in auto mode
2134
- AUTO_STATE_FILE="$SPECWEAVE_DIR/state/auto.json"
2135
- IN_AUTO_MODE=false
2136
- if [[ -f "$AUTO_STATE_FILE" ]] && command -v jq >/dev/null 2>&1; then
2137
- AUTO_STATUS=$(jq -r '.status // "idle"' "$AUTO_STATE_FILE" 2>/dev/null)
2138
- [[ "$AUTO_STATUS" == "running" ]] && IN_AUTO_MODE=true
2139
- fi
2140
-
2141
- if [[ "$IN_AUTO_MODE" == "true" ]]; then
2142
- # Auto mode: archive silently in background
2143
- if command -v specweave >/dev/null 2>&1; then
2144
- specweave archive --keep-last 10 2>/dev/null &
2145
- fi
2146
- else
2147
- # Interactive: suggest to LLM
2148
- ARCHIVE_SUGGESTION_MSG="⚠️ **Archive suggested**: ${TOTAL_INCREMENT_DIRS} increments in workspace (threshold: ${_ARCHIVE_THRESHOLD}). Run: \`specweave archive --keep-last 10\`\n\n"
2149
- fi
2150
-
2151
- # Write today's date as marker
2152
- mkdir -p "$(dirname "$ARCHIVE_MARKER")" 2>/dev/null
2153
- echo "$(date +%Y-%m-%d)" > "$ARCHIVE_MARKER" 2>/dev/null || true
2154
- fi
2155
- fi
2156
- fi
2157
-
2158
- # ==============================================================================
2159
- # SMART INTERVIEW GATE (v1.0.243 - LLM-Driven Prompt Assessment)
2160
- # ==============================================================================
2161
- # When Deep Interview Mode is enabled AND no active increment exists,
2162
- # inject instructions for the LLM to assess prompt completeness.
2163
- # The LLM decides: ask targeted questions OR proceed to increment creation.
2164
- # Fires on EVERY prompt until an increment is created.
2165
- # See ADR-0243 for architecture decision.
2166
-
2167
- SMART_INTERVIEW_GATE_MSG=""
2168
- if [[ "$DEEP_INTERVIEW_ENABLED" == "true" ]] && [[ -z "$ACTIVE_INCREMENT" ]]; then
2169
- # Also check active-increment.json state file as secondary source
2170
- HAVE_ACTIVE_STATE=false
2171
- STATE_FILE="$SPECWEAVE_DIR/state/active-increment.json"
2172
- if [[ -f "$STATE_FILE" ]] && command -v jq >/dev/null 2>&1; then
2173
- ACTIVE_IDS=$(jq -r '.ids // [] | length' "$STATE_FILE" 2>/dev/null || echo "0")
2174
- [[ "$ACTIVE_IDS" -gt 0 ]] && HAVE_ACTIVE_STATE=true
2175
- fi
2176
-
2177
- if [[ "$HAVE_ACTIVE_STATE" != "true" ]]; then
2178
- SMART_INTERVIEW_GATE_MSG="No active increment. Assess prompt completeness for complexity — if gaps, ask targeted questions (count depends on complexity). If sufficient, call sw:increment."
2179
- fi
2180
- fi
2181
-
2182
- # ==============================================================================
2183
- # DISCIPLINE VALIDATION: Warn about WIP limits (configurable, not hard block!)
2184
- # ==============================================================================
2185
-
2186
- # ==============================================================================
2187
- # PROJECT CONTEXT + WIP LIMITS FOR /sw:increment (v0.34.0)
2188
- # ==============================================================================
2189
- # CRITICAL: Inject project/board context BEFORE Claude generates spec.md
2190
- # This ensures Claude knows available projects and uses correct IDs
2191
- # ALSO: Check WIP limits in same block to avoid duplicate command detection
2192
-
2193
- if echo "$PROMPT" | grep -qE "^/sw:increment"; then
2194
- # Get project context (uses specweave CLI if available)
2195
- PROJECT_CONTEXT=""
2196
-
2197
- if command -v specweave >/dev/null 2>&1; then
2198
- # Use CLI for accurate project/board detection
2199
- CONTEXT_JSON=$(specweave context projects 2>/dev/null || echo '{}')
2200
-
2201
- # Validate JSON before parsing (defensive coding)
2202
- if [[ -n "$CONTEXT_JSON" ]] && [[ "$CONTEXT_JSON" != "{}" ]]; then
2203
- if command -v jq >/dev/null 2>&1; then
2204
- # Verify JSON is parseable before extracting fields
2205
- if ! echo "$CONTEXT_JSON" | jq empty 2>/dev/null; then
2206
- CONTEXT_JSON='{}' # Invalid JSON - reset to empty
2207
- fi
2208
- fi
2209
- fi
2210
-
2211
- if [[ -n "$CONTEXT_JSON" ]] && [[ "$CONTEXT_JSON" != "{}" ]]; then
2212
- # Parse JSON with jq
2213
- if command -v jq >/dev/null 2>&1; then
2214
- LEVEL=$(echo "$CONTEXT_JSON" | jq -r '.level // 1')
2215
- PROJECTS=$(echo "$CONTEXT_JSON" | jq -r '.projects | map(.id) | join(", ")' 2>/dev/null || echo "")
2216
-
2217
- if [[ "$LEVEL" == "2" ]]; then
2218
- # 2-level structure: include boards
2219
- BOARDS_JSON=$(echo "$CONTEXT_JSON" | jq -r '.boardsByProject // {}' 2>/dev/null)
2220
- if [[ -n "$BOARDS_JSON" ]] && [[ "$BOARDS_JSON" != "{}" ]]; then
2221
- PROJECT_CONTEXT="\\n\\n📦 PROJECT CONTEXT (2-LEVEL STRUCTURE)\\n\\n"
2222
- PROJECT_CONTEXT="${PROJECT_CONTEXT}⚠️ MANDATORY: Each User Story MUST have both:\\n"
2223
- PROJECT_CONTEXT="${PROJECT_CONTEXT} - **Project**: <project_id>\\n"
2224
- PROJECT_CONTEXT="${PROJECT_CONTEXT} - **Board**: <board_id>\\n\\n"
2225
- PROJECT_CONTEXT="${PROJECT_CONTEXT}Available projects: ${PROJECTS}\\n"
2226
- PROJECT_CONTEXT="${PROJECT_CONTEXT}Boards by project:\\n"
2227
-
2228
- # Format boards
2229
- for proj in $(echo "$CONTEXT_JSON" | jq -r '.projects[].id' 2>/dev/null); do
2230
- PROJ_BOARDS=$(echo "$CONTEXT_JSON" | jq -r ".boardsByProject[\"$proj\"] | map(.id) | join(\", \")" 2>/dev/null || echo "")
2231
- [[ -n "$PROJ_BOARDS" ]] && PROJECT_CONTEXT="${PROJECT_CONTEXT} - ${proj}: ${PROJ_BOARDS}\\n"
2232
- done
2233
-
2234
- PROJECT_CONTEXT="${PROJECT_CONTEXT}\\n❌ FORBIDDEN: Comma-separated values (e.g., **Project**: fe, be)\\n"
2235
- PROJECT_CONTEXT="${PROJECT_CONTEXT}✅ REQUIRED: One project + one board per User Story"
2236
- fi
2237
- elif [[ -n "$PROJECTS" ]]; then
2238
- # 1-level structure: projects only
2239
- PROJECT_COUNT=$(echo "$CONTEXT_JSON" | jq '.projects // [] | length' 2>/dev/null || echo "0")
2240
-
2241
- if [[ "$PROJECT_COUNT" -gt 1 ]]; then
2242
- PROJECT_CONTEXT="\\n\\n📦 PROJECT CONTEXT (MULTI-PROJECT)\\n\\n"
2243
- PROJECT_CONTEXT="${PROJECT_CONTEXT}⚠️ MANDATORY: Each User Story MUST have:\\n"
2244
- PROJECT_CONTEXT="${PROJECT_CONTEXT} - **Project**: <project_id>\\n\\n"
2245
- PROJECT_CONTEXT="${PROJECT_CONTEXT}Available projects: ${PROJECTS}\\n"
2246
- PROJECT_CONTEXT="${PROJECT_CONTEXT}\\n❌ FORBIDDEN: Comma-separated values\\n"
2247
- PROJECT_CONTEXT="${PROJECT_CONTEXT}✅ REQUIRED: One project per User Story"
2248
- elif [[ "$PROJECT_COUNT" -eq 1 ]]; then
2249
- # Single project: auto-select
2250
- SINGLE_PROJECT=$(echo "$CONTEXT_JSON" | jq -r '.projects[0].id' 2>/dev/null)
2251
- PROJECT_CONTEXT="\\n\\n📦 PROJECT CONTEXT\\n"
2252
- PROJECT_CONTEXT="${PROJECT_CONTEXT}Single project detected: ${SINGLE_PROJECT} (auto-selected)"
2253
- fi
2254
- fi
2255
- fi
2256
- fi
2257
- else
2258
- # Fallback: Check for multi-project folders
2259
- if [[ -d "$SPECWEAVE_DIR/docs/internal/specs" ]]; then
2260
- PROJ_COUNT=$(find "$SPECWEAVE_DIR/docs/internal/specs" -maxdepth 1 -type d | wc -l)
2261
- if [[ "$PROJ_COUNT" -gt 2 ]]; then
2262
- PROJ_LIST=$(ls -1 "$SPECWEAVE_DIR/docs/internal/specs" 2>/dev/null | grep -v "_" | tr '\n' ', ' | sed 's/,$//')
2263
- PROJECT_CONTEXT="\\n\\n📦 PROJECT CONTEXT (MULTI-PROJECT)\\n"
2264
- PROJECT_CONTEXT="${PROJECT_CONTEXT}⚠️ MANDATORY: Each User Story MUST have **Project**: field\\n"
2265
- PROJECT_CONTEXT="${PROJECT_CONTEXT}Available folders: ${PROJ_LIST}"
2266
- fi
2267
- fi
2268
- fi
2269
-
2270
- # WIP + PROJECT CONTEXT: Combine WIP_WARNING (built earlier) with project context
2271
- # Never blocks — always approve with informational context
2272
- COMBINED_CONTEXT="${WIP_WARNING}${PROJECT_CONTEXT}"
2273
- if [[ -n "$COMBINED_CONTEXT" ]]; then
2274
- output_approve_with_context "$COMBINED_CONTEXT"
2275
- exit 0
2276
- fi
2277
- fi
2278
-
2279
- # ==============================================================================
2280
- # PRE-FLIGHT SYNC CHECK (LIGHTWEIGHT - uses cached ACTIVE_INCREMENT)
2281
- # ==============================================================================
2282
-
2283
- # Detect increment operations that need fresh data
2284
- if echo "$PROMPT" | grep -qE "/(specweave:)?(done|validate|progress|do)"; then
2285
- # Extract increment ID from prompt OR use cached active
2286
- INCREMENT_ID=$(echo "$PROMPT" | grep -oE "[0-9]{4}[a-z0-9-]*" | head -1)
2287
- [[ -z "$INCREMENT_ID" ]] && INCREMENT_ID="$ACTIVE_INCREMENT"
2288
-
2289
- # If we have an increment ID, check freshness (pure bash - no node!)
2290
- if [[ -n "$INCREMENT_ID" ]]; then
2291
- INCREMENT_SPEC="$SPECWEAVE_DIR/increments/$INCREMENT_ID/spec.md"
2292
- LIVING_DOCS_SPEC="$SPECWEAVE_DIR/docs/internal/specs/spec-$INCREMENT_ID.md"
2293
-
2294
- if [[ -f "$INCREMENT_SPEC" ]]; then
2295
- # Use find -newer for mtime comparison (single syscall!)
2296
- if [[ ! -f "$LIVING_DOCS_SPEC" ]] || [[ -n $(find "$INCREMENT_SPEC" -newer "$LIVING_DOCS_SPEC" 2>/dev/null) ]]; then
2297
- # Sync needed - run async (non-blocking!)
2298
- PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
2299
- SYNC_SCRIPT="$PLUGIN_ROOT/lib/hooks/sync-living-docs.js"
2300
- [[ -f "$SYNC_SCRIPT" ]] && node "$SYNC_SCRIPT" "$INCREMENT_ID" >/dev/null 2>&1 &
2301
- fi
2302
- fi
2303
- fi
2304
- fi
2305
-
2306
- # ==============================================================================
2307
- # SPEC SYNC CHECK (LIGHTWEIGHT - only when really needed)
2308
- # ==============================================================================
2309
- # Skip SpecSyncManager for most prompts - it's HEAVY!
2310
- # Only check on explicit sync-related commands
2311
-
2312
- if [[ -n "$ACTIVE_INCREMENT" ]] && echo "$PROMPT" | grep -qE "/(specweave:)?(sync|done)"; then
2313
- # Simple mtime check: spec.md vs plan.md (pure bash!)
2314
- SPEC_FILE="$SPECWEAVE_DIR/increments/$ACTIVE_INCREMENT/spec.md"
2315
- PLAN_FILE="$SPECWEAVE_DIR/increments/$ACTIVE_INCREMENT/plan.md"
2316
-
2317
- if [[ -f "$SPEC_FILE" ]] && [[ -f "$PLAN_FILE" ]]; then
2318
- # Check if spec is newer than plan (indicates spec changes need sync)
2319
- if [[ -n $(find "$SPEC_FILE" -newer "$PLAN_FILE" 2>/dev/null) ]]; then
2320
- output_approve_with_context "⚠️ Spec changes detected in ${ACTIVE_INCREMENT}\n\nspec.md has been modified after plan.md.\nConsider running /sw:sync-docs to update living documentation."
2321
- exit 0
2322
- fi
2323
- fi
2324
- fi
2325
-
2326
- # ==============================================================================
2327
- # CONTEXT INJECTION (uses cached ACTIVE_INCREMENT - no more find loops!)
2328
- # ==============================================================================
2329
-
2330
- CONTEXT=""
2331
-
2332
- if [[ -n "$ACTIVE_INCREMENT" ]]; then
2333
- # Read from status-line.json cache (single source of truth)
2334
- CACHE_FILE="$SPECWEAVE_DIR/state/status-line.json"
2335
-
2336
- if [[ -f "$CACHE_FILE" ]]; then
2337
- # Single jq call for all values (or pure bash fallback)
2338
- if command -v jq >/dev/null 2>&1; then
2339
- read -r TOTAL_TASKS COMPLETED_TASKS TOTAL_ACS COMPLETED_ACS < <(
2340
- jq -r '[.current.total // 0, .current.completed // 0, .current.acsTotal // 0, .current.acsCompleted // 0] | @tsv' "$CACHE_FILE" 2>/dev/null || echo "0 0 0 0"
2341
- )
2342
- else
2343
- # Pure grep fallback (no node!)
2344
- TOTAL_TASKS=$(sed -n 's/.*"total"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$CACHE_FILE" 2>/dev/null | head -1 || echo "0")
2345
- COMPLETED_TASKS=$(sed -n 's/.*"completed"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$CACHE_FILE" 2>/dev/null | head -1 || echo "0")
2346
- TOTAL_ACS=$(sed -n 's/.*"acsTotal"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$CACHE_FILE" 2>/dev/null || echo "0")
2347
- COMPLETED_ACS=$(sed -n 's/.*"acsCompleted"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$CACHE_FILE" 2>/dev/null || echo "0")
2348
- fi
2349
-
2350
- # Ensure valid numbers
2351
- TOTAL_TASKS=${TOTAL_TASKS:-0}
2352
- COMPLETED_TASKS=${COMPLETED_TASKS:-0}
2353
- TOTAL_ACS=${TOTAL_ACS:-0}
2354
- COMPLETED_ACS=${COMPLETED_ACS:-0}
2355
-
2356
- if [[ "$TOTAL_TASKS" -gt 0 ]] 2>/dev/null; then
2357
- PERCENTAGE=$(( COMPLETED_TASKS * 100 / TOTAL_TASKS ))
2358
-
2359
- if [[ "$TOTAL_ACS" -gt 0 ]] 2>/dev/null; then
2360
- AC_PERCENTAGE=$(( COMPLETED_ACS * 100 / TOTAL_ACS ))
2361
- CONTEXT="✓ Active: $ACTIVE_INCREMENT ($COMPLETED_TASKS/$TOTAL_TASKS tasks, $PERCENTAGE% | $COMPLETED_ACS/$TOTAL_ACS ACs, $AC_PERCENTAGE%)"
2362
- else
2363
- CONTEXT="✓ Active: $ACTIVE_INCREMENT ($COMPLETED_TASKS/$TOTAL_TASKS tasks, $PERCENTAGE%)"
2364
- fi
2365
- else
2366
- CONTEXT="✓ Active: $ACTIVE_INCREMENT"
2367
- fi
2368
- else
2369
- CONTEXT="✓ Active: $ACTIVE_INCREMENT"
2370
- fi
2371
- fi
2372
-
2373
- # ==============================================================================
2374
- # COMMAND SUGGESTIONS: Guide users to structured workflow
2375
- # ==============================================================================
2376
-
2377
- # Command suggestions removed (v1.0.257) — already in CLAUDE.md, reduces per-turn context
2378
-
2379
- # ==============================================================================
2380
- # STATUS LINE REFRESH (v0.26.13 - CONDITIONAL + ASYNC)
2381
- # ==============================================================================
2382
- # Only refresh when we have an active increment (skip for most prompts)
2383
- # Runs in background to avoid blocking user prompt
2384
-
2385
- if [[ -n "$ACTIVE_INCREMENT" ]] && [[ -d "$SPECWEAVE_DIR" ]]; then
2386
- HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
2387
- # Run async (non-blocking!) - update-status-line.sh has its own TTL/mtime guards
2388
- bash "$HOOK_DIR/lib/update-status-line.sh" 2>/dev/null &
2389
- fi
2390
-
2391
- # ==============================================================================
2392
- # ANALYTICS TRACKING: Track /sw:* command invocations (v0.35.0+)
2393
- # ==============================================================================
2394
- # Non-blocking, runs in background. Tracks command usage for /sw:analytics.
2395
-
2396
- if echo "$PROMPT" | grep -qE "^/sw:[a-z]"; then
2397
- COMMAND_NAME=$(echo "$PROMPT" | grep -oE "^/sw:[a-z0-9:-]+" | head -1)
2398
- if [[ -n "$COMMAND_NAME" ]] && [[ -f "$SCRIPTS_DIR/track-analytics.sh" ]]; then
2399
- bash "$SCRIPTS_DIR/track-analytics.sh" command "$COMMAND_NAME" --plugin specweave --increment "$ACTIVE_INCREMENT" 2>/dev/null &
2400
- fi
2401
- fi
2402
-
2403
- # ==============================================================================
2404
- # OUTPUT: Priority-based context assembly with budget (v1.0.260)
2405
- # ==============================================================================
2406
- # Assembles final message by adding context items in priority order,
2407
- # stopping when the budget is exhausted. This prevents blind concatenation
2408
- # that wastes budget on low-priority items while truncating critical ones.
2409
- #
2410
- # Priority tiers:
2411
- # P1 (critical): Plugin status (RESTART/Using), active increment status
2412
- # P2 (important): LSP explicit request, WIP/interview gate
2413
- # P3 (informational): LSP setup/install suggestions, archive suggestion
2414
-
2415
- # ==============================================================================
2416
- # CONTEXT BUDGET RESOLUTION (v1.0.262)
2417
- # ==============================================================================
2418
- # 1. Read base level from config.json (contextBudget.level)
2419
- # 2. If autoAdapt=true, check context-pressure.json and step down
2420
- # 3. Map level to char budget
2421
- # 4. Environment override: SPECWEAVE_CONTEXT_BUDGET
2422
-
2423
- BUDGET_LEVEL="full"
2424
- AUTO_ADAPT=true
2425
- if [[ -f "$CONFIG_PATH" ]] && command -v jq >/dev/null 2>&1; then
2426
- BUDGET_LEVEL=$(jq -r '.contextBudget.level // "full"' "$CONFIG_PATH" 2>/dev/null || echo "full")
2427
- AUTO_ADAPT_VAL=$(jq -r '.contextBudget.autoAdapt // true' "$CONFIG_PATH" 2>/dev/null || echo "true")
2428
- [[ "$AUTO_ADAPT_VAL" == "false" ]] && AUTO_ADAPT=false
2429
- fi
2430
-
2431
- # Environment override (for quick testing without config changes)
2432
- [[ -n "${SPECWEAVE_CONTEXT_BUDGET:-}" ]] && BUDGET_LEVEL="$SPECWEAVE_CONTEXT_BUDGET"
2433
-
2434
- # Auto-adapt: check pressure state from PreCompact hook
2435
- if [[ "$AUTO_ADAPT" == "true" ]] && [[ -n "$SW_PROJECT_ROOT" ]]; then
2436
- PRESSURE_FILE="$SW_PROJECT_ROOT/.specweave/state/context-pressure.json"
2437
- if [[ -f "$PRESSURE_FILE" ]] && command -v jq >/dev/null 2>&1; then
2438
- PRESSURE_LEVEL=$(jq -r '.level // "normal"' "$PRESSURE_FILE" 2>/dev/null || echo "normal")
2439
- case "$PRESSURE_LEVEL" in
2440
- elevated)
2441
- # Step down one level
2442
- case "$BUDGET_LEVEL" in
2443
- full) BUDGET_LEVEL="compact" ;;
2444
- compact) BUDGET_LEVEL="minimal" ;;
2445
- esac
2446
- ;;
2447
- critical)
2448
- # Jump to minimal regardless of base
2449
- [[ "$BUDGET_LEVEL" != "off" ]] && BUDGET_LEVEL="minimal"
2450
- ;;
2451
- emergency)
2452
- # Nuclear option: strip ALL context (v1.0.268 - 3+ compactions)
2453
- BUDGET_LEVEL="off"
2454
- ;;
2455
- esac
2456
- fi
2457
- fi
2458
-
2459
- # Map level to char budget
2460
- case "$BUDGET_LEVEL" in
2461
- full) CONTEXT_BUDGET=2500 ;;
2462
- compact) CONTEXT_BUDGET=1000 ;;
2463
- minimal) CONTEXT_BUDGET=300 ;;
2464
- off) CONTEXT_BUDGET=0 ;;
2465
- *) CONTEXT_BUDGET=2500 ;;
2466
- esac
2467
-
2468
- # If budget is 0, send remediation message if pressure-triggered, then exit
2469
- if [[ "$CONTEXT_BUDGET" -eq 0 ]]; then
2470
- if [[ "${PRESSURE_LEVEL:-}" == "emergency" || "${PRESSURE_LEVEL:-}" == "critical" ]]; then
2471
- output_approve_with_context "Context budget auto-reduced due to prompt pressure. Consider starting a fresh session."
2472
- else
2473
- echo '{"decision":"approve"}'
2474
- fi
2475
- exit 0
2476
- fi
2477
-
2478
- # Helper: Append message to FINAL_MESSAGE if it fits within budget
2479
- # Args: $1=message to append
2480
- # Returns: 0 if appended, 1 if skipped (budget exceeded)
2481
- _budget_append() {
2482
- local msg="$1"
2483
- [[ -z "$msg" ]] && return 0
2484
- local new_len=$(( ${#FINAL_MESSAGE} + ${#msg} ))
2485
- if [[ $new_len -le $CONTEXT_BUDGET ]]; then
2486
- FINAL_MESSAGE="${FINAL_MESSAGE}${msg}"
2487
- return 0
2488
- fi
2489
- return 1
2490
- }
2491
-
2492
- FINAL_MESSAGE=""
2493
-
2494
- # P1: Critical — plugin status and active increment
2495
- _budget_append "$AUTOLOAD_PLUGINS_MSG"
2496
- _budget_append "$CONTEXT"
2497
-
2498
- # P2: Important — LSP explicit request, interview gate
2499
- _budget_append "$LSP_EXPLICIT_REQUEST_MSG"
2500
- if [[ -n "$SMART_INTERVIEW_GATE_MSG" ]]; then
2501
- _budget_append "\\n${SMART_INTERVIEW_GATE_MSG}"
2502
- fi
2503
-
2504
- # P3: Informational — LSP setup, archive, environment
2505
- _budget_append "$LSP_ENV_SETUP_MSG"
2506
- _budget_append "$LSP_INSTALL_MSG"
2507
- _budget_append "$LSP_SETUP_SUGGESTION_MSG"
2508
- _budget_append "$ARCHIVE_SUGGESTION_MSG"
2509
-
2510
- # ==============================================================================
2511
- # TURN DEDUPLICATION: Skip identical context to save tokens (v1.0.262)
2512
- # ==============================================================================
2513
- # If this turn's context is identical to last turn's, don't re-inject it.
2514
- # Claude already has it in history. Saves ~2500 chars per duplicate turn.
2515
- if [[ -n "$FINAL_MESSAGE" ]] && [[ -n "$SW_PROJECT_ROOT" ]]; then
2516
- DEDUP_HASH_FILE="$SW_PROJECT_ROOT/.specweave/state/.context-hash"
2517
- CURRENT_HASH=""
2518
- # SMART_INTERVIEW_GATE_MSG is excluded from the dedup hash so the gate
2519
- # fires on every turn even when the rest of the context is identical.
2520
- HASH_INPUT="$FINAL_MESSAGE"
2521
- if [[ -n "$SMART_INTERVIEW_GATE_MSG" ]]; then
2522
- HASH_INPUT="${HASH_INPUT//$SMART_INTERVIEW_GATE_MSG/}"
2523
- fi
2524
- if command -v md5sum >/dev/null 2>&1; then
2525
- CURRENT_HASH=$(printf '%s' "$HASH_INPUT" | md5sum | cut -d' ' -f1)
2526
- elif command -v md5 >/dev/null 2>&1; then
2527
- CURRENT_HASH=$(printf '%s' "$HASH_INPUT" | md5)
2528
- fi
2529
-
2530
- if [[ -n "$CURRENT_HASH" ]]; then
2531
- if [[ -f "$DEDUP_HASH_FILE" ]]; then
2532
- PREV_HASH=$(cat "$DEDUP_HASH_FILE" 2>/dev/null)
2533
- if [[ "$CURRENT_HASH" == "$PREV_HASH" ]]; then
2534
- # Identical to last turn — skip injection
2535
- echo '{"decision":"approve"}'
2536
- exit 0
2537
- fi
2538
- fi
2539
- # Save hash for next turn
2540
- echo "$CURRENT_HASH" > "$DEDUP_HASH_FILE" 2>/dev/null
2541
- fi
2542
- fi
2543
-
2544
- if [[ -n "$FINAL_MESSAGE" ]]; then
2545
- output_approve_with_context "$FINAL_MESSAGE"
2546
- else
2547
- echo '{"decision":"approve"}'
2548
- fi
2549
-
2550
- exit 0