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.
- package/CLAUDE.md +1 -1
- package/bin/specweave.js +23 -1
- package/dist/src/cli/commands/hook.d.ts +15 -0
- package/dist/src/cli/commands/hook.d.ts.map +1 -0
- package/dist/src/cli/commands/hook.js +61 -0
- package/dist/src/cli/commands/hook.js.map +1 -0
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +5 -0
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/refresh-plugins.d.ts.map +1 -1
- package/dist/src/cli/commands/refresh-plugins.js +11 -1
- package/dist/src/cli/commands/refresh-plugins.js.map +1 -1
- package/dist/src/cli/commands/sync-setup.d.ts.map +1 -1
- package/dist/src/cli/commands/sync-setup.js +7 -3
- package/dist/src/cli/commands/sync-setup.js.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/project-mapping-wizard.d.ts +9 -0
- package/dist/src/cli/helpers/issue-tracker/project-mapping-wizard.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/project-mapping-wizard.js +9 -3
- package/dist/src/cli/helpers/issue-tracker/project-mapping-wizard.js.map +1 -1
- package/dist/src/config/types.d.ts +2 -2
- package/dist/src/core/config/types.d.ts +18 -2
- package/dist/src/core/config/types.d.ts.map +1 -1
- package/dist/src/core/config/types.js.map +1 -1
- package/dist/src/core/hooks/handlers/hook-router.d.ts +19 -0
- package/dist/src/core/hooks/handlers/hook-router.d.ts.map +1 -0
- package/dist/src/core/hooks/handlers/hook-router.js +75 -0
- package/dist/src/core/hooks/handlers/hook-router.js.map +1 -0
- package/dist/src/core/hooks/handlers/index.d.ts +10 -0
- package/dist/src/core/hooks/handlers/index.d.ts.map +1 -0
- package/dist/src/core/hooks/handlers/index.js +9 -0
- package/dist/src/core/hooks/handlers/index.js.map +1 -0
- package/dist/src/core/hooks/handlers/post-tool-use-analytics.d.ts +11 -0
- package/dist/src/core/hooks/handlers/post-tool-use-analytics.d.ts.map +1 -0
- package/dist/src/core/hooks/handlers/post-tool-use-analytics.js +73 -0
- package/dist/src/core/hooks/handlers/post-tool-use-analytics.js.map +1 -0
- package/dist/src/core/hooks/handlers/post-tool-use.d.ts +11 -0
- package/dist/src/core/hooks/handlers/post-tool-use.d.ts.map +1 -0
- package/dist/src/core/hooks/handlers/post-tool-use.js +76 -0
- package/dist/src/core/hooks/handlers/post-tool-use.js.map +1 -0
- package/dist/src/core/hooks/handlers/pre-compact.d.ts +11 -0
- package/dist/src/core/hooks/handlers/pre-compact.d.ts.map +1 -0
- package/dist/src/core/hooks/handlers/pre-compact.js +77 -0
- package/dist/src/core/hooks/handlers/pre-compact.js.map +1 -0
- package/dist/src/core/hooks/handlers/pre-tool-use.d.ts +11 -0
- package/dist/src/core/hooks/handlers/pre-tool-use.d.ts.map +1 -0
- package/dist/src/core/hooks/handlers/pre-tool-use.js +318 -0
- package/dist/src/core/hooks/handlers/pre-tool-use.js.map +1 -0
- package/dist/src/core/hooks/handlers/session-start.d.ts +9 -0
- package/dist/src/core/hooks/handlers/session-start.d.ts.map +1 -0
- package/dist/src/core/hooks/handlers/session-start.js +111 -0
- package/dist/src/core/hooks/handlers/session-start.js.map +1 -0
- package/dist/src/core/hooks/handlers/stop-auto.d.ts +16 -0
- package/dist/src/core/hooks/handlers/stop-auto.d.ts.map +1 -0
- package/dist/src/core/hooks/handlers/stop-auto.js +122 -0
- package/dist/src/core/hooks/handlers/stop-auto.js.map +1 -0
- package/dist/src/core/hooks/handlers/stop-reflect.d.ts +14 -0
- package/dist/src/core/hooks/handlers/stop-reflect.d.ts.map +1 -0
- package/dist/src/core/hooks/handlers/stop-reflect.js +43 -0
- package/dist/src/core/hooks/handlers/stop-reflect.js.map +1 -0
- package/dist/src/core/hooks/handlers/stop-sync.d.ts +15 -0
- package/dist/src/core/hooks/handlers/stop-sync.d.ts.map +1 -0
- package/dist/src/core/hooks/handlers/stop-sync.js +68 -0
- package/dist/src/core/hooks/handlers/stop-sync.js.map +1 -0
- package/dist/src/core/hooks/handlers/types.d.ts +63 -0
- package/dist/src/core/hooks/handlers/types.d.ts.map +1 -0
- package/dist/src/core/hooks/handlers/types.js +27 -0
- package/dist/src/core/hooks/handlers/types.js.map +1 -0
- package/dist/src/core/hooks/handlers/user-prompt-submit.d.ts +14 -0
- package/dist/src/core/hooks/handlers/user-prompt-submit.d.ts.map +1 -0
- package/dist/src/core/hooks/handlers/user-prompt-submit.js +173 -0
- package/dist/src/core/hooks/handlers/user-prompt-submit.js.map +1 -0
- package/dist/src/core/hooks/handlers/utils.d.ts +25 -0
- package/dist/src/core/hooks/handlers/utils.d.ts.map +1 -0
- package/dist/src/core/hooks/handlers/utils.js +64 -0
- package/dist/src/core/hooks/handlers/utils.js.map +1 -0
- package/dist/src/core/increment/completion-validator.d.ts.map +1 -1
- package/dist/src/core/increment/completion-validator.js +32 -0
- package/dist/src/core/increment/completion-validator.js.map +1 -1
- package/dist/src/init/research/types.d.ts +1 -1
- package/dist/src/sync/sync-target-resolver.js.map +1 -1
- package/dist/src/utils/lock-manager.d.ts.map +1 -1
- package/dist/src/utils/lock-manager.js +5 -0
- package/dist/src/utils/lock-manager.js.map +1 -1
- package/dist/src/utils/plugin-copier.d.ts +10 -0
- package/dist/src/utils/plugin-copier.d.ts.map +1 -1
- package/dist/src/utils/plugin-copier.js +63 -35
- package/dist/src/utils/plugin-copier.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/agents/sw-closer.md +3 -2
- package/plugins/specweave/hooks/hooks.json +10 -10
- package/plugins/specweave/skills/code-reviewer/SKILL.md +180 -16
- package/plugins/specweave/skills/code-reviewer/agents/reviewer-comments.md +83 -0
- package/plugins/specweave/skills/code-reviewer/agents/reviewer-silent-failures.md +19 -0
- package/plugins/specweave/skills/code-reviewer/agents/reviewer-spec-compliance.md +19 -0
- package/plugins/specweave/skills/code-reviewer/agents/reviewer-tests.md +101 -0
- package/plugins/specweave/skills/code-reviewer/agents/reviewer-types.md +20 -0
- package/plugins/specweave/skills/done/SKILL.md +56 -21
- package/plugins/specweave/skills/grill/SKILL.md +1 -1
- package/plugins/specweave/skills/team-lead/agents/reviewer-logic.md +19 -0
- package/plugins/specweave/skills/team-lead/agents/reviewer-performance.md +20 -0
- package/plugins/specweave/skills/team-lead/agents/reviewer-security.md +20 -0
- package/src/templates/CLAUDE.md.template +7 -4
- package/plugins/specweave/hooks/README.md +0 -493
- package/plugins/specweave/hooks/_archive/stop-auto-v4-legacy.sh +0 -1319
- package/plugins/specweave/hooks/lib/common-setup.sh +0 -144
- package/plugins/specweave/hooks/lib/hook-errors.sh +0 -414
- package/plugins/specweave/hooks/lib/migrate-increment-work.sh +0 -245
- package/plugins/specweave/hooks/lib/resolve-package.sh +0 -146
- package/plugins/specweave/hooks/lib/scheduler-startup.sh +0 -135
- package/plugins/specweave/hooks/lib/score-increment.sh +0 -87
- package/plugins/specweave/hooks/lib/sync-spec-content.sh +0 -193
- package/plugins/specweave/hooks/lib/update-active-increment.sh +0 -95
- package/plugins/specweave/hooks/lib/update-status-line.sh +0 -233
- package/plugins/specweave/hooks/lib/validate-spec-status.sh +0 -171
- package/plugins/specweave/hooks/llm-judge-validator.sh +0 -219
- package/plugins/specweave/hooks/log-decision.sh +0 -168
- package/plugins/specweave/hooks/pre-compact.sh +0 -64
- package/plugins/specweave/hooks/startup-health-check.sh +0 -64
- package/plugins/specweave/hooks/stop-auto-v5.sh +0 -276
- package/plugins/specweave/hooks/stop-reflect.sh +0 -336
- package/plugins/specweave/hooks/stop-sync.sh +0 -283
- package/plugins/specweave/hooks/tests/test-auto-context-integration.sh +0 -126
- package/plugins/specweave/hooks/tests/test-stop-auto-enriched.sh +0 -128
- package/plugins/specweave/hooks/universal/dispatcher.mjs +0 -336
- package/plugins/specweave/hooks/universal/fail-fast-wrapper.sh +0 -325
- package/plugins/specweave/hooks/universal/hook-wrapper.cmd +0 -26
- package/plugins/specweave/hooks/universal/hook-wrapper.sh +0 -69
- package/plugins/specweave/hooks/universal/run-hook.sh +0 -20
- package/plugins/specweave/hooks/universal/session-start.cmd +0 -16
- package/plugins/specweave/hooks/universal/session-start.ps1 +0 -16
- package/plugins/specweave/hooks/user-prompt-submit.sh +0 -2550
- package/plugins/specweave/hooks/v2/detectors/lifecycle-detector.sh +0 -87
- package/plugins/specweave/hooks/v2/detectors/us-completion-detector.sh +0 -186
- package/plugins/specweave/hooks/v2/dispatchers/post-tool-use-analytics.sh +0 -83
- package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +0 -447
- package/plugins/specweave/hooks/v2/dispatchers/pre-tool-use.sh +0 -104
- package/plugins/specweave/hooks/v2/dispatchers/session-start.sh +0 -270
- package/plugins/specweave/hooks/v2/guards/completion-guard.sh +0 -14
- package/plugins/specweave/hooks/v2/guards/increment-duplicate-guard.sh +0 -14
- package/plugins/specweave/hooks/v2/guards/increment-existence-guard.sh +0 -240
- package/plugins/specweave/hooks/v2/guards/interview-enforcement-guard.sh +0 -171
- package/plugins/specweave/hooks/v2/guards/metadata-json-guard.sh +0 -14
- package/plugins/specweave/hooks/v2/guards/skill-chain-enforcement-guard.sh +0 -222
- package/plugins/specweave/hooks/v2/guards/spec-template-enforcement-guard.sh +0 -21
- package/plugins/specweave/hooks/v2/guards/spec-validation-guard.sh +0 -14
- package/plugins/specweave/hooks/v2/guards/status-completion-guard.sh +0 -84
- package/plugins/specweave/hooks/v2/guards/task-ac-sync-guard.sh +0 -475
- package/plugins/specweave/hooks/v2/guards/tdd-enforcement-guard.sh +0 -268
- package/plugins/specweave/hooks/v2/handlers/ac-sync-dispatcher.sh +0 -332
- package/plugins/specweave/hooks/v2/handlers/ac-validation-handler.sh +0 -50
- package/plugins/specweave/hooks/v2/handlers/github-sync-handler.sh +0 -347
- package/plugins/specweave/hooks/v2/handlers/living-docs-handler.sh +0 -83
- package/plugins/specweave/hooks/v2/handlers/living-specs-handler.sh +0 -268
- package/plugins/specweave/hooks/v2/handlers/project-bridge-handler.sh +0 -104
- package/plugins/specweave/hooks/v2/handlers/status-line-handler.sh +0 -165
- package/plugins/specweave/hooks/v2/handlers/status-update.sh +0 -61
- package/plugins/specweave/hooks/v2/handlers/universal-auto-create-dispatcher.sh +0 -270
- package/plugins/specweave/hooks/v2/integrations/ado-post-living-docs-update.sh +0 -367
- package/plugins/specweave/hooks/v2/integrations/ado-post-task.sh +0 -179
- package/plugins/specweave/hooks/v2/integrations/github-auto-create-handler.sh +0 -553
- package/plugins/specweave/hooks/v2/integrations/github-post-task.sh +0 -345
- package/plugins/specweave/hooks/v2/integrations/jira-post-task.sh +0 -180
- package/plugins/specweave/hooks/v2/lib/check-provider-enabled.sh +0 -52
- package/plugins/specweave/hooks/v2/queue/enqueue.sh +0 -81
- package/plugins/specweave/hooks/v2/session-end.sh +0 -139
- 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
|