@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,656 +1,332 @@
1
1
  # ═══════════════════════════════════════════════════════════════════════════
2
2
  # ekkOS_ Hook: UserPromptSubmit - SEAMLESS CONTEXT CONTINUITY (Windows)
3
- # ═══════════════════════════════════════════════════════════════════════════
4
- # ZERO USER ACTION NEEDED:
5
- # 1. Tracks turn number and context size
6
- # 2. Detects when compaction happened (context dropped from high to low)
7
- # 3. AUTO-INJECTS restored context - user just keeps working
8
- # Per spec v1.2 Addendum: NO jq dependency, uses Node.js for JSON
3
+ # MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
4
+ # EKKOS_MANAGED=1
5
+ # EKKOS_MANIFEST_SHA256=<computed-at-build>
6
+ # EKKOS_TEMPLATE_VERSION=1.0.0
7
+ #
8
+ # Per ekkOS Onboarding Spec v1.2 FINAL + ADDENDUM:
9
+ # - All persisted records MUST include: instanceId, sessionId, sessionName
10
+ # - Uses EKKOS_INSTANCE_ID env var for multi-session isolation
9
11
  # ═══════════════════════════════════════════════════════════════════════════
10
12
 
11
13
  $ErrorActionPreference = "SilentlyContinue"
12
14
 
13
- $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
14
- $ProjectRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
15
+ # ═══════════════════════════════════════════════════════════════════════════
16
+ # CONFIG PATHS - No hardcoded word arrays per spec v1.2 Addendum
17
+ # ═══════════════════════════════════════════════════════════════════════════
18
+ $EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { "$env:USERPROFILE\.ekkos" }
19
+ $SessionWordsJson = "$EkkosConfigDir\session-words.json"
20
+ $SessionWordsDefault = "$EkkosConfigDir\.defaults\session-words.json"
15
21
 
16
22
  # ═══════════════════════════════════════════════════════════════════════════
17
- # CONFIG PATHS - Per spec v1.2 Addendum
23
+ # INSTANCE ID - Multi-session isolation per v1.2 ADDENDUM
18
24
  # ═══════════════════════════════════════════════════════════════════════════
19
- $EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { Join-Path $env:USERPROFILE ".ekkos" }
20
- $SessionWordsJson = Join-Path $EkkosConfigDir "session-words.json"
21
- $SessionWordsDefault = Join-Path $EkkosConfigDir ".defaults\session-words.json"
22
- $JsonParseHelper = Join-Path $EkkosConfigDir ".helpers\json-parse.cjs"
25
+ $EkkosInstanceId = if ($env:EKKOS_INSTANCE_ID) { $env:EKKOS_INSTANCE_ID } else { "default" }
23
26
 
24
27
  # ═══════════════════════════════════════════════════════════════════════════
25
- # WORD-BASED SESSION NAMES - Uses external session-words.json
28
+ # Load session words from JSON file - NO HARDCODED ARRAYS
26
29
  # ═══════════════════════════════════════════════════════════════════════════
27
- $script:ADJECTIVES = @()
28
- $script:NOUNS = @()
29
- $script:VERBS = @()
30
- $script:SESSION_WORDS_LOADED = $false
30
+ $script:SessionWords = $null
31
31
 
