@ekkos/cli 1.0.35 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +57 -0
  2. package/dist/commands/dashboard.js +561 -186
  3. package/dist/deploy/settings.js +13 -26
  4. package/package.json +2 -4
  5. package/templates/CLAUDE.md +135 -23
  6. package/templates/ekkos-manifest.json +8 -8
  7. package/templates/hooks/assistant-response.ps1 +256 -160
  8. package/templates/hooks/assistant-response.sh +130 -66
  9. package/templates/hooks/hooks.json +24 -6
  10. package/templates/hooks/lib/contract.sh +43 -31
  11. package/templates/hooks/lib/count-tokens.cjs +0 -0
  12. package/templates/hooks/lib/ekkos-reminders.sh +0 -0
  13. package/templates/hooks/lib/state.sh +53 -1
  14. package/templates/hooks/session-start.ps1 +91 -391
  15. package/templates/hooks/session-start.sh +201 -166
  16. package/templates/hooks/stop.ps1 +202 -341
  17. package/templates/hooks/stop.sh +275 -948
  18. package/templates/hooks/user-prompt-submit.ps1 +224 -548
  19. package/templates/hooks/user-prompt-submit.sh +382 -456
  20. package/templates/plan-template.md +0 -0
  21. package/templates/spec-template.md +0 -0
  22. package/templates/windsurf-hooks/hooks.json +9 -2
  23. package/templates/windsurf-hooks/install.sh +0 -0
  24. package/templates/windsurf-hooks/lib/contract.sh +2 -0
  25. package/templates/windsurf-hooks/post-cascade-response.sh +0 -0
  26. package/templates/windsurf-hooks/pre-user-prompt.sh +0 -0
  27. package/templates/agents/README.md +0 -182
  28. package/templates/agents/code-reviewer.md +0 -166
  29. package/templates/agents/debug-detective.md +0 -169
  30. package/templates/agents/ekkOS_Vercel.md +0 -99
  31. package/templates/agents/extension-manager.md +0 -229
  32. package/templates/agents/git-companion.md +0 -185
  33. package/templates/agents/github-test-agent.md +0 -321
  34. package/templates/agents/railway-manager.md +0 -179
  35. package/templates/windsurf-hooks/before-submit-prompt.sh +0 -238
  36. package/templates/windsurf-skills/ekkos-memory/SKILL.md +0 -219
@@ -1,6 +1,14 @@
1
1
  #!/bin/bash
2
- # Post-response hook: Validates and enforces ekkOS footer format
2
+ # ═══════════════════════════════════════════════════════════════════════════
3
+ # ekkOS_ Hook: AssistantResponse - Validates and enforces footer format
4
+ # MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
5
+ # EKKOS_MANAGED=1
6
+ # ═══════════════════════════════════════════════════════════════════════════
3
7
  # Runs AFTER assistant response, checks footer compliance
8
+ # Per spec v1.2 Addendum: NO jq, NO hardcoded arrays
9
+ # ═══════════════════════════════════════════════════════════════════════════
10
+
11
+ set +e
4
12
 
5
13
  RESPONSE_FILE="$1"
6
14
  HOOK_ENV="$2"
@@ -10,87 +18,143 @@ if [[ ! -f "$RESPONSE_FILE" ]]; then
10
18
  exit 0
11
19
  fi
12
20
 
