@ekkos/cli 1.0.34 → 1.0.35

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 (51) hide show
  1. package/dist/capture/jsonl-rewriter.js +72 -7
  2. package/dist/commands/dashboard.js +186 -557
  3. package/dist/commands/init.js +3 -15
  4. package/dist/commands/run.js +222 -256
  5. package/dist/commands/setup.js +0 -47
  6. package/dist/commands/swarm-dashboard.js +4 -13
  7. package/dist/deploy/instructions.d.ts +2 -5
  8. package/dist/deploy/instructions.js +8 -11
  9. package/dist/deploy/settings.js +21 -15
  10. package/dist/deploy/skills.d.ts +0 -8
  11. package/dist/deploy/skills.js +0 -26
  12. package/dist/index.js +2 -2
  13. package/dist/lib/usage-parser.js +1 -2
  14. package/dist/utils/platform.d.ts +0 -3
  15. package/dist/utils/platform.js +1 -4
  16. package/dist/utils/session-binding.d.ts +1 -1
  17. package/dist/utils/session-binding.js +2 -3
  18. package/package.json +4 -2
  19. package/templates/CLAUDE.md +23 -135
  20. package/templates/agents/README.md +182 -0
  21. package/templates/agents/code-reviewer.md +166 -0
  22. package/templates/agents/debug-detective.md +169 -0
  23. package/templates/agents/ekkOS_Vercel.md +99 -0
  24. package/templates/agents/extension-manager.md +229 -0
  25. package/templates/agents/git-companion.md +185 -0
  26. package/templates/agents/github-test-agent.md +321 -0
  27. package/templates/agents/railway-manager.md +179 -0
  28. package/templates/ekkos-manifest.json +8 -8
  29. package/templates/hooks/assistant-response.ps1 +160 -256
  30. package/templates/hooks/assistant-response.sh +66 -130
  31. package/templates/hooks/hooks.json +0 -6
  32. package/templates/hooks/lib/contract.sh +31 -43
  33. package/templates/hooks/lib/count-tokens.cjs +0 -0
  34. package/templates/hooks/lib/ekkos-reminders.sh +0 -0
  35. package/templates/hooks/lib/state.sh +1 -53
  36. package/templates/hooks/session-start.ps1 +391 -91
  37. package/templates/hooks/session-start.sh +166 -201
  38. package/templates/hooks/stop.ps1 +341 -202
  39. package/templates/hooks/stop.sh +948 -275
  40. package/templates/hooks/user-prompt-submit.ps1 +548 -224
  41. package/templates/hooks/user-prompt-submit.sh +456 -382
  42. package/templates/plan-template.md +0 -0
  43. package/templates/spec-template.md +0 -0
  44. package/templates/windsurf-hooks/before-submit-prompt.sh +238 -0
  45. package/templates/windsurf-hooks/hooks.json +2 -9
  46. package/templates/windsurf-hooks/install.sh +0 -0
  47. package/templates/windsurf-hooks/lib/contract.sh +0 -2
  48. package/templates/windsurf-hooks/post-cascade-response.sh +0 -0
  49. package/templates/windsurf-hooks/pre-user-prompt.sh +0 -0
  50. package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -0
  51. package/README.md +0 -57
@@ -1,332 +1,656 @@
1
1
  # ═══════════════════════════════════════════════════════════════════════════
2
2
  # ekkOS_ Hook: UserPromptSubmit - SEAMLESS CONTEXT CONTINUITY (Windows)
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
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
11
9
  # ═══════════════════════════════════════════════════════════════════════════
12
10
 
13
11
  $ErrorActionPreference = "SilentlyContinue"
14
12
 
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"
13
+ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
14
+ $ProjectRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
21
15
 
22
16
  # ═══════════════════════════════════════════════════════════════════════════
23
- # INSTANCE ID - Multi-session isolation per v1.2 ADDENDUM
17
+ # CONFIG PATHS - Per spec v1.2 Addendum
24
18
  # ═══════════════════════════════════════════════════════════════════════════
25
- $EkkosInstanceId = if ($env:EKKOS_INSTANCE_ID) { $env:EKKOS_INSTANCE_ID } else { "default" }
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"
26
23
 