32
32
  function Load-SessionWords {
33
- if ($script:SESSION_WORDS_LOADED) { return }
34
-
35
33
  $wordsFile = $SessionWordsJson
34
+
35
+ # Fallback to managed defaults if user file missing/invalid
36
36
  if (-not (Test-Path $wordsFile)) {
37
37
  $wordsFile = $SessionWordsDefault
38
38
  }
39
39
 
40
- if ((-not (Test-Path $wordsFile)) -or (-not (Test-Path $JsonParseHelper))) {
41
- $script:ADJECTIVES = @("unknown")
42
- $script:NOUNS = @("session")
43
- $script:VERBS = @("starts")
44
- return
40
+ if (-not (Test-Path $wordsFile)) {
41
+ return $null
45
42
  }
46
43
 
47
44
  try {
48
- $adjRaw = & node $JsonParseHelper $wordsFile ".adjectives" 2>$null
49
- $nounRaw = & node $JsonParseHelper $wordsFile ".nouns" 2>$null
50
- $verbRaw = & node $JsonParseHelper $wordsFile ".verbs" 2>$null
51
-
52
- $script:ADJECTIVES = @()
53
- $script:NOUNS = @()
54
- $script:VERBS = @()
55
-
56
- if ($adjRaw) { $script:ADJECTIVES = @($adjRaw -split "`n" | Where-Object { $_ }) }
57
- if ($nounRaw) { $script:NOUNS = @($nounRaw -split "`n" | Where-Object { $_ }) }
58
- if ($verbRaw) { $script:VERBS = @($verbRaw -split "`n" | Where-Object { $_ }) }
59
-
60
- if ($script:ADJECTIVES.Count -eq 0) { $script:ADJECTIVES = @("unknown") }
61
- if ($script:NOUNS.Count -eq 0) { $script:NOUNS = @("session") }
62
- if ($script:VERBS.Count -eq 0) { $script:VERBS = @("starts") }
63
-
64
- $script:SESSION_WORDS_LOADED = $true
45
+ $script:SessionWords = Get-Content $wordsFile -Raw | ConvertFrom-Json
65
46
  } catch {
66
- $script:ADJECTIVES = @("unknown")
67
- $script:NOUNS = @("session")
68
- $script:VERBS = @("starts")
47
+ return $null
69
48
  }
70
49
  }
71
50
 
72
- function Convert-UuidToWords {
73
- param([string]$uuid)
74
-
75
- Load-SessionWords
76
-
77
- $hex = $uuid -replace "-", ""
78
- $hex = $hex.Substring(0, [Math]::Min(12, $hex.Length))
79
- if ($hex -notmatch '^[0-9a-fA-F]+$') { return "unknown-session-starts" }
80
-
81
- try {
82
- $adjSeed = [Convert]::ToInt32($hex.Substring(0, 4), 16)
83
- $nounSeed = [Convert]::ToInt32($hex.Substring(4, 4), 16)
84
- $verbSeed = [Convert]::ToInt32($hex.Substring(8, 4), 16)
85
-
86
- $adjIdx = $adjSeed % $script:ADJECTIVES.Count
87
- $nounIdx = $nounSeed % $script:NOUNS.Count
88
- $verbIdx = $verbSeed % $script:VERBS.Count
89
-
90
- return "$($script:ADJECTIVES[$adjIdx])-$($script:NOUNS[$nounIdx])-$($script:VERBS[$verbIdx])"
91
- } catch {
92
- return "unknown-session-starts"
93
- }
94
- }
51
+ # Read input from stdin
52
+ $inputJson = [Console]::In.ReadToEnd()
53
+ if (-not $inputJson) { exit 0 }
95
54
 
96
- # ═══════════════════════════════════════════════════════════════════════════
97
- # JSON parsing helper (no jq) - pipes JSON to node, extracts dot-path value
98
- # ═══════════════════════════════════════════════════════════════════════════
99
- function Parse-JsonValue {
100
- param(
101
- [string]$Json,
102
- [string]$Path
103
- )
104
- if (-not $Json) { return "" }
105
- try {
106
- $result = $Json | node -e "
107
- const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
108
- const path = '$Path'.replace(/^\./,'').split('.').filter(Boolean);
109
- let result = data;
110
- for (const p of path) {
111
- if (result === undefined || result === null) { result = undefined; break; }
112
- result = result[p];
113
- }
114
- if (result !== undefined && result !== null) console.log(result);
115
- " 2>$null
116
- if ($result) { return $result.Trim() } else { return "" }
117
- } catch {
118
- return ""
119
- }
55
+ try {
56
+ $input = $inputJson | ConvertFrom-Json
57
+ } catch {
58
+ exit 0
120
59
  }
121
60
 
122
- # ═══════════════════════════════════════════════════════════════════════════
123
- # Read input from stdin
124
- # ═══════════════════════════════════════════════════════════════════════════
125
- $INPUT = [Console]::In.ReadToEnd()
126
- if (-not $INPUT) { exit 0 }
61
+ $userQuery = $input.query
62
+ if (-not $userQuery) { $userQuery = $input.message }
63
+ if (-not $userQuery) { $userQuery = $input.prompt }
64
+ if (-not $userQuery) { exit 0 }
127
65
 
128
- $USER_QUERY = Parse-JsonValue $INPUT ".query"
129
- if (-not $USER_QUERY) { $USER_QUERY = Parse-JsonValue $INPUT ".message" }
130
- if (-not $USER_QUERY) { $USER_QUERY = Parse-JsonValue $INPUT ".prompt" }
131
- if (-not $USER_QUERY -or $USER_QUERY -eq "null") { exit 0 }
66
+ $rawSessionId = $input.session_id
67
+ if (-not $rawSessionId -or $rawSessionId -eq "null") { $rawSessionId = "unknown" }
132
68
 
133
- $RAW_SESSION_ID = Parse-JsonValue $INPUT ".session_id"
134
- if (-not $RAW_SESSION_ID -or $RAW_SESSION_ID -eq "null") { $RAW_SESSION_ID = "unknown" }
135
- $TRANSCRIPT_PATH = Parse-JsonValue $INPUT ".transcript_path"
136
-
137
- # Fallback: read session_id from saved state if not in INPUT
138
- if ($RAW_SESSION_ID -eq "unknown") {
69
+ # Fallback: read session_id from saved state
70
+ if ($rawSessionId -eq "unknown") {
139
71
  $stateFile = Join-Path $env:USERPROFILE ".claude\state\current-session.json"
140
72
  if (Test-Path $stateFile) {
141
- $savedState = Get-Content $stateFile -Raw
142
- $savedId = Parse-JsonValue $savedState ".session_id"
143
- if ($savedId -and $savedId -ne "unknown" -and $savedId -ne "null") {
144
- $RAW_SESSION_ID = $savedId
145
- }
73
+ try {
74
+ $state = Get-Content $stateFile -Raw | ConvertFrom-Json
75
+ $rawSessionId = $state.session_id
76
+ } catch {}
146
77
  }
147
78
  }
148
79
 
149
80
  # ═══════════════════════════════════════════════════════════════════════════
150
81
  # INTELLIGENT TOOL ROUTER: Multi-trigger skill detection
151
- # Detects ALL applicable skills/tools and injects as system reminder
152
82
  # ═══════════════════════════════════════════════════════════════════════════
153
- $SKILL_REMINDERS = @()
154
- $QUERY_LOWER = $USER_QUERY.ToLower()
83
+ $skillReminders = @()
84
+ $queryLower = $userQuery.ToLower()
155
85
 
156
86
  # Memory First - Debug/Error/Problem solving
157
- if ($QUERY_LOWER -match '(how do i|debug|error|bug|fix|not working|broken|fails|issue|problem|wrong|crash)') {
158
- $SKILL_REMINDERS += "SKILL REQUIRED: Call Skill(skill: `"ekkOS_Memory_First`") FIRST before debugging"
87
+ if ($queryLower -match '(how do i|debug|error|bug|fix|not working|broken|fails|issue|problem|wrong|crash)') {
88
+ $skillReminders += "SKILL REQUIRED: Call Skill(skill: `"ekkOS_Memory_First`") FIRST before debugging"
159
89
  }
160
90
 
161
- # Recall Triggers (Time-based memory)
162
- if ($QUERY_LOWER -match '(yesterday|last week|last month|remember when|what did we|where did we leave|before|earlier|previous|ago)') {
163
- $SKILL_REMINDERS += "SKILL REQUIRED: Call Skill(skill: `"ekkOS_Deep_Recall`") for time-based memory"
91
+ # Recall Triggers - Time-based memory
92
+ if ($queryLower -match '(yesterday|last week|last month|remember when|what did we|where did we leave|before|earlier|previous|ago)') {
93
+ $skillReminders += "SKILL REQUIRED: Call Skill(skill: `"ekkOS_Deep_Recall`") for time-based memory"
164
94
  }
165
95
 
166
- # Directive Triggers (User preferences)
167
- if ($QUERY_LOWER -match '(always |never |i prefer|i like |dont |don.t |avoid |remember that |from now on)') {
168
- $SKILL_REMINDERS += "SKILL REQUIRED: Call Skill(skill: `"ekkOS_Preferences`") to capture directive"
96
+ # Directive Triggers - User preferences
97
+ if ($queryLower -match '(always |never |i prefer|i like |dont |don.t |avoid |remember that |from now on)') {
98
+ $skillReminders += "SKILL REQUIRED: Call Skill(skill: `"ekkOS_Preferences`") to capture directive"
169
99
  }