13
- # Parse metadata from hook environment
14
- SESSION_ID=$(echo "$HOOK_ENV" | jq -r '.sessionId // "unknown"')
15
- TURN=$(echo "$HOOK_ENV" | jq -r '.turn // 0')
16
- CONTEXT_PERCENT=$(echo "$HOOK_ENV" | jq -r '.contextUsagePercent // 0')
17
- MODEL=$(echo "$HOOK_ENV" | jq -r '.model // "Claude Code (Opus 4.5)"')
21
+ # ═══════════════════════════════════════════════════════════════════════════
22
+ # CONFIG PATHS - No hardcoded word arrays per spec v1.2 Addendum
23
+ # ═══════════════════════════════════════════════════════════════════════════
24
+ EKKOS_CONFIG_DIR="${EKKOS_CONFIG_DIR:-$HOME/.ekkos}"
25
+ SESSION_WORDS_JSON="$EKKOS_CONFIG_DIR/session-words.json"
26
+ SESSION_WORDS_DEFAULT="$EKKOS_CONFIG_DIR/.defaults/session-words.json"
27
+ JSON_PARSE_HELPER="$EKKOS_CONFIG_DIR/.helpers/json-parse.cjs"
28
+
29
+ # Parse metadata from hook environment using Node (no jq)
30
+ parse_hook_env() {
31
+ local json="$1"
32
+ local path="$2"
33
+ echo "$json" | node -e "
34
+ const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
35
+ const path = '$path'.replace(/^\./,'').split('.').filter(Boolean);
36
+ let result = data;
37
+ for (const p of path) {
38
+ if (result === undefined || result === null) { result = undefined; break; }
39
+ result = result[p];
40
+ }
41
+ if (result !== undefined && result !== null) console.log(result);
42
+ " 2>/dev/null || echo ""
43
+ }
44
+
45
+ SESSION_ID=$(parse_hook_env "$HOOK_ENV" '.sessionId')
46
+ [ -z "$SESSION_ID" ] && SESSION_ID="unknown"
47
+
48
+ MODEL=$(parse_hook_env "$HOOK_ENV" '.model')
49
+ [ -z "$MODEL" ] && MODEL="Claude Code (Opus 4.5)"
50
+
51
+ # ═══════════════════════════════════════════════════════════════════════════
52
+ # Session name conversion - Uses external session-words.json (NO hardcoded arrays)
53
+ # ═══════════════════════════════════════════════════════════════════════════
54
+ declare -a ADJECTIVES
55
+ declare -a NOUNS
56
+ declare -a VERBS
57
+ SESSION_WORDS_LOADED=false
58
+
59
+ load_session_words() {
60
+ if [ "$SESSION_WORDS_LOADED" = "true" ]; then
61
+ return 0
62
+ fi
63
+
64
+ local words_file="$SESSION_WORDS_JSON"
65
+ if [ ! -f "$words_file" ]; then
66
+ words_file="$SESSION_WORDS_DEFAULT"
67
+ fi
68
+
69
+ if [ ! -f "$words_file" ] || [ ! -f "$JSON_PARSE_HELPER" ]; then
70
+ return 1
71
+ fi
72
+
73
+ if command -v node &>/dev/null; then
74
+ if [ "${BASH_VERSINFO[0]}" -ge 4 ]; then
75
+ readarray -t ADJECTIVES < <(node "$JSON_PARSE_HELPER" "$words_file" '.adjectives' 2>/dev/null)
76
+ readarray -t NOUNS < <(node "$JSON_PARSE_HELPER" "$words_file" '.nouns' 2>/dev/null)
77
+ readarray -t VERBS < <(node "$JSON_PARSE_HELPER" "$words_file" '.verbs' 2>/dev/null)
78
+ else
79
+ local i=0
80
+ while IFS= read -r line; do
81
+ ADJECTIVES[i]="$line"
82
+ ((i++))
83
+ done < <(node "$JSON_PARSE_HELPER" "$words_file" '.adjectives' 2>/dev/null)
84
+ i=0
85
+ while IFS= read -r line; do
86
+ NOUNS[i]="$line"
87
+ ((i++))
88
+ done < <(node "$JSON_PARSE_HELPER" "$words_file" '.nouns' 2>/dev/null)
89
+ i=0
90
+ while IFS= read -r line; do
91
+ VERBS[i]="$line"
92
+ ((i++))
93
+ done < <(node "$JSON_PARSE_HELPER" "$words_file" '.verbs' 2>/dev/null)
94
+ fi
95
+
96
+ if [ ${#ADJECTIVES[@]} -gt 0 ] && [ ${#NOUNS[@]} -gt 0 ] && [ ${#VERBS[@]} -gt 0 ]; then
97
+ SESSION_WORDS_LOADED=true
98
+ return 0
99
+ fi
100
+ fi
101
+ return 1
102
+ }
18
103
 
19
- # Convert session UUID to word-based name
20
104
  convert_uuid_to_name() {
21
- local uuid="$1"
22
-
23
- # Word lists (same as MCP)
24
- local ADJECTIVES=("cosmic" "turbo" "mega" "hyper" "quantum" "atomic" "stellar" "epic"
25
- "mighty" "groovy" "zippy" "snappy" "jazzy" "funky" "zesty" "peppy"
26
- "spicy" "crispy" "fluffy" "sparkly" "chunky" "bouncy" "bubbly" "sassy"
27
- "slick" "sleek" "bold" "nifty" "perky" "plucky" "witty" "nimble"
28
- "dapper" "fancy" "quirky" "punchy" "swift" "brave" "clever" "dandy"
29
- "eager" "fiery" "golden" "hasty" "icy" "jolly" "keen" "lively"
30
- "merry" "noble" "odd" "plush" "quick" "royal" "silly" "tidy"
31
- "ultra" "vivid" "wacky" "zany" "alpha" "beta" "cyber" "delta"
32
- "electric" "foggy" "giga" "hazy" "ionic" "jumpy" "kinky" "lunar"
33
- "magic" "nerdy" "omega" "pixel" "quaint" "retro" "solar" "techno"
34
- "unified" "viral" "wonky" "xerox" "yappy" "zen" "agile" "binary"
35
- "chrome" "disco" "elastic" "fizzy" "glossy" "humble" "itchy" "jiffy"
36
- "kooky" "loopy" "moody" "noisy")
37
-
38
- local NOUNS=("penguin" "panda" "otter" "narwhal" "alpaca" "llama" "badger" "walrus"
39
- "waffle" "pickle" "noodle" "pretzel" "muffin" "taco" "nugget" "biscuit"
40
- "rocket" "comet" "nebula" "quasar" "meteor" "photon" "pulsar" "nova"
41
- "ninja" "pirate" "wizard" "robot" "yeti" "phoenix" "sphinx" "kraken"
42
- "thunder" "blizzard" "tornado" "avalanche" "mango" "kiwi" "banana" "coconut"
43
- "donut" "espresso" "falafel" "gyro" "hummus" "icecream" "jambon" "kebab"
44
- "latte" "mocha" "nachos" "olive" "pasta" "quinoa" "ramen" "sushi"
45
- "tamale" "udon" "velvet" "wasabi" "xmas" "yogurt" "ziti" "anchor"
46
- "beacon" "canyon" "drifter" "echo" "falcon" "glacier" "harbor" "island"
47
- "jetpack" "kayak" "lagoon" "meadow" "orbit" "parrot" "quest"
48
- "rapids" "summit" "tunnel" "umbrella" "volcano" "whisper" "xylophone" "yacht"
49
- "zephyr" "acorn" "bobcat" "cactus" "dolphin" "eagle" "ferret" "gopher"
50
- "hedgehog" "iguana" "jackal" "koala")
51
-
52
- # Extract first 8 hex chars
53
- local hex="${uuid:0:8}"
54
- hex="${hex//-/}"
55
-
56
- # Convert to number
57
- local num=$((16#$hex))
58
-
59
- # Calculate indices
60
- local adj_idx=$((num % 100))
61
- local noun_idx=$(((num / 100) % 100))
62
-
63
- echo "${ADJECTIVES[$adj_idx]}-${NOUNS[$noun_idx]}"
105
+ local uuid="$1"
106
+
107
+ load_session_words || {
108
+ echo "unknown-session"
109
+ return
110
+ }
111
+
112
+ local hex="${uuid//-/}"
113
+ hex="${hex:0:12}"
114
+
115
+ if [[ ! "$hex" =~ ^[0-9a-fA-F]+$ ]]; then
116
+ echo "unknown-session"
117
+ return
118
+ fi
119
+
120
+ local adj_seed=$((16#${hex:0:4}))
121
+ local noun_seed=$((16#${hex:4:4}))
122
+ local verb_seed=$((16#${hex:8:4}))
123
+ local adj_idx=$((adj_seed % ${#ADJECTIVES[@]}))
124
+ local noun_idx=$((noun_seed % ${#NOUNS[@]}))
125
+ local verb_idx=$((verb_seed % ${#VERBS[@]}))
126
+
127
+ echo "${ADJECTIVES[$adj_idx]}-${NOUNS[$noun_idx]}-${VERBS[$verb_idx]}"
64
128
  }
65
129
 
66
130
  SESSION_NAME=$(convert_uuid_to_name "$SESSION_ID")
67
- TIMESTAMP=$(date "+%Y-%m-%d %I:%M %p %Z")
131
+ TIMESTAMP=$(date "+%Y-%m-%d %I:%M:%S %p %Z")
68
132
 
69
133
  # Required footer format
70
134
  REQUIRED_FOOTER="---
71
- $MODEL · $SESSION_NAME · Turn $TURN · ${CONTEXT_PERCENT}% · 🧠 **ekkOS_™** · 📅 $TIMESTAMP"
135
+ $MODEL · 🧠 ekkOS_™ · $SESSION_NAME · 📅 $TIMESTAMP"
72
136
 
73
137
  # Check if response has correct footer
74
138
  RESPONSE_CONTENT=$(cat "$RESPONSE_FILE")
75
139
  LAST_LINE=$(echo "$RESPONSE_CONTENT" | tail -1)
76
140
 
77
141
  # Check if footer exists and is correct
78
- if [[ "$LAST_LINE" == *"ekkOS"* ]] && [[ "$LAST_LINE" == *"Turn"* ]]; then
79
- # Footer exists - validate format
80
- if [[ "$LAST_LINE" == *"Turn $TURN"* ]] && [[ "$LAST_LINE" == *"${CONTEXT_PERCENT}%"* ]] && [[ "$LAST_LINE" == *"$SESSION_NAME"* ]]; then
81
- # Footer is correct
82
- exit 0
83
- else
84
- # Footer exists but is malformed - replace it
85
- RESPONSE_WITHOUT_FOOTER=$(echo "$RESPONSE_CONTENT" | head -n -2) # Remove last 2 lines (--- and footer)
86
- echo "$RESPONSE_WITHOUT_FOOTER" > "$RESPONSE_FILE"
142
+ if [[ "$LAST_LINE" == *"ekkOS"* ]] && [[ "$LAST_LINE" == *"$SESSION_NAME"* ]]; then
143
+ # Footer exists - validate format
144
+ if [[ "$LAST_LINE" == *"$SESSION_NAME"* ]] && [[ "$LAST_LINE" == *"📅"* ]]; then
145
+ # Footer is correct
146
+ exit 0
147
+ else
148
+ # Footer exists but is malformed - replace it
149
+ RESPONSE_WITHOUT_FOOTER=$(echo "$RESPONSE_CONTENT" | head -n -2)
150
+ echo "$RESPONSE_WITHOUT_FOOTER" > "$RESPONSE_FILE"
151
+ echo "" >> "$RESPONSE_FILE"
152
+ echo "$REQUIRED_FOOTER" >> "$RESPONSE_FILE"
153
+ fi
154
+ else
155
+ # Footer missing - append it
87
156
  echo "" >> "$RESPONSE_FILE"
88
157
  echo "$REQUIRED_FOOTER" >> "$RESPONSE_FILE"
89
- fi
90
- else
91
- # Footer missing - append it
92
- echo "" >> "$RESPONSE_FILE"
93
- echo "$REQUIRED_FOOTER" >> "$RESPONSE_FILE"
94
158
  fi
95
159
 
96
160
  exit 0
@@ -2,20 +2,38 @@
2
2
  "hooks": {
3
3
  "SessionStart": [
4
4
  {
5
- "type": "command",
6
- "command": "bash .claude/hooks/session-start.sh"
5
+ "matcher": "*",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "bash .claude/hooks/session-start.sh",
10
+ "timeout": 5000
11
+ }
12
+ ]
7
13
  }
8
14
  ],
9
15
  "UserPromptSubmit": [
10
16
  {
11
- "type": "command",
12
- "command": "bash .claude/hooks/user-prompt-submit.sh"
17
+ "matcher": "*",
18
+ "hooks": [
19
+ {
20
+ "type": "command",
21
+ "command": "bash .claude/hooks/user-prompt-submit.sh",
22
+ "timeout": 5000
23
+ }
24
+ ]
13
25
  }
14
26
  ],
15
27
  "Stop": [
16
28
  {
17
- "type": "command",
18
- "command": "bash .claude/hooks/stop.sh"
29
+ "matcher": "*",
30
+ "hooks": [
31
+ {
32
+ "type": "command",
33
+ "command": "bash .claude/hooks/stop.sh",
34
+ "timeout": 5000
35
+ }
36
+ ]
19
37
  }
20
38
  ]
21
39
  }
@@ -78,7 +78,7 @@ write_turn_contract() {
78
78
  "retrieved_directive_ids": $directive_array,
79
79
  "timestamp": "$timestamp",
80
80
  "query_hash": "$query_hash",
81
- "ekkos_strict": ${EKKOS_STRICT:-0}
81
+ "ekkos_strict": ${EKKOS_STRICT:-1}
82
82
  }
83
83
  EOF
84
84
 
@@ -134,7 +134,7 @@ cleanup_turn_contract() {
134
134
 
135
135
  # Check if strict mode is enabled
136
136
  is_strict_mode() {
137
- [ "${EKKOS_STRICT:-0}" = "1" ]
137
+ [ "${EKKOS_STRICT:-1}" = "1" ] # DEFAULT: ON
138
138
  }
139
139
 
140
140
  # Generate strict mode blocker message for Claude Code
@@ -157,50 +157,57 @@ EOF
157
157
  }
158
158
 
159
159
  # Validate PatternGuard coverage (returns 0-100)
160
+ # PORTABLE: Works on macOS without Perl regex (grep -P)
160
161
  calculate_pattern_guard_coverage() {
161
162
  local assistant_response="$1"
162
163
  local pattern_ids="$2" # Comma-separated
163
164
 
164
- # Count total patterns
165
- local total_count
166
- total_count=$(echo "$pattern_ids" | tr ',' '\n' | grep -c '.' || echo 0)
165
+ # Count total patterns - more robust counting
166
+ local total_count=0
167
+ if [ -n "$pattern_ids" ]; then
168
+ total_count=$(echo "$pattern_ids" | tr ',' '\n' | grep -v '^$' | wc -l | tr -d ' ')
169
+ fi
167
170
 
168
171
  if [ "$total_count" -eq 0 ]; then
169
172
  echo "100" # No patterns = 100% coverage by definition
170
173
  return 0
171
174
  fi
172
175
 
173
- # Extract acknowledged IDs from [ekkOS_SELECT] and [ekkOS_SKIP] blocks
176
+ # Extract acknowledged IDs using portable awk (works on macOS and Linux)
174
177
  local acknowledged_count=0
175
178
 
176
- # Check SELECT block
177
- local select_block
178
- select_block=$(echo "$assistant_response" | grep -ozP '\[ekkOS_SELECT\][\s\S]*?\[/ekkOS_SELECT\]' 2>/dev/null | tr '\0' '\n' || true)
179
- if [ -n "$select_block" ]; then
180
- local select_count
181
- select_count=$(echo "$select_block" | grep -oE 'id:\s*[a-f0-9-]+' | wc -l | tr -d ' ')
182
- acknowledged_count=$((acknowledged_count + select_count))
183
- fi
184
-
185
- # Check SKIP block
186
- local skip_block
187
- skip_block=$(echo "$assistant_response" | grep -ozP '\[ekkOS_SKIP\][\s\S]*?\[/ekkOS_SKIP\]' 2>/dev/null | tr '\0' '\n' || true)
188
- if [ -n "$skip_block" ]; then
189
- local skip_count
190
- skip_count=$(echo "$skip_block" | grep -oE 'id:\s*[a-f0-9-]+' | wc -l | tr -d ' ')
191
- acknowledged_count=$((acknowledged_count + skip_count))
192
- fi
179
+ # Use awk to extract SELECT block and count IDs - PORTABLE approach
180
+ local select_count=0
181
+ select_count=$(echo "$assistant_response" | awk '
182
+ /\[ekkOS_SELECT\]/{in_block=1; next}
183
+ /\[\/ekkOS_SELECT\]/{in_block=0}
184
+ in_block && /id:/{count++}
185
+ END{print count+0}
186
+ ')
187
+ acknowledged_count=$((acknowledged_count + select_count))
188
+
189
+ # Use awk to extract SKIP block and count IDs
190
+ local skip_count=0
191
+ skip_count=$(echo "$assistant_response" | awk '
192
+ /\[ekkOS_SKIP\]/{in_block=1; next}
193
+ /\[\/ekkOS_SKIP\]/{in_block=0}
194
+ in_block && /id:/{count++}
195
+ END{print count+0}
196
+ ')
197
+ acknowledged_count=$((acknowledged_count + skip_count))
193
198
 
194
199
  # Legacy: Check for [ekkOS_APPLY] markers (fallback)
195
200
  if [ "$acknowledged_count" -eq 0 ]; then
196
201
  local apply_count
197
- apply_count=$(echo "$assistant_response" | grep -c '\[ekkOS_APPLY\]' || echo 0)
198
- acknowledged_count=$apply_count
202
+ apply_count=$(echo "$assistant_response" | grep -c '\[ekkOS_APPLY\]' 2>/dev/null || echo 0)
203
+ acknowledged_count=$((acknowledged_count + apply_count))
199
204
  fi
200
205
 
201
206
  # Calculate coverage percentage
202
- local coverage
203
- coverage=$((acknowledged_count * 100 / total_count))
207
+ local coverage=0
208
+ if [ "$total_count" -gt 0 ]; then
209
+ coverage=$((acknowledged_count * 100 / total_count))
210
+ fi
204
211
 
205
212
  # Cap at 100%
206
213
  if [ "$coverage" -gt 100 ]; then
@@ -255,11 +262,16 @@ EOF
255
262
  }
256
263
 
257
264
  # Determine if turn is compliant
265
+ # ROBUST: Handles edge cases with non-numeric or empty values
258
266
  is_turn_compliant() {
259
- local retrieval_ok="$1"
260
- local pattern_guard_coverage="$2"
261
- local footer_present="$3"
262
- local pattern_count="$4"
267
+ local retrieval_ok="${1:-true}"
268
+ local pattern_guard_coverage="${2:-100}"
269
+ local footer_present="${3:-true}"
270
+ local pattern_count="${4:-0}"
271
+
272
+ # Sanitize numeric values (default to safe values)
273
+ pattern_guard_coverage=$(echo "$pattern_guard_coverage" | grep -oE '^[0-9]+$' || echo "100")
274
+ pattern_count=$(echo "$pattern_count" | grep -oE '^[0-9]+$' || echo "0")
263
275
 
264
276
  # Retrieval must have succeeded
265
277
  if [ "$retrieval_ok" != "true" ]; then
File without changes
File without changes
@@ -20,25 +20,40 @@ mkdir -p "$STATE_DIR"
20
20
  # State File Management
21
21
  # ═══════════════════════════════════════════════════════════════════════════
22
22
 
23
- # Save patterns for session
23
+ # Save patterns for session (with retrieval_token for verified tracking)
24
24
  save_patterns() {
25
25
  local session_id="$1"
26
26
  local patterns="$2"
27
27
  local model_used="$3"
28
+ local retrieval_token="${4:-}" # Optional retrieval_token for verified applications
28
29
 
29
30
  local state_file="$STATE_DIR/patterns-${session_id}.json"
30
31
 
31
32
  jq -n \
32
33
  --argjson patterns "$patterns" \
33
34
  --arg model "$model_used" \
35
+ --arg token "$retrieval_token" \
34
36
  --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
35
37
  '{
36
38
  patterns: $patterns,
37
39
  model_used: $model,
40
+ retrieval_token: $token,
38
41
  saved_at: $timestamp
39
42
  }' > "$state_file"
40
43
  }
41
44
 
45
+ # Get retrieval token from saved patterns
46
+ get_retrieval_token() {
47
+ local session_id="$1"
48
+ local state_file="$STATE_DIR/patterns-${session_id}.json"
49
+
50
+ if [ -f "$state_file" ]; then
51
+ jq -r '.retrieval_token // ""' "$state_file" 2>/dev/null
52
+ else
53
+ echo ""
54
+ fi
55
+ }
56
+
42
57
  # Load patterns for session
43
58
  load_patterns() {
44
59
  local session_id="$1"
@@ -57,6 +72,43 @@ clear_patterns() {
57
72
  rm -f "$STATE_DIR/patterns-${session_id}.json"
58
73
  }
59
74
 
75
+ # ═══════════════════════════════════════════════════════════════════════════
76
+ # Conversation Context (for Golden Loop MEASURE phase)
77
+ # ═══════════════════════════════════════════════════════════════════════════
78
+
79
+ # Save current conversation context for pattern tracking
80
+ # Called by user-prompt-submit.sh when new query is submitted
81
+ save_conversation_context() {
82
+ local session_id="$1"
83
+ local user_query="$2"
84
+ local prev_response="$3"
85
+
86
+ local context_file="$STATE_DIR/conversation-${session_id}.json"
87
+
88
+ jq -n \
89
+ --arg query "$user_query" \
90
+ --arg response "$prev_response" \
91
+ --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
92
+ '{
93
+ query: $query,
94
+ response: $response,
95
+ saved_at: $timestamp
96
+ }' > "$context_file" 2>/dev/null || true
97
+ }
98
+
99
+ # Load current conversation context
100
+ # Called by post-tool-use.sh when tracking pattern applications
101
+ load_conversation_context() {
102
+ local session_id="$1"
103
+ local context_file="$STATE_DIR/conversation-${session_id}.json"
104
+
105
+ if [ -f "$context_file" ]; then
106
+ cat "$context_file"
107
+ else
108
+ echo '{"query":"","response":""}'
109
+ fi
110
+ }
111
+
60
112
  # ═══════════════════════════════════════════════════════════════════════════
61
113
  # Capture Deduplication
62
114
  # ═══════════════════════════════════════════════════════════════════════════