27
24
  # ═══════════════════════════════════════════════════════════════════════════
28
- # Load session words from JSON file - NO HARDCODED ARRAYS
25
+ # WORD-BASED SESSION NAMES - Uses external session-words.json
29
26
  # ═══════════════════════════════════════════════════════════════════════════
30
- $script:SessionWords = $null
27
+ $script:ADJECTIVES = @()
28
+ $script:NOUNS = @()
29
+ $script:VERBS = @()
30
+ $script:SESSION_WORDS_LOADED = $false
31
31
 
32
32
  function Load-SessionWords {
33
- $wordsFile = $SessionWordsJson
33
+ if ($script:SESSION_WORDS_LOADED) { return }
34
34
 
35
- # Fallback to managed defaults if user file missing/invalid
35
+ $wordsFile = $SessionWordsJson
36
36
  if (-not (Test-Path $wordsFile)) {
37
37
  $wordsFile = $SessionWordsDefault
38
38
  }
39
39
 
40
- if (-not (Test-Path $wordsFile)) {
41
- return $null
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
42
45
  }
43
46
 
44
47
  try {
45
- $script:SessionWords = Get-Content $wordsFile -Raw | ConvertFrom-Json
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
46
65
  } catch {
47
- return $null
66
+ $script:ADJECTIVES = @("unknown")
67
+ $script:NOUNS = @("session")
68
+ $script:VERBS = @("starts")
48
69
  }
49
70
  }
50
71
 
51
- # Read input from stdin
52
- $inputJson = [Console]::In.ReadToEnd()
53
- if (-not $inputJson) { exit 0 }
72
+ function Convert-UuidToWords {
73
+ param([string]$uuid)
54
74
 
55
- try {
56
- $input = $inputJson | ConvertFrom-Json
57
- } catch {
58
- exit 0
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
+ }
95
+
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
+ }
59
120
  }
60
121
 
61
- $userQuery = $input.query
62
- if (-not $userQuery) { $userQuery = $input.message }
63
- if (-not $userQuery) { $userQuery = $input.prompt }
64
- if (-not $userQuery) { exit 0 }
122
+ # ═══════════════════════════════════════════════════════════════════════════
123
+ # Read input from stdin
124
+ # ═══════════════════════════════════════════════════════════════════════════
125
+ $INPUT = [Console]::In.ReadToEnd()
126
+ if (-not $INPUT) { exit 0 }
65
127
 
66
- $rawSessionId = $input.session_id
67
- if (-not $rawSessionId -or $rawSessionId -eq "null") { $rawSessionId = "unknown" }
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 }
68
132
 
69
- # Fallback: read session_id from saved state
70
- if ($rawSessionId -eq "unknown") {
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") {
71
139
  $stateFile = Join-Path $env:USERPROFILE ".claude\state\current-session.json"
72
140
  if (Test-Path $stateFile) {
73
- try {
74
- $state = Get-Content $stateFile -Raw | ConvertFrom-Json
75
- $rawSessionId = $state.session_id
76
- } catch {}
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
+ }
77
146
  }
78
147
  }
79
148
 
80
149
  # ═══════════════════════════════════════════════════════════════════════════
81
150
  # INTELLIGENT TOOL ROUTER: Multi-trigger skill detection
151
+ # Detects ALL applicable skills/tools and injects as system reminder
82
152
  # ═══════════════════════════════════════════════════════════════════════════
83
- $skillReminders = @()
84
- $queryLower = $userQuery.ToLower()
153
+ $SKILL_REMINDERS = @()
154
+ $QUERY_LOWER = $USER_QUERY.ToLower()
85
155
 
86
156
  # Memory First - Debug/Error/Problem solving
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"
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"
89
159
  }
90
160
 
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"
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"
94
164
  }
95
165
 
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"
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"
99
169
  }
100
170
 
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"
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"
104
174
  }
105
175
 
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"
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"
179
+ }
180
+
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"
109
207
  }
110
208
 
111
209
  # ═══════════════════════════════════════════════════════════════════════════
112
- # SESSION NAME - Resolve early so it's available for all downstream use
210
+ # Load auth
113
211
  # ═══════════════════════════════════════════════════════════════════════════