170
100
 
171
- # Safety Triggers (Destructive actions)
172
- if ($QUERY_LOWER -match '(delete|drop |rm -rf|deploy|push.*main|push.*master|production|migrate|rollback)') {
173
- $SKILL_REMINDERS += "SAFETY REQUIRED: Call ekkOS_Conflict before this destructive action"
101
+ # Safety Triggers - Destructive actions
102
+ if ($queryLower -match '(delete|drop |rm -rf|deploy|push.*main|push.*master|production|migrate|rollback)') {
103
+ $skillReminders += "SAFETY REQUIRED: Call ekkOS_Conflict before this destructive action"
174
104
  }
175
105
 
176
- # Schema Triggers (Database operations)
177
- if ($QUERY_LOWER -match '(sql|query|supabase|prisma|database|table|column|select |insert |update |where )') {
178
- $SKILL_REMINDERS += "SCHEMA REQUIRED: Call ekkOS_GetSchema for correct field names"
106
+ # Schema Triggers - Database operations
107
+ if ($queryLower -match '(sql|query|supabase|prisma|database|table|column|select |insert |update |where )') {
108
+ $skillReminders += "SCHEMA REQUIRED: Call ekkOS_GetSchema for correct field names"
179
109
  }
180
110
 
181
- # Secret Triggers (API keys, credentials)
182
- if ($QUERY_LOWER -match '(api key|token|password|credential|secret|my.*key|store.*key)') {
183
- $SKILL_REMINDERS += "SECRETS: Use ekkOS_StoreSecret to securely save credentials"
184
- }
185
-
186
- # Plan Triggers (Complex multi-step tasks)
187
- if ($QUERY_LOWER -match '(implement|build|create.*feature|refactor|migrate|set up|architecture)') {
188
- $SKILL_REMINDERS += "PLAN REQUIRED: Call ekkOS_Plan for complex multi-step tasks"
189
- }
190
-
191
- # Learn Triggers (User expressing success/failure)
192
- if ($QUERY_LOWER -match '(that worked|thanks|perfect|great|awesome|nailed it|solved|fixed it)') {
193
- $SKILL_REMINDERS += "LEARN: Consider calling ekkOS_Forge to capture this solution as a pattern"
194
- }
195
-
196
- # Codebase Triggers (Project-specific code search)
197
- if ($QUERY_LOWER -match '(where is|find.*file|search.*code|in this project|in the codebase)') {
198
- $SKILL_REMINDERS += "CODEBASE: Use ekkOS_Codebase for project-specific code search"
199
- }
200
-
201
- # Combine skill reminders (only take first 3 to avoid noise)
202
- $SKILL_REMINDER = ""
203
- $reminderCount = $SKILL_REMINDERS.Count
204
- if ($reminderCount -gt 0) {
205
- $maxReminders = [Math]::Min(3, $reminderCount)
206
- $SKILL_REMINDER = ($SKILL_REMINDERS | Select-Object -First $maxReminders) -join "`n"
207
- }
208
-
209
- # ═══════════════════════════════════════════════════════════════════════════
210
- # Load auth
211
- # ═══════════════════════════════════════════════════════════════════════════
212
- $EKKOS_CONFIG = Join-Path $env:USERPROFILE ".ekkos\config.json"
213
- $AUTH_TOKEN = ""
214
-
215
- if ((Test-Path $EKKOS_CONFIG) -and (Test-Path $JsonParseHelper)) {
216
- $AUTH_TOKEN = & node $JsonParseHelper $EKKOS_CONFIG ".hookApiKey" 2>$null
217
- if (-not $AUTH_TOKEN) {
218
- $AUTH_TOKEN = & node $JsonParseHelper $EKKOS_CONFIG ".apiKey" 2>$null
219
- }
220
- }
221
-
222
- if (-not $AUTH_TOKEN) {
223
- $envLocalFile = Join-Path $ProjectRoot ".env.local"
224
- if (Test-Path $envLocalFile) {
225
- $envLines = Get-Content $envLocalFile
226
- foreach ($line in $envLines) {
227
- if ($line -match '^SUPABASE_SECRET_KEY=(.+)$') {
228
- $AUTH_TOKEN = $Matches[1].Trim('"', "'", ' ', "`r")
229
- break
230
- }
231
- }
232
- }
233
- }
234
-
235
- $MEMORY_API_URL = "https://mcp.ekkos.dev"
236
-
237
111
  # ═══════════════════════════════════════════════════════════════════════════
238
- # Session ID - NEW ID per conversation (not persisted 24h anymore)
112
+ # SESSION NAME - Resolve early so it's available for all downstream use
239
113
  # ═══════════════════════════════════════════════════════════════════════════
240
- $StateDir = Join-Path $ProjectRoot ".claude\state"
241
- if (-not (Test-Path $StateDir)) {
242
- New-Item -ItemType Directory -Path $StateDir -Force | Out-Null
243
- }
244
- $SESSION_FILE = Join-Path $StateDir "current-session.json"
114
+ function Convert-UuidToWords {
115
+ param([string]$uuid)
245
116
 
246
- $SESSION_ID = $RAW_SESSION_ID
117
+ if (-not $script:SessionWords) { Load-SessionWords }
118
+ if (-not $script:SessionWords) { return "unknown-session" }
247
119
 
248
- # Skip if no valid session ID from Claude
249
- if (-not $SESSION_ID -or $SESSION_ID -eq "unknown" -or $SESSION_ID -eq "null") {
250
- exit 0
251
- }
120
+ $adjectives = $script:SessionWords.adjectives
121
+ $nouns = $script:SessionWords.nouns
122
+ $verbs = $script:SessionWords.verbs
252
123
 
253
- # Save for other hooks to reference
254
- $timestampUtc = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
255
- Set-Content -Path $SESSION_FILE -Value "{`"session_id`": `"$SESSION_ID`", `"timestamp`": `"$timestampUtc`"}" -Force
124
+ if (-not $adjectives -or -not $nouns -or -not $verbs) { return "unknown-session" }
125
+ if (-not $uuid -or $uuid -eq "unknown") { return "unknown-session" }
256
126
 
257
- # ═══════════════════════════════════════════════════════════════════════════
258
- # Turn counter - PROJECT-LOCAL storage
259
- # ═══════════════════════════════════════════════════════════════════════════
260
- $ProjectSessionDir = Join-Path $StateDir "sessions"
261
- if (-not (Test-Path $ProjectSessionDir)) {
262
- New-Item -ItemType Directory -Path $ProjectSessionDir -Force | Out-Null
263
- }
264
- $TURN_COUNTER_FILE = Join-Path $ProjectSessionDir "$SESSION_ID.turn"
127
+ $clean = $uuid -replace "-", ""
128
+ if ($clean.Length -lt 12) { return "unknown-session" }
265
129
 
266
- # Count actual user messages in transcript for accurate turn number
267
- $TURN_NUMBER = 1
268
- if ($TRANSCRIPT_PATH -and (Test-Path $TRANSCRIPT_PATH)) {
269
130
  try {
270
- $transcriptContent = Get-Content $TRANSCRIPT_PATH -Raw
271
- $TURN_NUMBER = ([regex]::Matches($transcriptContent, '"type":"user"')).Count
272
- if ($TURN_NUMBER -eq 0) { $TURN_NUMBER = 1 }
131
+ $a = [Convert]::ToInt32($clean.Substring(0,4), 16) % $adjectives.Length
132
+ $n = [Convert]::ToInt32($clean.Substring(4,4), 16) % $nouns.Length
133
+ $an = [Convert]::ToInt32($clean.Substring(8,4), 16) % $verbs.Length
134
+ return "$($adjectives[$a])-$($nouns[$n])-$($verbs[$an])"
273
135
  } catch {
274
- $TURN_NUMBER = 1
136
+ return "unknown-session"
275
137
  }
276
138
  }
277
139
 
278
- # PRESERVE HISTORY: Don't overwrite if saved count is higher (after /clear)
279
- $SAVED_TURN_COUNT = 0
280
- $TRANSCRIPT_TURN_COUNT = $TURN_NUMBER
281
- $POST_CLEAR_DETECTED = $false
282
-
283
- if (Test-Path $TURN_COUNTER_FILE) {
284
- try {
285
- $SAVED_TURN_COUNT = [int](Get-Content $TURN_COUNTER_FILE -Raw).Trim()
286
- } catch {
287
- $SAVED_TURN_COUNT = 0
288
- }
289
- }
290
-
291
- if ($SAVED_TURN_COUNT -gt $TURN_NUMBER) {
292
- # Post-clear: INCREMENT from saved count
293
- $TURN_NUMBER = $SAVED_TURN_COUNT + 1
294
- $POST_CLEAR_DETECTED = $true
295
- }
296
- Set-Content -Path $TURN_COUNTER_FILE -Value "$TURN_NUMBER" -Force
140
+ $sessionName = Convert-UuidToWords $rawSessionId
297
141
 
298
142
  # ═══════════════════════════════════════════════════════════════════════════
299
- # WORKING MEMORY: Fast capture each turn (async, non-blocking)
143
+ # PROXY SESSION BIND: _pending real session name (fires every turn)
144
+ # Mirrors bash user-prompt-submit.sh lines 319-338.
145
+ # No PTY on Windows so run.ts can't detect session name — hook must bind it.
300
146
  # ═══════════════════════════════════════════════════════════════════════════
301
- if (Test-Path $EKKOS_CONFIG) {
302
- $CAPTURE_TOKEN = ""
303
- if (Test-Path $JsonParseHelper) {
304
- $CAPTURE_TOKEN = & node $JsonParseHelper $EKKOS_CONFIG ".hookApiKey" 2>$null
305
- if (-not $CAPTURE_TOKEN) {
306
- $CAPTURE_TOKEN = & node $JsonParseHelper $EKKOS_CONFIG ".apiKey" 2>$null
307
- }
308
- }
309
-
310
- if ($CAPTURE_TOKEN -and $CAPTURE_TOKEN -ne "null") {
311
- # Async capture to Redis/Supabase - doesn't block hook execution
312
- $queryEscaped = $USER_QUERY -replace '"', '\"' -replace '\\', '\\\\'
313
- $captureBody = "{`"session_id`":`"$RAW_SESSION_ID`",`"turn`":$TURN_NUMBER,`"query`":`"$queryEscaped`"}"
314
- Start-Job -ScriptBlock {
315
- param($url, $token, $body)
316
- try {
317
- Invoke-RestMethod -Uri "$url/api/v1/working/fast-capture" `
318
- -Method POST `
319
- -Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
320
- -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
321
- -TimeoutSec 3 `
322
- -ErrorAction SilentlyContinue | Out-Null
323
- } catch {}
324
- } -ArgumentList $MEMORY_API_URL, $CAPTURE_TOKEN, $captureBody | Out-Null
147
+ if ($sessionName -ne "unknown-session" -and $rawSessionId -ne "unknown") {
148
+ $configFile = Join-Path $EkkosConfigDir "config.json"
149
+ if (Test-Path $configFile) {
150
+ try {
151
+ $config = Get-Content $configFile -Raw | ConvertFrom-Json
152
+ $userId = $config.userId
153
+ if ($userId) {
154
+ $projectPath = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
155
+ $pendingSession = if ($env:EKKOS_PENDING_SESSION) { $env:EKKOS_PENDING_SESSION } else { "_pending" }
156
+ $projectPath = $projectPath -replace '\\', '/'
157
+ $bindBody = @{
158
+ userId = $userId
159
+ realSession = $sessionName
160
+ projectPath = $projectPath
161
+ pendingSession = $pendingSession
162
+ } | ConvertTo-Json -Depth 10 -Compress
163
+
164
+ Start-Job -ScriptBlock {
165
+ param($body)
166
+ Invoke-RestMethod -Uri "https://mcp.ekkos.dev/proxy/session/bind" `
167
+ -Method POST `
168
+ -Headers @{ "Content-Type" = "application/json" } `
169
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue | Out-Null
170
+ } -ArgumentList $bindBody | Out-Null
171
+ }
172
+ } catch {}
325
173
  }
326
174
  }
