specweave 1.0.96 → 1.0.97
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/package.json +1 -1
- package/plugins/specweave/scripts/reflect.sh +295 -330
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specweave",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.97",
|
|
4
4
|
"description": "Spec-driven development framework for Claude Code. AI-native workflow with living documentation, intelligent agents, and multilingual support (9 languages). Enterprise-grade traceability with permanent specs and temporary increments.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# reflect.sh - Self-Improving Skills Reflection System (
|
|
2
|
+
# reflect.sh - Self-Improving Skills Reflection System (v3.0)
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# PHILOSOPHY: Only store HIGH-VALUE learnings that contain actionable rules.
|
|
5
|
+
# Most sessions produce NO learnings - that's correct behavior.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
# -
|
|
9
|
-
# -
|
|
10
|
-
# -
|
|
11
|
-
# - Compact learning format (no verbose metadata)
|
|
7
|
+
# What we capture:
|
|
8
|
+
# - Direct corrections with both WRONG and RIGHT: "No, don't X. Do Y instead."
|
|
9
|
+
# - Explicit rules: "Always use X" / "Never use Y" / "In this project, use X"
|
|
10
|
+
# - Project-specific patterns that differ from defaults
|
|
12
11
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
12
|
+
# What we SKIP:
|
|
13
|
+
# - Generic praise ("Perfect!", "Great!")
|
|
14
|
+
# - Vague approval ("That's right") - no actionable info
|
|
15
|
+
# - Things already in CLAUDE.md
|
|
15
16
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
17
|
+
# FORMAT: Test-like rules with optional example
|
|
18
|
+
# RULE: When [context], [always|never] [action]
|
|
19
|
+
# EXAMPLE: [one concrete example if helpful]
|
|
20
|
+
#
|
|
21
|
+
# LIMITS:
|
|
22
|
+
# - 30 rules max per category (quality over quantity)
|
|
23
|
+
# - ~100 chars per rule (compact)
|
|
24
|
+
# - Deduplication by keyword similarity
|
|
20
25
|
|
|
21
|
-
set +e
|
|
26
|
+
set +e
|
|
22
27
|
|
|
23
28
|
# ============================================================================
|
|
24
29
|
# CONFIGURATION
|
|
@@ -30,187 +35,218 @@ REFLECT_CONFIG="$STATE_DIR/reflect-config.json"
|
|
|
30
35
|
LOGS_DIR="$PROJECT_ROOT/.specweave/logs/reflect"
|
|
31
36
|
MEMORY_DIR="$PROJECT_ROOT/.specweave/memory"
|
|
32
37
|
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
MAX_LOG_LINES=
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# Defaults
|
|
39
|
-
DEFAULT_CONFIDENCE="medium"
|
|
40
|
-
DEFAULT_MAX_LEARNINGS=10
|
|
38
|
+
# Conservative limits - quality over quantity
|
|
39
|
+
MAX_RULES_PER_CATEGORY=30
|
|
40
|
+
MAX_LOG_LINES=50
|
|
41
|
+
DEFAULT_CONFIDENCE="high" # Only high-confidence by default
|
|
42
|
+
DEFAULT_MAX_LEARNINGS=5 # Max 5 per session (most sessions = 0)
|
|
41
43
|
|
|
42
44
|
# ============================================================================
|
|
43
|
-
#
|
|
45
|
+
# LOGGING (with rotation)
|
|
44
46
|
# ============================================================================
|
|
45
47
|
|
|
46
48
|
log() {
|
|
47
|
-
local level="$1"
|
|
48
|
-
shift
|
|
49
|
-
local timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
49
|
+
local level="$1"; shift
|
|
50
50
|
mkdir -p "$LOGS_DIR"
|
|
51
|
-
|
|
52
|
-
# Log rotation - keep only last MAX_LOG_LINES
|
|
53
51
|
local log_file="$LOGS_DIR/reflect.log"
|
|
52
|
+
|
|
53
|
+
# Rotate if needed
|
|
54
54
|
if [ -f "$log_file" ]; then
|
|
55
|
-
local
|
|
56
|
-
|
|
57
|
-
tail -n $((MAX_LOG_LINES / 2)) "$log_file" > "$log_file.tmp" && mv "$log_file.tmp" "$log_file"
|
|
58
|
-
fi
|
|
55
|
+
local lines=$(wc -l < "$log_file" 2>/dev/null || echo 0)
|
|
56
|
+
[ "$lines" -gt "$MAX_LOG_LINES" ] && tail -n 25 "$log_file" > "$log_file.tmp" && mv "$log_file.tmp" "$log_file"
|
|
59
57
|
fi
|
|
60
58
|
|
|
61
|
-
echo "
|
|
59
|
+
echo "[$(date +%H:%M:%S)] $level: $*" >> "$log_file"
|
|
62
60
|
}
|
|
63
61
|
|
|
64
|
-
ensure_dirs() {
|
|
65
|
-
|
|
62
|
+
ensure_dirs() { mkdir -p "$LOGS_DIR" "$MEMORY_DIR" "$STATE_DIR"; }
|
|
63
|
+
|
|
64
|
+
get_config() {
|
|
65
|
+
local key="$1" default="$2"
|
|
66
|
+
[ -f "$REFLECT_CONFIG" ] && jq -r ".$key // \"$default\"" "$REFLECT_CONFIG" 2>/dev/null || echo "$default"
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
# ============================================================================
|
|
70
|
+
# HIGH-VALUE SIGNAL DETECTION
|
|
71
|
+
# ============================================================================
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
# Patterns that indicate a CORRECTION with actionable info
|
|
74
|
+
# Must have both "don't do X" AND "do Y instead" to be valuable
|
|
75
|
+
CORRECTION_PATTERNS=(
|
|
76
|
+
"No,? don.t.*instead"
|
|
77
|
+
"No,? use.*not"
|
|
78
|
+
"Wrong.*should be"
|
|
79
|
+
"Never.*always"
|
|
80
|
+
"Don.t.*use.*instead"
|
|
81
|
+
"That.s incorrect.*correct"
|
|
82
|
+
"Actually,?.*should"
|
|
83
|
+
"Stop using.*use"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Patterns for explicit rules (no correction needed, just a rule)
|
|
87
|
+
RULE_PATTERNS=(
|
|
88
|
+
"Always use"
|
|
89
|
+
"Never use"
|
|
90
|
+
"In this (project|codebase|repo)"
|
|
91
|
+
"The convention (here|is)"
|
|
92
|
+
"We always"
|
|
93
|
+
"We never"
|
|
94
|
+
"The rule is"
|
|
95
|
+
"Remember to always"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Patterns to SKIP (generic praise with no actionable info)
|
|
99
|
+
SKIP_PATTERNS=(
|
|
100
|
+
"^Perfect!?$"
|
|
101
|
+
"^Great!?$"
|
|
102
|
+
"^Exactly!?$"
|
|
103
|
+
"^That.s right\.?$"
|
|
104
|
+
"^Well done\.?$"
|
|
105
|
+
"^Good job\.?$"
|
|
106
|
+
"^Correct\.?$"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Check if context contains actionable info (not just praise)
|
|
110
|
+
is_actionable() {
|
|
111
|
+
local text="$1"
|
|
112
|
+
|
|
113
|
+
# Skip if it's just generic praise
|
|
114
|
+
for pattern in "${SKIP_PATTERNS[@]}"; do
|
|
115
|
+
if echo "$text" | grep -qiE "$pattern"; then
|
|
116
|
+
return 1
|
|
117
|
+
fi
|
|
118
|
+
done
|
|
79
119
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
120
|
+
# Must be longer than 20 chars to have real content
|
|
121
|
+
[ ${#text} -lt 20 ] && return 1
|
|
122
|
+
|
|
123
|
+
# Must contain some actionable verb
|
|
124
|
+
echo "$text" | grep -qiE "(use|don.t|never|always|should|must|avoid|prefer)" || return 1
|
|
84
125
|
|
|
85
|
-
|
|
86
|
-
local auto=$(get_config_value "autoReflect" "false")
|
|
87
|
-
[ "$auto" = "true" ]
|
|
126
|
+
return 0
|
|
88
127
|
}
|
|
89
128
|
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
|
|
129
|
+
# Extract the actual rule from a correction context
|
|
130
|
+
extract_rule() {
|
|
131
|
+
local context="$1"
|
|
93
132
|
|
|
94
|
-
#
|
|
95
|
-
|
|
133
|
+
# Try to find the "do Y instead" part
|
|
134
|
+
local rule=""
|
|
96
135
|
|
|
97
|
-
#
|
|
98
|
-
|
|
136
|
+
# Pattern: "No, don't X. Use Y instead" -> "Use Y"
|
|
137
|
+
rule=$(echo "$context" | grep -oiE "(use|prefer|always)[^.!?]*" | head -1)
|
|
138
|
+
[ -n "$rule" ] && echo "$rule" && return
|
|
99
139
|
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
|
|
140
|
+
# Pattern: "Should be X" -> "Should be X"
|
|
141
|
+
rule=$(echo "$context" | grep -oiE "should (be|use)[^.!?]*" | head -1)
|
|
142
|
+
[ -n "$rule" ] && echo "$rule" && return
|
|
103
143
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
fi
|
|
144
|
+
# Pattern: "Never X, always Y" -> "Always Y"
|
|
145
|
+
rule=$(echo "$context" | grep -oiE "always[^.!?]*" | head -1)
|
|
146
|
+
[ -n "$rule" ] && echo "$rule" && return
|
|
108
147
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
# Find corrections with context (using process substitution to avoid subshell)
|
|
113
|
-
local matches
|
|
114
|
-
matches=$(grep -inE "$CORRECTION_REGEX" "$transcript" 2>/dev/null | head -5) || true
|
|
115
|
-
if [ -n "$matches" ]; then
|
|
116
|
-
echo "$matches" | while read -r match; do
|
|
117
|
-
local linenum=$(echo "$match" | cut -d: -f1)
|
|
118
|
-
local start=$((linenum > 3 ? linenum - 2 : 1))
|
|
119
|
-
local end=$((linenum + 2))
|
|
120
|
-
local context=$(sed -n "${start},${end}p" "$transcript" 2>/dev/null | tr '\n' ' ' | cut -c1-300)
|
|
121
|
-
echo "CORRECTION|$context"
|
|
122
|
-
done >> "$signals_file"
|
|
123
|
-
fi
|
|
148
|
+
# Fallback: return cleaned context
|
|
149
|
+
echo "$context" | cut -c1-100
|
|
150
|
+
}
|
|
124
151
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
local linenum=$(echo "$match" | cut -d: -f1)
|
|
130
|
-
local start=$((linenum > 3 ? linenum - 2 : 1))
|
|
131
|
-
local end=$((linenum + 2))
|
|
132
|
-
local context=$(sed -n "${start},${end}p" "$transcript" 2>/dev/null | tr '\n' ' ' | cut -c1-300)
|
|
133
|
-
echo "APPROVAL|$context"
|
|
134
|
-
done >> "$signals_file"
|
|
135
|
-
fi
|
|
152
|
+
# Detect high-value signals in transcript
|
|
153
|
+
detect_signals() {
|
|
154
|
+
local transcript="$1"
|
|
155
|
+
local signals_file="$STATE_DIR/reflect-signals.txt"
|
|
136
156
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
157
|
+
[ ! -f "$transcript" ] && return 1
|
|
158
|
+
> "$signals_file"
|
|
159
|
+
|
|
160
|
+
# Look for corrections (high value)
|
|
161
|
+
for pattern in "${CORRECTION_PATTERNS[@]}"; do
|
|
162
|
+
local matches=$(grep -inE "$pattern" "$transcript" 2>/dev/null | head -3) || true
|
|
163
|
+
if [ -n "$matches" ]; then
|
|
164
|
+
echo "$matches" | while read -r match; do
|
|
165
|
+
local linenum=$(echo "$match" | cut -d: -f1)
|
|
166
|
+
local start=$((linenum > 2 ? linenum - 1 : 1))
|
|
167
|
+
local end=$((linenum + 1))
|
|
168
|
+
local context=$(sed -n "${start},${end}p" "$transcript" 2>/dev/null | tr '\n' ' ' | sed 's/ */ /g')
|
|
169
|
+
|
|
170
|
+
# Only add if actionable
|
|
171
|
+
if is_actionable "$context"; then
|
|
172
|
+
local rule=$(extract_rule "$context")
|
|
173
|
+
echo "CORRECTION|$rule" >> "$signals_file"
|
|
174
|
+
fi
|
|
175
|
+
done
|
|
176
|
+
fi
|
|
177
|
+
done
|
|
178
|
+
|
|
179
|
+
# Look for explicit rules (medium value)
|
|
180
|
+
for pattern in "${RULE_PATTERNS[@]}"; do
|
|
181
|
+
local matches=$(grep -inE "$pattern" "$transcript" 2>/dev/null | head -2) || true
|
|
182
|
+
if [ -n "$matches" ]; then
|
|
183
|
+
echo "$matches" | while read -r match; do
|
|
184
|
+
local linenum=$(echo "$match" | cut -d: -f1)
|
|
185
|
+
local context=$(sed -n "${linenum}p" "$transcript" 2>/dev/null | tr '\n' ' ')
|
|
186
|
+
|
|
187
|
+
if is_actionable "$context"; then
|
|
188
|
+
local rule=$(extract_rule "$context")
|
|
189
|
+
echo "RULE|$rule" >> "$signals_file"
|
|
190
|
+
fi
|
|
191
|
+
done
|
|
192
|
+
fi
|
|
193
|
+
done
|
|
141
194
|
|
|
142
|
-
|
|
195
|
+
local count=$(wc -l < "$signals_file" 2>/dev/null || echo 0)
|
|
196
|
+
count=$(echo "$count" | tr -d ' ')
|
|
143
197
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return 1
|
|
147
|
-
fi
|
|
198
|
+
log "info" "Found $count actionable signals"
|
|
199
|
+
[ "$count" -eq 0 ] && return 1
|
|
148
200
|
|
|
149
201
|
echo "$signals_file"
|
|
150
202
|
}
|
|
151
203
|
|
|
152
204
|
# ============================================================================
|
|
153
|
-
#
|
|
205
|
+
# DEDUPLICATION
|
|
154
206
|
# ============================================================================
|
|
155
207
|
|
|
156
|
-
#
|
|
157
|
-
|
|
158
|
-
echo "L$(date +%m%d)-$(head -c 2 /dev/urandom | od -An -tx1 | tr -d ' ')"
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
# Categorize based on keywords
|
|
162
|
-
categorize_learning() {
|
|
163
|
-
local context="$1"
|
|
164
|
-
local lower=$(echo "$context" | tr '[:upper:]' '[:lower:]')
|
|
165
|
-
|
|
166
|
-
case "$lower" in
|
|
167
|
-
*button*|*component*|*ui*|*style*|*css*|*react*|*vue*) echo "frontend" ;;
|
|
168
|
-
*api*|*endpoint*|*route*|*rest*|*graphql*|*backend*) echo "backend" ;;
|
|
169
|
-
*test*|*spec*|*mock*|*playwright*|*vitest*|*jest*) echo "testing" ;;
|
|
170
|
-
*deploy*|*docker*|*k8s*|*ci*|*cd*|*terraform*) echo "devops" ;;
|
|
171
|
-
*auth*|*security*|*token*|*password*|*encrypt*) echo "security" ;;
|
|
172
|
-
*query*|*database*|*sql*|*schema*|*postgres*) echo "database" ;;
|
|
173
|
-
*) echo "general" ;;
|
|
174
|
-
esac
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
# Prune memory file to max learnings
|
|
178
|
-
prune_memory_file() {
|
|
208
|
+
# Check if a similar rule already exists (by key words)
|
|
209
|
+
rule_exists() {
|
|
179
210
|
local file="$1"
|
|
211
|
+
local new_rule="$2"
|
|
180
212
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
213
|
+
[ ! -f "$file" ] && return 1
|
|
214
|
+
|
|
215
|
+
# Extract key words from new rule (nouns, verbs)
|
|
216
|
+
local keywords=$(echo "$new_rule" | tr '[:upper:]' '[:lower:]' | grep -oE '\b[a-z]{4,}\b' | sort -u | head -5 | tr '\n' ' ')
|
|
184
217
|
|
|
185
|
-
#
|
|
186
|
-
local
|
|
218
|
+
# Check if 3+ keywords match any existing rule
|
|
219
|
+
local match_count=0
|
|
220
|
+
for kw in $keywords; do
|
|
221
|
+
if grep -qi "\b$kw\b" "$file" 2>/dev/null; then
|
|
222
|
+
match_count=$((match_count + 1))
|
|
223
|
+
fi
|
|
224
|
+
done
|
|
187
225
|
|
|
188
|
-
|
|
189
|
-
|
|
226
|
+
[ "$match_count" -ge 3 ] && return 0
|
|
227
|
+
return 1
|
|
228
|
+
}
|
|
190
229
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
230
|
+
# ============================================================================
|
|
231
|
+
# MEMORY MANAGEMENT
|
|
232
|
+
# ============================================================================
|
|
194
233
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
fi
|
|
234
|
+
categorize() {
|
|
235
|
+
local text="$1"
|
|
236
|
+
local lower=$(echo "$text" | tr '[:upper:]' '[:lower:]')
|
|
199
237
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
echo "
|
|
208
|
-
echo ""
|
|
209
|
-
|
|
210
|
-
fi
|
|
238
|
+
case "$lower" in
|
|
239
|
+
*component*|*button*|*ui*|*style*|*css*|*react*|*vue*|*html*) echo "frontend" ;;
|
|
240
|
+
*api*|*endpoint*|*route*|*rest*|*graphql*|*server*) echo "backend" ;;
|
|
241
|
+
*test*|*spec*|*mock*|*assert*|*expect*) echo "testing" ;;
|
|
242
|
+
*deploy*|*docker*|*k8s*|*ci*|*terraform*|*aws*) echo "devops" ;;
|
|
243
|
+
*auth*|*security*|*token*|*password*|*secret*) echo "security" ;;
|
|
244
|
+
*query*|*database*|*sql*|*schema*|*table*) echo "database" ;;
|
|
245
|
+
*file*|*path*|*import*|*export*|*module*) echo "structure" ;;
|
|
246
|
+
*) echo "general" ;;
|
|
247
|
+
esac
|
|
211
248
|
}
|
|
212
249
|
|
|
213
|
-
# Ensure memory file exists with header
|
|
214
250
|
ensure_memory_file() {
|
|
215
251
|
local category="$1"
|
|
216
252
|
local file="$MEMORY_DIR/${category}.md"
|
|
@@ -218,227 +254,181 @@ ensure_memory_file() {
|
|
|
218
254
|
mkdir -p "$MEMORY_DIR"
|
|
219
255
|
|
|
220
256
|
if [ ! -f "$file" ]; then
|
|
221
|
-
|
|
222
|
-
local cap_category=$(echo "$category" | awk '{print toupper(substr($0,1,1)) substr($0,2)}')
|
|
257
|
+
local cap=$(echo "$category" | awk '{print toupper(substr($0,1,1)) substr($0,2)}')
|
|
223
258
|
cat > "$file" << EOF
|
|
224
|
-
# $
|
|
225
|
-
>
|
|
226
|
-
>
|
|
259
|
+
# $cap Rules
|
|
260
|
+
> Project-specific patterns learned from corrections.
|
|
261
|
+
> Max $MAX_RULES_PER_CATEGORY rules, auto-deduplicated.
|
|
227
262
|
|
|
228
|
-
## Learnings
|
|
229
263
|
EOF
|
|
230
264
|
fi
|
|
231
265
|
|
|
232
266
|
echo "$file"
|
|
233
267
|
}
|
|
234
268
|
|
|
235
|
-
|
|
236
|
-
add_learning() {
|
|
269
|
+
add_rule() {
|
|
237
270
|
local category="$1"
|
|
238
|
-
local
|
|
239
|
-
local
|
|
271
|
+
local type="$2" # CORRECTION or RULE
|
|
272
|
+
local rule="$3"
|
|
240
273
|
|
|
241
274
|
local file=$(ensure_memory_file "$category")
|
|
242
|
-
local id=$(generate_learning_id)
|
|
243
275
|
|
|
244
|
-
#
|
|
245
|
-
|
|
246
|
-
|
|
276
|
+
# Skip if similar rule exists
|
|
277
|
+
if rule_exists "$file" "$rule"; then
|
|
278
|
+
log "info" "Skipped duplicate: $rule"
|
|
279
|
+
return 1
|
|
280
|
+
fi
|
|
247
281
|
|
|
248
|
-
|
|
282
|
+
# Clean and format
|
|
283
|
+
local clean=$(echo "$rule" | tr -d '\n\r' | sed 's/ */ /g' | cut -c1-100)
|
|
284
|
+
local marker="→"
|
|
285
|
+
[ "$type" = "CORRECTION" ] && marker="✗→✓"
|
|
249
286
|
|
|
250
|
-
|
|
251
|
-
sed -i.bak "s/^> Updated:.*/> Updated: $(date +%Y-%m-%d)/" "$file" 2>/dev/null
|
|
252
|
-
rm -f "${file}.bak"
|
|
287
|
+
echo "- $marker $clean" >> "$file"
|
|
253
288
|
|
|
254
|
-
# Prune if
|
|
255
|
-
|
|
289
|
+
# Prune if over limit (keep newest, they're at bottom)
|
|
290
|
+
local count=$(grep -c "^- " "$file" 2>/dev/null || echo 0)
|
|
291
|
+
if [ "$count" -gt "$MAX_RULES_PER_CATEGORY" ]; then
|
|
292
|
+
local header=$(head -4 "$file")
|
|
293
|
+
local rules=$(grep "^- " "$file" | tail -n "$MAX_RULES_PER_CATEGORY")
|
|
294
|
+
echo "$header" > "$file"
|
|
295
|
+
echo "$rules" >> "$file"
|
|
296
|
+
log "info" "Pruned $category to $MAX_RULES_PER_CATEGORY rules"
|
|
297
|
+
fi
|
|
256
298
|
|
|
257
|
-
log "info" "Added
|
|
299
|
+
log "info" "Added to $category: $clean"
|
|
300
|
+
return 0
|
|
258
301
|
}
|
|
259
302
|
|
|
260
303
|
# ============================================================================
|
|
261
|
-
# MAIN REFLECTION
|
|
304
|
+
# MAIN REFLECTION
|
|
262
305
|
# ============================================================================
|
|
263
306
|
|
|
264
307
|
reflect_session() {
|
|
265
308
|
local transcript="$1"
|
|
266
309
|
local dry_run="${2:-false}"
|
|
267
|
-
local
|
|
268
|
-
local max_learnings="${4:-$DEFAULT_MAX_LEARNINGS}"
|
|
310
|
+
local max="${3:-$DEFAULT_MAX_LEARNINGS}"
|
|
269
311
|
|
|
270
312
|
ensure_dirs
|
|
271
313
|
|
|
272
|
-
# Detect signals
|
|
273
314
|
local signals_file=$(detect_signals "$transcript")
|
|
274
315
|
|
|
275
316
|
if [ -z "$signals_file" ] || [ ! -f "$signals_file" ]; then
|
|
276
|
-
echo "No signals
|
|
277
|
-
return
|
|
317
|
+
echo "No actionable signals found (this is normal)."
|
|
318
|
+
return 0
|
|
278
319
|
fi
|
|
279
320
|
|
|
280
|
-
local total=$(wc -l < "$signals_file" 2>/dev/null || echo
|
|
321
|
+
local total=$(wc -l < "$signals_file" 2>/dev/null || echo 0)
|
|
322
|
+
total=$(echo "$total" | tr -d ' ')
|
|
281
323
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
fi
|
|
324
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
325
|
+
echo "🧠 REFLECT: $total actionable signals"
|
|
326
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
286
327
|
|
|
287
|
-
|
|
288
|
-
echo "🧠 REFLECT: Found $total signals"
|
|
289
|
-
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
328
|
+
local added=0
|
|
290
329
|
|
|
291
|
-
|
|
330
|
+
while IFS='|' read -r type rule; do
|
|
331
|
+
[ "$added" -ge "$max" ] && break
|
|
332
|
+
[ -z "$rule" ] && continue
|
|
292
333
|
|
|
293
|
-
|
|
294
|
-
while IFS='|' read -r type context; do
|
|
295
|
-
if [ "$learnings_created" -ge "$max_learnings" ]; then
|
|
296
|
-
break
|
|
297
|
-
fi
|
|
298
|
-
|
|
299
|
-
local category=$(categorize_learning "$context")
|
|
300
|
-
local confidence="medium"
|
|
301
|
-
[ "$type" = "CORRECTION" ] && confidence="high"
|
|
302
|
-
|
|
303
|
-
# Skip low confidence if threshold is high
|
|
304
|
-
if [ "$confidence_threshold" = "high" ] && [ "$confidence" != "high" ]; then
|
|
305
|
-
continue
|
|
306
|
-
fi
|
|
334
|
+
local category=$(categorize "$rule")
|
|
307
335
|
|
|
308
|
-
echo "
|
|
336
|
+
echo " [$category] $type"
|
|
309
337
|
|
|
310
338
|
if [ "$dry_run" = "false" ]; then
|
|
311
|
-
|
|
339
|
+
if add_rule "$category" "$type" "$rule"; then
|
|
340
|
+
added=$((added + 1))
|
|
341
|
+
fi
|
|
342
|
+
else
|
|
343
|
+
added=$((added + 1))
|
|
312
344
|
fi
|
|
313
|
-
|
|
314
|
-
learnings_created=$((learnings_created + 1))
|
|
315
345
|
done < "$signals_file"
|
|
316
346
|
|
|
317
|
-
echo "
|
|
347
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
318
348
|
|
|
319
349
|
if [ "$dry_run" = "true" ]; then
|
|
320
|
-
echo "🔍 DRY RUN -
|
|
350
|
+
echo "🔍 DRY RUN - would add $added rules"
|
|
321
351
|
else
|
|
322
|
-
echo "✅
|
|
352
|
+
echo "✅ Added $added rules"
|
|
323
353
|
fi
|
|
324
354
|
|
|
325
|
-
# Cleanup
|
|
326
355
|
rm -f "$signals_file"
|
|
327
|
-
|
|
328
356
|
return 0
|
|
329
357
|
}
|
|
330
358
|
|
|
331
359
|
# ============================================================================
|
|
332
|
-
#
|
|
360
|
+
# COMMANDS
|
|
333
361
|
# ============================================================================
|
|
334
362
|
|
|
335
|
-
|
|
363
|
+
cmd_on() {
|
|
336
364
|
ensure_dirs
|
|
337
|
-
|
|
338
365
|
cat > "$REFLECT_CONFIG" << EOF
|
|
339
366
|
{
|
|
340
367
|
"enabled": true,
|
|
341
368
|
"autoReflect": true,
|
|
342
369
|
"enabledAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
343
|
-
"confidenceThreshold": "
|
|
344
|
-
"maxLearningsPerSession": $DEFAULT_MAX_LEARNINGS
|
|
345
|
-
"gitCommit": false,
|
|
346
|
-
"gitPush": false
|
|
370
|
+
"confidenceThreshold": "high",
|
|
371
|
+
"maxLearningsPerSession": $DEFAULT_MAX_LEARNINGS
|
|
347
372
|
}
|
|
348
373
|
EOF
|
|
349
|
-
|
|
350
|
-
echo "
|
|
351
|
-
echo "🧠 REFLECT: Auto-mode ENABLED"
|
|
352
|
-
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
353
|
-
echo ""
|
|
354
|
-
echo "Sessions will now extract learnings on exit."
|
|
355
|
-
echo "Limits: $MAX_LEARNINGS_PER_CATEGORY per category, ${MAX_MEMORY_FILE_KB}KB max"
|
|
356
|
-
echo ""
|
|
357
|
-
echo "To disable: /sw:reflect-off"
|
|
358
|
-
|
|
359
|
-
log "info" "Auto-reflection enabled"
|
|
374
|
+
echo "🧠 Auto-reflection ENABLED (high-value only)"
|
|
375
|
+
echo " Max $DEFAULT_MAX_LEARNINGS rules/session, $MAX_RULES_PER_CATEGORY per category"
|
|
360
376
|
}
|
|
361
377
|
|
|
362
|
-
|
|
378
|
+
cmd_off() {
|
|
363
379
|
ensure_dirs
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
jq '.autoReflect = false' "$REFLECT_CONFIG" > "$REFLECT_CONFIG.tmp" && mv "$REFLECT_CONFIG.tmp" "$REFLECT_CONFIG"
|
|
367
|
-
else
|
|
368
|
-
cat > "$REFLECT_CONFIG" << EOF
|
|
369
|
-
{
|
|
370
|
-
"enabled": true,
|
|
371
|
-
"autoReflect": false,
|
|
372
|
-
"disabledAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
380
|
+
[ -f "$REFLECT_CONFIG" ] && jq '.autoReflect = false' "$REFLECT_CONFIG" > "$REFLECT_CONFIG.tmp" && mv "$REFLECT_CONFIG.tmp" "$REFLECT_CONFIG"
|
|
381
|
+
echo "🧠 Auto-reflection DISABLED"
|
|
373
382
|
}
|
|
374
|
-
EOF
|
|
375
|
-
fi
|
|
376
|
-
|
|
377
|
-
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
378
|
-
echo "🧠 REFLECT: Auto-mode DISABLED"
|
|
379
|
-
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
380
|
-
echo ""
|
|
381
|
-
echo "Manual /sw:reflect still works."
|
|
382
|
-
echo "To re-enable: /sw:reflect-on"
|
|
383
383
|
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
cmd_reflect_status() {
|
|
384
|
+
cmd_status() {
|
|
388
385
|
ensure_dirs
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
echo "
|
|
394
|
-
echo "
|
|
395
|
-
echo "
|
|
386
|
+
local auto=$(get_config "autoReflect" "false")
|
|
387
|
+
|
|
388
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
389
|
+
echo "🧠 REFLECT STATUS"
|
|
390
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
391
|
+
[ "$auto" = "true" ] && echo " Auto: ✅ ON" || echo " Auto: ❌ OFF"
|
|
392
|
+
echo " Max/session: $DEFAULT_MAX_LEARNINGS"
|
|
393
|
+
echo " Max/category: $MAX_RULES_PER_CATEGORY"
|
|
396
394
|
echo ""
|
|
397
395
|
|
|
398
|
-
[ "$enabled" = "true" ] && echo " Reflection: ✅ Enabled" || echo " Reflection: ❌ Disabled"
|
|
399
|
-
[ "$auto_reflect" = "true" ] && echo " Auto-reflect: ✅ On" || echo " Auto-reflect: ❌ Off"
|
|
400
|
-
echo " Max/category: $MAX_LEARNINGS_PER_CATEGORY"
|
|
401
|
-
echo " Max file: ${MAX_MEMORY_FILE_KB}KB"
|
|
402
|
-
echo ""
|
|
403
|
-
|
|
404
|
-
# Count learnings
|
|
405
396
|
local total=0
|
|
406
397
|
if [ -d "$MEMORY_DIR" ]; then
|
|
407
|
-
echo " 📚 Memory files:"
|
|
408
398
|
for f in "$MEMORY_DIR"/*.md; do
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
total=$((total + count))
|
|
415
|
-
fi
|
|
399
|
+
[ -f "$f" ] || continue
|
|
400
|
+
local name=$(basename "$f" .md)
|
|
401
|
+
local count=$(grep -c "^- " "$f" 2>/dev/null || echo 0)
|
|
402
|
+
echo " 📁 $name: $count rules"
|
|
403
|
+
total=$((total + count))
|
|
416
404
|
done
|
|
417
405
|
fi
|
|
418
406
|
|
|
419
407
|
echo ""
|
|
420
|
-
echo " Total: $total
|
|
421
|
-
echo "
|
|
408
|
+
echo " Total: $total rules"
|
|
409
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
422
410
|
}
|
|
423
411
|
|
|
424
|
-
|
|
425
|
-
local
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
echo "
|
|
429
|
-
rm -rf "$MEMORY_DIR"/*.md
|
|
430
|
-
echo "✅ All learnings cleared"
|
|
412
|
+
cmd_clear() {
|
|
413
|
+
local cat="${1:-all}"
|
|
414
|
+
if [ "$cat" = "all" ]; then
|
|
415
|
+
rm -f "$MEMORY_DIR"/*.md
|
|
416
|
+
echo "✅ Cleared all rules"
|
|
431
417
|
else
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
rm -f "$file"
|
|
435
|
-
echo "✅ Cleared $category learnings"
|
|
436
|
-
else
|
|
437
|
-
echo "⚠️ No $category memory file found"
|
|
438
|
-
fi
|
|
418
|
+
rm -f "$MEMORY_DIR/${cat}.md"
|
|
419
|
+
echo "✅ Cleared $cat rules"
|
|
439
420
|
fi
|
|
421
|
+
}
|
|
440
422
|
|
|
441
|
-
|
|
423
|
+
cmd_show() {
|
|
424
|
+
local cat="${1:-all}"
|
|
425
|
+
if [ "$cat" = "all" ]; then
|
|
426
|
+
for f in "$MEMORY_DIR"/*.md; do
|
|
427
|
+
[ -f "$f" ] && cat "$f" && echo ""
|
|
428
|
+
done
|
|
429
|
+
else
|
|
430
|
+
[ -f "$MEMORY_DIR/${cat}.md" ] && cat "$MEMORY_DIR/${cat}.md" || echo "No $cat rules"
|
|
431
|
+
fi
|
|
442
432
|
}
|
|
443
433
|
|
|
444
434
|
# ============================================================================
|
|
@@ -446,59 +436,34 @@ cmd_reflect_clear() {
|
|
|
446
436
|
# ============================================================================
|
|
447
437
|
|
|
448
438
|
main() {
|
|
449
|
-
local
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
cmd_reflect_off
|
|
458
|
-
;;
|
|
459
|
-
status)
|
|
460
|
-
cmd_reflect_status
|
|
461
|
-
;;
|
|
462
|
-
clear)
|
|
463
|
-
cmd_reflect_clear "$@"
|
|
464
|
-
;;
|
|
439
|
+
local cmd="${1:-reflect}"; shift || true
|
|
440
|
+
|
|
441
|
+
case "$cmd" in
|
|
442
|
+
on|enable) cmd_on ;;
|
|
443
|
+
off|disable) cmd_off ;;
|
|
444
|
+
status) cmd_status ;;
|
|
445
|
+
clear) cmd_clear "$@" ;;
|
|
446
|
+
show) cmd_show "$@" ;;
|
|
465
447
|
reflect|analyze)
|
|
466
|
-
local transcript=""
|
|
467
|
-
local dry_run="false"
|
|
468
|
-
local confidence="$DEFAULT_CONFIDENCE"
|
|
469
|
-
local max="$DEFAULT_MAX_LEARNINGS"
|
|
470
|
-
|
|
448
|
+
local transcript="" dry_run="false" max="$DEFAULT_MAX_LEARNINGS"
|
|
471
449
|
while [ $# -gt 0 ]; do
|
|
472
450
|
case "$1" in
|
|
473
451
|
--transcript) transcript="$2"; shift 2 ;;
|
|
474
452
|
--dry-run) dry_run="true"; shift ;;
|
|
475
|
-
--confidence) confidence="$2"; shift 2 ;;
|
|
476
453
|
--max) max="$2"; shift 2 ;;
|
|
477
454
|
*) shift ;;
|
|
478
455
|
esac
|
|
479
456
|
done
|
|
480
457
|
|
|
481
|
-
# Auto-detect transcript if not provided
|
|
482
458
|
if [ -z "$transcript" ]; then
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if [ -z "$transcript" ]; then
|
|
487
|
-
echo "⚠️ No transcript found. Use --transcript <path>"
|
|
488
|
-
return 1
|
|
489
|
-
fi
|
|
459
|
+
transcript=$(find "${TMPDIR:-/tmp}" -name "*.md" -mmin -5 2>/dev/null | head -1)
|
|
460
|
+
[ -z "$transcript" ] && echo "No transcript found" && return 1
|
|
490
461
|
fi
|
|
491
462
|
|
|
492
|
-
reflect_session "$transcript" "$dry_run" "$
|
|
493
|
-
;;
|
|
494
|
-
*)
|
|
495
|
-
echo "Usage: reflect.sh [on|off|status|clear|reflect] [options]"
|
|
496
|
-
return 1
|
|
463
|
+
reflect_session "$transcript" "$dry_run" "$max"
|
|
497
464
|
;;
|
|
465
|
+
*) echo "Usage: reflect.sh [on|off|status|clear|show|reflect]" ;;
|
|
498
466
|
esac
|
|
499
467
|
}
|
|
500
468
|
|
|
501
|
-
|
|
502
|
-
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
|
503
|
-
main "$@"
|
|
504
|
-
fi
|
|
469
|
+
[ "${BASH_SOURCE[0]}" = "$0" ] && main "$@"
|