114
- function Convert-UuidToWords {
115
- param([string]$uuid)
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
+ }
116
234
 
117
- if (-not $script:SessionWords) { Load-SessionWords }
118
- if (-not $script:SessionWords) { return "unknown-session" }
235
+ $MEMORY_API_URL = "https://mcp.ekkos.dev"
119
236
 
120
- $adjectives = $script:SessionWords.adjectives
121
- $nouns = $script:SessionWords.nouns
122
- $verbs = $script:SessionWords.verbs
237
+ # ═══════════════════════════════════════════════════════════════════════════
238
+ # Session ID - NEW ID per conversation (not persisted 24h anymore)
239
+ # ═══════════════════════════════════════════════════════════════════════════
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"
245
+
246
+ $SESSION_ID = $RAW_SESSION_ID
247
+
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
+ }
123
252
 
124
- if (-not $adjectives -or -not $nouns -or -not $verbs) { return "unknown-session" }
125
- if (-not $uuid -or $uuid -eq "unknown") { return "unknown-session" }
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
126
256
 
127
- $clean = $uuid -replace "-", ""
128
- if ($clean.Length -lt 12) { return "unknown-session" }
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"
129
265
 
266
+ # Count actual user messages in transcript for accurate turn number
267
+ $TURN_NUMBER = 1
268
+ if ($TRANSCRIPT_PATH -and (Test-Path $TRANSCRIPT_PATH)) {
130
269
  try {
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])"
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 }
135
273
  } catch {
136
- return "unknown-session"
274
+ $TURN_NUMBER = 1
137
275
  }
138
276
  }
139
277
 
140
- $sessionName = Convert-UuidToWords $rawSessionId
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
141
297
 
142
298
  # ═══════════════════════════════════════════════════════════════════════════
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.
299
+ # WORKING MEMORY: Fast capture each turn (async, non-blocking)
146
300
  # ═══════════════════════════════════════════════════════════════════════════
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 {}
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
173
325
  }
174
326
  }
175
327
 
176
328
  # ═══════════════════════════════════════════════════════════════════════════
177
- # SESSION CURRENT: Update Redis with current session name
329
+ # LOCAL CACHE: Tier 0 capture for instant /continue (async, non-blocking)
178
330
  # ═══════════════════════════════════════════════════════════════════════════
179
- if ($sessionName -ne "unknown-session" -and $rawSessionId -ne "unknown") {
180
- $configFile2 = Join-Path $EkkosConfigDir "config.json"
181
- if (Test-Path $configFile2) {
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)
182
337
  try {
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
- }
338
+ & ekkos-capture user $sessionId $sessionName $turnNum $query $projRoot 2>$null | Out-Null
196
339
  } catch {}
197
- }
340
+ } -ArgumentList $RAW_SESSION_ID, $SESSION_NAME_FOR_CAPTURE, $TURN_NUMBER, $USER_QUERY, $projectRootNorm | Out-Null
198
341
  }
199
342
 
200
343
  # ═══════════════════════════════════════════════════════════════════════════
201
- # TURN TRACKING & STATE MANAGEMENT
344
+ # GOLDEN LOOP: CAPTURE PHASE - Track turn start
202
345
  # ═══════════════════════════════════════════════════════════════════════════
203
- $stateDir = Join-Path $env:USERPROFILE ".claude\state"
204
- if (-not (Test-Path $stateDir)) {
205
- New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
346
+ $EkkosDir = Join-Path $ProjectRoot ".ekkos"
347
+ if (-not (Test-Path $EkkosDir)) {
348
+ New-Item -ItemType Directory -Path $EkkosDir -Force | Out-Null
206
349
  }
350
+ $GOLDEN_LOOP_FILE = Join-Path $EkkosDir "golden-loop-current.json"
207
351
 
208
- $stateFile = Join-Path $stateDir "hook-state.json"
209
- $turn = 0
210
- $contextPercent = ""
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 {}
211
367
 
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
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
377
+ }
378
+
379
+ $RETRIEVED_PATTERNS = ""
380
+ $PATTERN_COUNT = 0
381
+ $RETRIEVED_DIRECTIVES = ""
382
+ $DIRECTIVE_COUNT = 0
383
+
384
+ # ═══════════════════════════════════════════════════════════════════════════
385
+ # DIRECTIVE CACHE: Local cache to avoid API calls every turn
386
+ # ═══════════════════════════════════════════════════════════════════════════
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
400
+ }
401
+
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
409
+
410
+ if ($cacheAge -lt $DIRECTIVE_CACHE_TTL -and -not $DIRECTIVE_TRIGGER_DETECTED) {
411
+ $DIRECTIVE_CACHE_VALID = $true
221
412
  }