327
175
 
328
176
  # ═══════════════════════════════════════════════════════════════════════════
329
- # LOCAL CACHE: Tier 0 capture for instant /continue (async, non-blocking)
177
+ # SESSION CURRENT: Update Redis with current session name
330
178
  # ═══════════════════════════════════════════════════════════════════════════
331
- $ekkosCapturePath = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
332
- if ($ekkosCapturePath) {
333
- $SESSION_NAME_FOR_CAPTURE = Convert-UuidToWords $RAW_SESSION_ID
334
- $projectRootNorm = $ProjectRoot -replace '\\', '/'
335
- Start-Job -ScriptBlock {
336
- param($sessionId, $sessionName, $turnNum, $query, $projRoot)
179
+ if ($sessionName -ne "unknown-session" -and $rawSessionId -ne "unknown") {
180
+ $configFile2 = Join-Path $EkkosConfigDir "config.json"
181
+ if (Test-Path $configFile2) {
337
182
  try {
338
- & ekkos-capture user $sessionId $sessionName $turnNum $query $projRoot 2>$null | Out-Null
183
+ $config2 = Get-Content $configFile2 -Raw | ConvertFrom-Json
184
+ $sessionToken = $config2.hookApiKey
185
+ if (-not $sessionToken) { $sessionToken = $config2.apiKey }
186
+ if ($sessionToken) {
187
+ $sessionBody = @{ session_name = $sessionName } | ConvertTo-Json -Depth 10
188
+ Start-Job -ScriptBlock {
189
+ param($body, $token)
190
+ Invoke-RestMethod -Uri "https://mcp.ekkos.dev/api/v1/working/session/current" `
191
+ -Method POST `
192
+ -Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
193
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue | Out-Null
194
+ } -ArgumentList $sessionBody, $sessionToken | Out-Null
195
+ }
339
196
  } catch {}
340
- } -ArgumentList $RAW_SESSION_ID, $SESSION_NAME_FOR_CAPTURE, $TURN_NUMBER, $USER_QUERY, $projectRootNorm | Out-Null
341
- }
342
-
343
- # ═══════════════════════════════════════════════════════════════════════════
344
- # GOLDEN LOOP: CAPTURE PHASE - Track turn start
345
- # ═══════════════════════════════════════════════════════════════════════════
346
- $EkkosDir = Join-Path $ProjectRoot ".ekkos"
347
- if (-not (Test-Path $EkkosDir)) {
348
- New-Item -ItemType Directory -Path $EkkosDir -Force | Out-Null
349
- }
350
- $GOLDEN_LOOP_FILE = Join-Path $EkkosDir "golden-loop-current.json"
351
-
352
- # Write current phase to file (extension watches this for real-time updates)
353
- $glPathEscaped = $GOLDEN_LOOP_FILE -replace '\\', '\\\\'
354
- try {
355
- node -e "
356
- const fs = require('fs');
357
- const data = {
358
- phase: 'capture',
359
- turn: $TURN_NUMBER,
360
- session: '$SESSION_ID',
361
- timestamp: new Date().toISOString(),
362
- stats: { retrieved: 0, applied: 0, forged: 0 }
363
- };
364
- fs.writeFileSync('$glPathEscaped', JSON.stringify(data, null, 2));
365
- " 2>$null
366
- } catch {}
367
-
368
- # ═══════════════════════════════════════════════════════════════════════════
369
- # GOLDEN LOOP: RETRIEVE PHASE - Auto-retrieve patterns from ekkOS
370
- # ═══════════════════════════════════════════════════════════════════════════
371
- $EKKOS_API_KEY = ""
372
- $hookApiKeyFile = Join-Path $env:USERPROFILE ".ekkos\.hookApiKey"
373
- if (Test-Path $hookApiKeyFile) {
374
- $EKKOS_API_KEY = (Get-Content $hookApiKeyFile -Raw).Trim()
375
- } elseif ((Test-Path $EKKOS_CONFIG) -and (Test-Path $JsonParseHelper)) {
376
- $EKKOS_API_KEY = & node $JsonParseHelper $EKKOS_CONFIG ".hookApiKey" 2>$null
197
+ }
377
198
  }
378
199
 
379
- $RETRIEVED_PATTERNS = ""
380
- $PATTERN_COUNT = 0
381
- $RETRIEVED_DIRECTIVES = ""
382
- $DIRECTIVE_COUNT = 0
383
-
384
200
  # ═══════════════════════════════════════════════════════════════════════════
385
- # DIRECTIVE CACHE: Local cache to avoid API calls every turn
201
+ # TURN TRACKING & STATE MANAGEMENT
386
202
  # ═══════════════════════════════════════════════════════════════════════════
387
- $DirectiveCacheDir = Join-Path $env:USERPROFILE ".ekkos\cache"
388
- $DirectiveCacheFile = Join-Path $DirectiveCacheDir "directives.json"
389
- $DIRECTIVE_CACHE_TTL = 3600 # 1 hour in seconds
390
- if (-not (Test-Path $DirectiveCacheDir)) {
391
- New-Item -ItemType Directory -Path $DirectiveCacheDir -Force | Out-Null
392
- }
393
-
394
- # Check if we need to refresh directive cache
395
- $DIRECTIVE_CACHE_VALID = $false
396
- $DIRECTIVE_TRIGGER_DETECTED = $false
397
-
398
- if ($QUERY_LOWER -match '(always |never |i prefer|i like |dont |don.t |avoid |remember that |from now on|directive|preference)') {
399
- $DIRECTIVE_TRIGGER_DETECTED = $true
203
+ $stateDir = Join-Path $env:USERPROFILE ".claude\state"
204
+ if (-not (Test-Path $stateDir)) {
205
+ New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
400
206
  }
401
207
 
402
- if (Test-Path $DirectiveCacheFile) {
403
- $cacheContent = Get-Content $DirectiveCacheFile -Raw
404
- $cacheTimestampStr = Parse-JsonValue $cacheContent ".cached_at"
405
- if ($cacheTimestampStr -match '^\d+$') {
406
- $cacheTimestamp = [int64]$cacheTimestampStr
407
- $currentTimestamp = [int64]([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())
408
- $cacheAge = $currentTimestamp - $cacheTimestamp
208
+ $stateFile = Join-Path $stateDir "hook-state.json"
209
+ $turn = 0
210
+ $contextPercent = ""
409
211
 
410
- if ($cacheAge -lt $DIRECTIVE_CACHE_TTL -and -not $DIRECTIVE_TRIGGER_DETECTED) {
411
- $DIRECTIVE_CACHE_VALID = $true
212
+ if (Test-Path $stateFile) {
213
+ try {
214
+ $hookState = Get-Content $stateFile -Raw | ConvertFrom-Json
215
+ # Only continue incrementing if this state belongs to the SAME session.
216
+ # If session changed, reset turn counter to 0.
217
+ if ($hookState.session_id -eq $rawSessionId) {
218
+ $turn = [int]$hookState.turn + 1
219
+ } else {
220
+ $turn = 0
412
221
  }
222
+ } catch {
223
+ $turn = 0
413
224
  }
414
225
  }
415
226
 
416
- # Decide whether to inject directives this turn
417
- $SHOULD_INJECT_DIRECTIVES = $false
418
- if ($TURN_NUMBER -eq 1 -or $POST_CLEAR_DETECTED -or $DIRECTIVE_TRIGGER_DETECTED) {
419
- $SHOULD_INJECT_DIRECTIVES = $true
420
- }
421
-
422
- if ($EKKOS_API_KEY -and $USER_QUERY) {
423
- # Update phase to RETRIEVE
424
- try {
425
- node -e "
426
- const fs = require('fs');
427
- const data = {
428
- phase: 'retrieve',
429
- turn: $TURN_NUMBER,
430
- session: '$SESSION_ID',
431
- timestamp: new Date().toISOString(),
432
- stats: { retrieved: 0, applied: 0, forged: 0 }
433
- };
434
- fs.writeFileSync('$glPathEscaped', JSON.stringify(data, null, 2));
435
- " 2>$null
436
- } catch {}
437
-
438
- # Build sources array - always include patterns, conditionally include directives
439
- if ($DIRECTIVE_CACHE_VALID) {
440
- $searchSources = '["patterns"]'
441
- } else {
442
- $searchSources = '["patterns", "directives"]'
443
- }
227
+ # Save updated state
228
+ $newState = @{
229
+ turn = $turn
230
+ session_id = $rawSessionId
231
+ last_query = $userQuery.Substring(0, [Math]::Min(100, $userQuery.Length))
232
+ timestamp = (Get-Date).ToString("o")
233
+ } | ConvertTo-Json -Depth 10
444
234
 
445
- # Escape user query for JSON
446
- $queryJsonEscaped = $USER_QUERY -replace '\\', '\\\\' -replace '"', '\"' -replace "`n", '\n' -replace "`r", '' -replace "`t", '\t'
235
+ Set-Content -Path $stateFile -Value $newState -Force
447
236
 
448
- # Call ekkOS MCP gateway
449
- $SEARCH_RESPONSE_RAW = $null
237
+ # ═══════════════════════════════════════════════════════════════════════════
238
+ # LOCAL CACHE: Tier 0 capture (async, non-blocking)
239
+ # Per v1.2 ADDENDUM: Pass instanceId for namespacing
240
+ # ═══════════════════════════════════════════════════════════════════════════
241
+ $captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
242
+ if ($captureCmd -and $rawSessionId -ne "unknown") {
450
243
  try {
451
- $searchBody = "{`"tool`": `"ekkOS_Search`", `"arguments`": {`"query`": `"$queryJsonEscaped`", `"limit`": 5, `"sources`": $searchSources}}"
452
- $SEARCH_RESPONSE_RAW = Invoke-RestMethod -Uri "https://api.ekkos.dev/api/v1/mcp/call" `
453
- -Method POST `
454
- -Headers @{ Authorization = "Bearer $EKKOS_API_KEY"; "Content-Type" = "application/json" } `
455
- -Body ([System.Text.Encoding]::UTF8.GetBytes($searchBody)) `
456
- -TimeoutSec 2 `
457
- -ErrorAction Stop
458
- } catch {
459
- $SEARCH_RESPONSE_RAW = $null
460
- }
461
-
462
- if ($SEARCH_RESPONSE_RAW) {
463
- # Convert to JSON string for Node parsing
464
- $SEARCH_JSON = ""
465
- if ($SEARCH_RESPONSE_RAW -is [string]) {
466
- $SEARCH_JSON = $SEARCH_RESPONSE_RAW
467
- } else {
468
- try {
469
- $SEARCH_JSON = $SEARCH_RESPONSE_RAW | ConvertTo-Json -Depth 20 -Compress
470
- } catch {
471
- $SEARCH_JSON = '{}'
472
- }
473
- }
244
+ # NEW format: ekkos-capture user <instance_id> <session_id> <session_name> <turn_id> <query> [project_path]
245
+ $queryBase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($userQuery))
246
+ $projectRoot = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
474
247
 
475
- # Count patterns retrieved
476
- $patternCountStr = $SEARCH_JSON | node -e "
477
- const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
478
- const patterns = (d.result && d.result.results && d.result.results.patterns) || [];
479
- console.log(patterns.length);
480
- " 2>$null
481
- if ($patternCountStr -match '^\d+$') {
482
- $PATTERN_COUNT = [int]$patternCountStr
483
- }
484
-
485
- # Update golden loop with retrieved count
486
- if ($PATTERN_COUNT -gt 0) {
248
+ Start-Job -ScriptBlock {
249
+ param($instanceId, $sessionId, $sessionName, $turnNum, $queryB64, $projectPath)
487
250
  try {
488
- node -e "
489
- const fs = require('fs');
490
- const data = {
491
- phase: 'inject',
492
- turn: $TURN_NUMBER,
493
- session: '$SESSION_ID',
494
- timestamp: new Date().toISOString(),
495
- stats: { retrieved: $PATTERN_COUNT, applied: 0, forged: 0 }
496
- };
497
- fs.writeFileSync('$glPathEscaped', JSON.stringify(data, null, 2));
498
- " 2>$null
251
+ $decoded = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($queryB64))
252
+ & ekkos-capture user $instanceId $sessionId $sessionName $turnNum $decoded $projectPath 2>&1 | Out-Null
499
253
  } catch {}
500
-
501
- # Format patterns for injection
502
- $RETRIEVED_PATTERNS = $SEARCH_JSON | node -e "
503
- const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
504
- const patterns = (d.result && d.result.results && d.result.results.patterns) || [];
505
- patterns.forEach(p => {
506
- console.log('**' + (p.title || '') + '**');
507
- console.log(p.problem || p.guidance || '');
508
- console.log('');
509
- console.log('## Solution');
510
- console.log(p.solution || p.content || '');
511
- console.log('');
512
- console.log('Success Rate: ' + ((p.success_rate || 0) * 100) + '%');
513
- console.log('Applied: ' + (p.applied_count || 0) + ' times');
514
- console.log('');
515
- });
516
- " 2>$null
517
- }
518
-
519
- # ═══════════════════════════════════════════════════════════════════
520
- # DIRECTIVE HANDLING: Use cache if valid, otherwise process response
521
- # ═══════════════════════════════════════════════════════════════════
522
- if ($DIRECTIVE_CACHE_VALID) {
523
- # Load directives from cache
524
- $cacheContent = Get-Content $DirectiveCacheFile -Raw
525
- $dcStr = Parse-JsonValue $cacheContent ".count"
526
- if ($dcStr -match '^\d+$') { $DIRECTIVE_COUNT = [int]$dcStr }
527
- $RETRIEVED_DIRECTIVES = Parse-JsonValue $cacheContent ".formatted"
528
- } else {
529
- # Extract and format DIRECTIVES from API response
530
- $directiveResult = $SEARCH_JSON | node -e "
531
- const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
532
- const directives = (d.result && d.result.results && d.result.results.directives) || [];
533
- const count = directives.length;
534
- if (count === 0) { console.log(JSON.stringify({count: 0, formatted: ''})); process.exit(0); }
535
-
536
- const grouped = {MUST: [], NEVER: [], PREFER: [], AVOID: []};
537
- directives.forEach(d => {
538
- if (grouped[d.type]) grouped[d.type].push(d.rule);
539
- });
540
-
541
- let formatted = 'USER DIRECTIVES (FOLLOW THESE):';
542
- for (const [type, rules] of Object.entries(grouped)) {
543
- if (rules.length > 0) {
544
- formatted += '\n\n' + type + ':';
545
- rules.forEach(r => { formatted += '\n - ' + r; });
546
- }
547
- }
548
-
549
- console.log(JSON.stringify({count, formatted}));
550
-
551
- // Save to cache
552
- const cacheData = {count, formatted, cached_at: Math.floor(Date.now() / 1000)};
553
- try {
554
- const fs = require('fs');
555
- fs.mkdirSync(process.argv[1], {recursive: true});
556
- fs.writeFileSync(process.argv[2], JSON.stringify(cacheData));
557
- } catch(e) {}
558
- " -- "$DirectiveCacheDir" "$DirectiveCacheFile" 2>$null
559
-
560
- if ($directiveResult) {
561
- $dcStr = $directiveResult | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')||'{}');console.log(d.count||0)" 2>$null
562
- if ($dcStr -match '^\d+$') { $DIRECTIVE_COUNT = [int]$dcStr }
563
- $RETRIEVED_DIRECTIVES = $directiveResult | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')||'{}');console.log(d.formatted||'')" 2>$null
564
- }
565
- }
566
- }
254
+ } -ArgumentList $EkkosInstanceId, $rawSessionId, $sessionName, $turn, $queryBase64, $projectRoot | Out-Null
255
+ } catch {}
567
256
  }
568
257
 
569
258
  # ═══════════════════════════════════════════════════════════════════════════
570
- # COLORS (ANSI escape sequences via [char]27)
259
+ # WORKING MEMORY: Fast capture to API (async, non-blocking)
571
260
  # ═══════════════════════════════════════════════════════════════════════════
572
- $ESC = [char]27
573
- $CYAN = "$ESC[0;36m"
574
- $GREEN = "$ESC[0;32m"
575
- $YELLOW = "$ESC[1;33m"
576
- $MAGENTA = "$ESC[0;35m"
577
- $DIM = "$ESC[2m"
578
- $BOLD = "$ESC[1m"
579
- $RESET = "$ESC[0m"
580
-
581
- $CURRENT_TIME = Get-Date -Format "yyyy-MM-dd hh:mm:ss tt K"
582
-
583
- # Generate session name
584
- $SESSION_NAME = ""
585
- if ($SESSION_ID -and $SESSION_ID -ne "unknown" -and $SESSION_ID -ne "null") {
586
- $SESSION_NAME = Convert-UuidToWords $SESSION_ID
261
+ $configFile = Join-Path $EkkosConfigDir "config.json"
262
+ if (Test-Path $configFile) {
263
+ try {
264
+ $config = Get-Content $configFile -Raw | ConvertFrom-Json
265
+ $captureToken = $config.hookApiKey
266
+ if (-not $captureToken) { $captureToken = $config.apiKey }
267
+
268
+ if ($captureToken) {
269
+ # Async capture using Start-Job (non-blocking)
270
+ Start-Job -ScriptBlock {
271
+ param($token, $instanceId, $sessionId, $sessionName, $turnNum, $query)
272
+ $body = @{
273
+ session_id = $sessionId
274
+ session_name = $sessionName
275
+ instance_id = $instanceId
276
+ turn = $turnNum
277
+ query = $query
278
+ } | ConvertTo-Json -Depth 10
279
+
280
+ Invoke-RestMethod -Uri "https://mcp.ekkos.dev/api/v1/working/fast-capture" `
281
+ -Method POST `
282
+ -Headers @{ Authorization = "Bearer $token" } `
283
+ -ContentType "application/json" `
284
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue
285
+ } -ArgumentList $captureToken, $EkkosInstanceId, $rawSessionId, $sessionName, $turn, $userQuery | Out-Null
286
+ }
287
+ } catch {}
587
288
  }
588
289
 
290
+ $timestamp = (Get-Date).ToString("yyyy-MM-dd hh:mm:ss tt") + " EST"
291
+
589
292
  # ═══════════════════════════════════════════════════════════════════════════
590
- # "/continue" COMMAND: Delegated to Skill system (DO NOT INTERCEPT)
293
+ # DASHBOARD HINT FILE: write session info for ekkos dashboard --wait-for-new
294
+ # On Windows, active-sessions.json is never populated (hook PIDs are dead).
295
+ # The dashboard reads this file instead to locate the JSONL path.
591
296
  # ═══════════════════════════════════════════════════════════════════════════
592
- # REMOVED: Hook used to intercept /continue and do simple restoration
593
- # NOW: Let /continue Skill handle it - supports session names + intelligent narrative
297
+ if ($sessionName -ne "unknown-session") {
298
+ try {
299
+ $projectPath = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
300
+ $hintFile = Join-Path $EkkosConfigDir "hook-session-hint.json"
301
+ $hint = @{
302
+ sessionName = $sessionName
303
+ sessionId = $rawSessionId
304
+ projectPath = $projectPath
305
+ ts = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
306
+ } | ConvertTo-Json -Depth 10 -Compress
307
+ Set-Content -Path $hintFile -Value $hint -Force
308
+ } catch {}
309
+ }
594
310
 
595
311
  # ═══════════════════════════════════════════════════════════════════════════
596
- # AUTO-RESTORE REMOVED: Manual /continue only (saves 79% token burn!)
312
+ # OUTPUT SYSTEM REMINDER
597
313
  # ═══════════════════════════════════════════════════════════════════════════
314
+ $esc = [char]27
315
+ $header = "${esc}[0;36m${esc}[1m🧠 ekkOS Memory${esc}[0m ${esc}[2m| $sessionName | $timestamp${esc}[0m"
598
316
 
599
- # Simple status line - no context warnings, Claude handles its own context
600
- Write-Output "${CYAN}${BOLD}ekkOS Memory${RESET} ${DIM}| Turn ${TURN_NUMBER} | ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
317
+ $output = @"
318
+ $header
601
319
 
602
- # Output skill reminder if detected
603
- if ($SKILL_REMINDER) {
604
- Write-Output ""
605
- Write-Output "${MAGENTA}${BOLD}${SKILL_REMINDER}${RESET}"
606
- }
320
+ "@
607
321
 
608
- # GOLDEN LOOP: INJECT PHASE - Inject directives FIRST (highest priority)
609
- # SMART INJECTION: Only on Turn 1, post-clear, or directive trigger
610
- if ($SHOULD_INJECT_DIRECTIVES -and $RETRIEVED_DIRECTIVES -and $DIRECTIVE_COUNT -gt 0) {
611
- Write-Output ""
612
- Write-Output "<system-reminder>"
613
- Write-Output "$RETRIEVED_DIRECTIVES"
614
- Write-Output "</system-reminder>"
322
+ if ($skillReminders.Count -gt 0) {
323
+ $output += "${esc}[0;35m${esc}[1m" + ($skillReminders -join "`n") + "${esc}[0m`n"
615
324
  }
616
325
 
617
- # GOLDEN LOOP: INJECT PHASE - Inject retrieved patterns into context
618
- if ($RETRIEVED_PATTERNS -and $PATTERN_COUNT -gt 0) {
619
- Write-Output ""
620
- Write-Output "<system-reminder>"
621
- Write-Output "RETRIEVED PATTERNS FROM ekkOS MEMORY ($PATTERN_COUNT patterns found)"
622
- Write-Output ""
623
- Write-Output "$RETRIEVED_PATTERNS"
624
- Write-Output ""
625
- Write-Output "These patterns were automatically retrieved based on your query. Review and apply relevant solutions."
626
- Write-Output "</system-reminder>"
627
- Write-Output ""
628
- Write-Output "MANDATORY PATTERN ACKNOWLEDGMENT (PatternGuard):"
629
- Write-Output "You MUST acknowledge ALL $PATTERN_COUNT patterns above - either SELECT or SKIP each one."
630
- Write-Output ""
631
- Write-Output "For patterns you USE:"
632
- Write-Output "[ekkOS_SELECT]"
633
- Write-Output "- id: <pattern_id>"
634
- Write-Output " reason: <1-line why using>"
635
- Write-Output " confidence: <0.0-1.0>"
636
- Write-Output "[/ekkOS_SELECT]"
637
- Write-Output ""
638
- Write-Output "For patterns NOT relevant:"
639
- Write-Output "[ekkOS_SKIP]"
640
- Write-Output "- id: <pattern_id>"
641
- Write-Output " reason: <1-line why not relevant>"
642
- Write-Output "[/ekkOS_SKIP]"
643
- Write-Output ""
644
- Write-Output "AUTOMATIC APPLICATION: When pattern matches your task, APPLY IT immediately without asking."
645
- Write-Output "Track outcome after: ekkOS_Outcome({success: true/false})"
646
- Write-Output ""
647
- Write-Output "100% coverage required. This is how the system learns what works."
648
- }
326
+ $output += @"
649
327
 
650
- # Inject footer format reminder (helps Claude remember session name)
651
- if ($SESSION_NAME -and $SESSION_NAME -ne "unknown-session") {
652
- Write-Output ""
653
- Write-Output "<footer-format>End responses with: Claude Code ({Model}) · ekkOS_ · Turn ${TURN_NUMBER} · ${SESSION_NAME} · ${CURRENT_TIME}</footer-format>"
654
- }
328
+ <footer-format>End responses with: Claude Code ({Model}) · 🧠 ekkOS_™ · $sessionName · $timestamp</footer-format>
329
+ <footer-note>Do not include a turn counter in the footer.</footer-note>
330
+ "@
655
331
 
656
- exit 0
332
+ Write-Output $output