222
- } catch {
223
- $turn = 0
224
413
  }
225
414
  }
226
415
 
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
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
+ }
234
421
 
235
- Set-Content -Path $stateFile -Value $newState -Force
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 {}
236
437
 
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") {
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
+ }
444
+
445
+ # Escape user query for JSON
446
+ $queryJsonEscaped = $USER_QUERY -replace '\\', '\\\\' -replace '"', '\"' -replace "`n", '\n' -replace "`r", '' -replace "`t", '\t'
447
+
448
+ # Call ekkOS MCP gateway
449
+ $SEARCH_RESPONSE_RAW = $null
243
450
  try {
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 }
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
+ }
247
461
 
248
- Start-Job -ScriptBlock {
249
- param($instanceId, $sessionId, $sessionName, $turnNum, $queryB64, $projectPath)
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
+ }
474
+
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) {
250
487
  try {
251
- $decoded = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($queryB64))
252
- & ekkos-capture user $instanceId $sessionId $sessionName $turnNum $decoded $projectPath 2>&1 | Out-Null
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
253
499
  } catch {}
254
- } -ArgumentList $EkkosInstanceId, $rawSessionId, $sessionName, $turn, $queryBase64, $projectRoot | Out-Null
255
- } catch {}
256
- }
257
500
 
258
- # ═══════════════════════════════════════════════════════════════════════════
259
- # WORKING MEMORY: Fast capture to API (async, non-blocking)
260
- # ═══════════════════════════════════════════════════════════════════════════
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
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
286
517
  }
287
- } catch {}
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
+ }
288
547
  }
289
548
 
290
- $timestamp = (Get-Date).ToString("yyyy-MM-dd hh:mm:ss tt") + " EST"
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
+ }
567
+ }
291
568
 
292
569
  # ═══════════════════════════════════════════════════════════════════════════
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.
570
+ # COLORS (ANSI escape sequences via [char]27)
296
571
  # ═══════════════════════════════════════════════════════════════════════════
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 {}
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
309
587
  }
310
588
 
311
589
  # ═══════════════════════════════════════════════════════════════════════════
312
- # OUTPUT SYSTEM REMINDER
590
+ # "/continue" COMMAND: Delegated to Skill system (DO NOT INTERCEPT)
313
591
  # ═══════════════════════════════════════════════════════════════════════════
314
- $esc = [char]27
315
- $header = "${esc}[0;36m${esc}[1m🧠 ekkOS Memory${esc}[0m ${esc}[2m| $sessionName | $timestamp${esc}[0m"
592
+ # REMOVED: Hook used to intercept /continue and do simple restoration
593
+ # NOW: Let /continue Skill handle it - supports session names + intelligent narrative
316
594
 
317
- $output = @"
318
- $header
595
+ # ═══════════════════════════════════════════════════════════════════════════
596
+ # AUTO-RESTORE REMOVED: Manual /continue only (saves 79% token burn!)
597
+ # ═══════════════════════════════════════════════════════════════════════════
319
598
 
320
- "@
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}"
321
601
 
322
- if ($skillReminders.Count -gt 0) {
323
- $output += "${esc}[0;35m${esc}[1m" + ($skillReminders -join "`n") + "${esc}[0m`n"
602
+ # Output skill reminder if detected
603
+ if ($SKILL_REMINDER) {
604
+ Write-Output ""
605
+ Write-Output "${MAGENTA}${BOLD}${SKILL_REMINDER}${RESET}"
324
606
  }
325
607
 
326
- $output += @"
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>"
615
+ }
616
+
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
+ }
327
649
 
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
- "@
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
+ }
331
655
 
332
- Write-Output $output
656
+ exit 0