arkaos 2.10.1 → 2.12.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.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 2.10.1
1
+ 2.12.0
@@ -0,0 +1,144 @@
1
+ # ============================================================================
2
+ # ArkaOS — CwdChanged Hook (Windows / PowerShell 5.1+)
3
+ #
4
+ # Port of config/hooks/cwd-changed.sh. Fires when the working directory
5
+ # changes. Detects ecosystem and stack so Claude knows which squad and
6
+ # tooling apply to the project.
7
+ #
8
+ # Contract:
9
+ # - Reads a JSON object on stdin with a `cwd` field.
10
+ # - Emits `{"additionalContext": "..."}` on stdout when context is found,
11
+ # or nothing (and exit 0) when there is nothing to say.
12
+ # ============================================================================
13
+
14
+ $ErrorActionPreference = 'Stop'
15
+ [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
16
+
17
+ # ─── Read hook payload from stdin (with empty-stdin fallback) ─────────
18
+ # On Claude Code Windows v2.1.97 the hook pipe is opened but the parent
19
+ # writes zero bytes before closing — every hook gets empty stdin on
20
+ # Windows. Instead of silent-exiting, fall back to `$env:CLAUDE_PROJECT_DIR`
21
+ # (the one env var CC Windows does set on hook invocations) and run the
22
+ # normal ecosystem / stack detection against it. This means a Windows
23
+ # user who `cd`s into a different project inside Claude Code still gets
24
+ # the [arka:project-context] line based on the project root CC knows
25
+ # about, even with the stdin delivery bug.
26
+ $stdinText = [Console]::In.ReadToEnd()
27
+ $newCwd = $null
28
+
29
+ if (-not [string]::IsNullOrWhiteSpace($stdinText)) {
30
+ try {
31
+ $payload = $stdinText | ConvertFrom-Json
32
+ if ($payload.cwd) { $newCwd = [string]$payload.cwd }
33
+ } catch {
34
+ # Malformed payload — fall through to the env-var fallback.
35
+ }
36
+ }
37
+
38
+ if ([string]::IsNullOrWhiteSpace($newCwd) -and $env:CLAUDE_PROJECT_DIR) {
39
+ $newCwd = $env:CLAUDE_PROJECT_DIR
40
+ }
41
+
42
+ if ([string]::IsNullOrWhiteSpace($newCwd)) { exit 0 }
43
+ if (-not (Test-Path -LiteralPath $newCwd -PathType Container)) { exit 0 }
44
+
45
+ # ─── Detect ecosystem from ecosystems.json ─────────────────────────────
46
+ $ecosystemsFile = Join-Path $env:USERPROFILE '.claude\skills\arka\knowledge\ecosystems.json'
47
+ $ecosystem = ''
48
+ $ecosystemName = ''
49
+
50
+ if (Test-Path -LiteralPath $ecosystemsFile) {
51
+ try {
52
+ $eco = Get-Content -Raw -LiteralPath $ecosystemsFile -Encoding UTF8 | ConvertFrom-Json
53
+ if ($eco.ecosystems) {
54
+ # Pass 1: substring match of any project name in the cwd path.
55
+ foreach ($ecoProp in $eco.ecosystems.PSObject.Properties) {
56
+ $ecoId = $ecoProp.Name
57
+ $ecoObj = $ecoProp.Value
58
+ $projects = @($ecoObj.projects)
59
+ foreach ($proj in $projects) {
60
+ if ($proj -and $newCwd.Contains([string]$proj)) {
61
+ $ecosystem = $ecoId
62
+ $ecosystemName = if ($ecoObj.name) { [string]$ecoObj.name } else { $ecoId }
63
+ break
64
+ }
65
+ }
66
+ if ($ecosystem) { break }
67
+ }
68
+
69
+ # Pass 2: if the path looks Herd-hosted, match by directory basename.
70
+ if (-not $ecosystem -and $newCwd -match 'herd') {
71
+ $dirName = Split-Path -Leaf $newCwd.TrimEnd('\','/')
72
+ foreach ($ecoProp in $eco.ecosystems.PSObject.Properties) {
73
+ $ecoId = $ecoProp.Name
74
+ $ecoObj = $ecoProp.Value
75
+ $projects = @($ecoObj.projects)
76
+ foreach ($proj in $projects) {
77
+ if ($proj -eq $dirName) {
78
+ $ecosystem = $ecoId
79
+ $ecosystemName = if ($ecoObj.name) { [string]$ecoObj.name } else { $ecoId }
80
+ break
81
+ }
82
+ }
83
+ if ($ecosystem) { break }
84
+ }
85
+ }
86
+ }
87
+ } catch {
88
+ # Malformed ecosystems.json — degrade gracefully.
89
+ }
90
+ }
91
+
92
+ # ─── Detect stack ──────────────────────────────────────────────────────
93
+ $stack = 'unknown'
94
+
95
+ $composerJson = Join-Path $newCwd 'composer.json'
96
+ $packageJson = Join-Path $newCwd 'package.json'
97
+ $pyprojectToml = Join-Path $newCwd 'pyproject.toml'
98
+
99
+ if (Test-Path -LiteralPath $composerJson) {
100
+ $stack = 'laravel'
101
+ } elseif (Test-Path -LiteralPath $packageJson) {
102
+ try {
103
+ $pkgText = Get-Content -Raw -LiteralPath $packageJson -Encoding UTF8
104
+ if ($pkgText -like '*"nuxt"*') { $stack = 'nuxt' }
105
+ elseif ($pkgText -like '*"next"*') { $stack = 'nextjs' }
106
+ elseif ($pkgText -like '*"react"*') { $stack = 'react' }
107
+ elseif ($pkgText -like '*"vue"*') { $stack = 'vue' }
108
+ else { $stack = 'node' }
109
+ } catch {
110
+ $stack = 'node'
111
+ }
112
+ } elseif (Test-Path -LiteralPath $pyprojectToml) {
113
+ $stack = 'python'
114
+ }
115
+
116
+ # ─── Check for project descriptor ─────────────────────────────────────
117
+ $dirName = Split-Path -Leaf $newCwd.TrimEnd('\','/')
118
+ $projectsDir = Join-Path $env:USERPROFILE '.claude\skills\arka\projects'
119
+ $descriptorFile = Join-Path $projectsDir "$dirName.md"
120
+ $descriptorDir = Join-Path (Join-Path $projectsDir $dirName) 'PROJECT.md'
121
+
122
+ $descriptor = ''
123
+ if (Test-Path -LiteralPath $descriptorFile) {
124
+ $descriptor = $descriptorFile
125
+ } elseif (Test-Path -LiteralPath $descriptorDir) {
126
+ $descriptor = $descriptorDir
127
+ }
128
+
129
+ # ─── Build context output ─────────────────────────────────────────────
130
+ $context = ''
131
+
132
+ if ($ecosystem) {
133
+ $context = "[arka:project-context] Ecosystem: $ecosystemName ($ecosystem) | Stack: $stack | Use /arka-$ecosystem for dedicated squad routing."
134
+ } elseif ($stack -ne 'unknown') {
135
+ $context = "[arka:project-context] Stack: $stack | No ecosystem assigned. Use /arka onboard to register this project."
136
+ }
137
+
138
+ if ($descriptor) {
139
+ $context = "$context Descriptor: $descriptor"
140
+ }
141
+
142
+ if ($context) {
143
+ [pscustomobject]@{ additionalContext = $context } | ConvertTo-Json -Compress
144
+ }
@@ -0,0 +1,347 @@
1
+ # ============================================================================
2
+ # ArkaOS — PostToolUse Hook (Gotchas Memory) (Windows / PowerShell 5.1+)
3
+ #
4
+ # Port of config/hooks/post-tool-use.sh. Detects errors emitted by tool runs
5
+ # and records recurring patterns in a local gotchas file so ArkaOS can learn
6
+ # which mistakes repeat across projects.
7
+ #
8
+ # Contract:
9
+ # - Reads a hook payload JSON from stdin (tool_name, tool_output, exit_code,
10
+ # cwd).
11
+ # - Always exits 0 and writes `{}` on stdout — PostToolUse does not inject
12
+ # context.
13
+ # - Side effect: updates %USERPROFILE%\.arkaos\gotchas.json and
14
+ # %USERPROFILE%\.arkaos\hook-metrics.json.
15
+ #
16
+ # State files live under the canonical v2 runtime directory `~/.arkaos/`.
17
+ # Older builds of the bash twin used `~/.arka-os/` (with a dash); the
18
+ # bash hook has been fixed in the same commit series, and the installer
19
+ # update path now carries legacy state forward on the first update.
20
+ # ============================================================================
21
+
22
+ $ErrorActionPreference = 'Stop'
23
+ [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
24
+
25
+ # Hard timeout budget for the whole hook: 5 seconds (same as settings).
26
+ $sw = [System.Diagnostics.Stopwatch]::StartNew()
27
+
28
+ # ─── Helper functions (used by both gotchas and metrics blocks) ───────
29
+ # Defined at module scope so the metrics-write block at the bottom of
30
+ # the file can still reach them when `shouldProcessGotchas` is false
31
+ # and the gotchas branch is skipped.
32
+
33
+ function Acquire-FileLock {
34
+ param([string]$LockPath, [int]$TimeoutMs = 3000)
35
+ $deadline = [Environment]::TickCount + $TimeoutMs
36
+ while ([Environment]::TickCount -lt $deadline) {
37
+ try {
38
+ $fs = [System.IO.File]::Open(
39
+ $LockPath,
40
+ [System.IO.FileMode]::OpenOrCreate,
41
+ [System.IO.FileAccess]::ReadWrite,
42
+ [System.IO.FileShare]::None
43
+ )
44
+ return $fs
45
+ } catch {
46
+ Start-Sleep -Milliseconds 50
47
+ }
48
+ }
49
+ return $null
50
+ }
51
+
52
+ # PS 5.1's `Set-Content -Encoding UTF8` writes a BOM; tools that parse
53
+ # UTF-8 strictly (jq, python's json module, etc.) treat the BOM as a
54
+ # stray character. Always write JSON state files BOM-less via the .NET
55
+ # API with an explicit UTF8Encoding($false).
56
+ function Write-JsonAtomic {
57
+ param([string]$Path, [string]$Json)
58
+ $tmp = "$Path.tmp"
59
+ $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
60
+ [System.IO.File]::WriteAllText($tmp, $Json, $utf8NoBom)
61
+ Move-Item -Force -LiteralPath $tmp -Destination $Path
62
+ }
63
+
64
+ # PS 5.1 `ConvertTo-Json` on a single-item collection serializes as a
65
+ # bare object, not a single-element array. We always want `[...]` on
66
+ # disk, even with 0 or 1 entries, so state files stay homogeneous and
67
+ # round-trip cleanly. This helper forces array wrapping by serializing
68
+ # each item and joining manually.
69
+ function ConvertTo-JsonArray {
70
+ param($Items, [int]$Depth = 10)
71
+ $arr = @($Items)
72
+ if ($arr.Count -eq 0) { return '[]' }
73
+ $parts = foreach ($item in $arr) {
74
+ $item | ConvertTo-Json -Depth $Depth -Compress
75
+ }
76
+ return '[' + ($parts -join ',') + ']'
77
+ }
78
+
79
+ # ArrayList survives ConvertTo-Json as a proper JSON array even with
80
+ # 0 or 1 elements, unlike `@()` which PS 5.1 happily collapses.
81
+ function New-StringArrayList {
82
+ param([string[]]$Initial)
83
+ $list = New-Object System.Collections.ArrayList
84
+ if ($Initial) {
85
+ foreach ($s in $Initial) {
86
+ if ($null -ne $s -and $s -ne '') { [void]$list.Add($s) }
87
+ }
88
+ }
89
+ return ,$list
90
+ }
91
+
92
+ # ─── Read hook payload from stdin (with empty-stdin tolerance) ────────
93
+ # On Claude Code Windows v2.1.97 the hook stdin pipe is opened but the
94
+ # parent writes zero bytes before closing (upstream bug — see
95
+ # Projects/ARKA OS/Windows Handoff.md canary data). We still want to
96
+ # log the metric that proves the hook fired, so instead of early-exiting
97
+ # on empty stdin we just skip the gotchas-processing block and fall
98
+ # through to the metrics write + final '{}' at the bottom of the file.
99
+ $stdinText = [Console]::In.ReadToEnd()
100
+ $shouldProcessGotchas = $true
101
+ $payload = $null
102
+ $toolName = ''
103
+ $toolOutput = ''
104
+ $exitCode = '0'
105
+ $cwd = ''
106
+
107
+ if ([string]::IsNullOrWhiteSpace($stdinText)) {
108
+ $shouldProcessGotchas = $false
109
+ } else {
110
+ try {
111
+ $payload = $stdinText | ConvertFrom-Json
112
+ } catch {
113
+ $shouldProcessGotchas = $false
114
+ }
115
+ }
116
+
117
+ if ($shouldProcessGotchas -and $null -ne $payload) {
118
+ $toolName = if ($null -ne $payload.tool_name) { [string]$payload.tool_name } else { '' }
119
+ $toolOutput = if ($null -ne $payload.tool_output) { [string]$payload.tool_output } else { '' }
120
+ $exitCode = if ($null -ne $payload.exit_code) { [string]$payload.exit_code } else { '0' }
121
+ $cwd = if ($null -ne $payload.cwd) { [string]$payload.cwd } else { '' }
122
+
123
+ # ─── Only process when there is actually an error ─────────────────
124
+ $errorPattern = '(?i)(error:|fatal:|exception:|failed|ENOENT|EACCES|EPERM|panic:)'
125
+ if ($exitCode -eq '0' -or [string]::IsNullOrEmpty($exitCode)) {
126
+ if ($toolOutput -notmatch $errorPattern) {
127
+ $shouldProcessGotchas = $false
128
+ }
129
+ }
130
+ }
131
+
132
+ # Paths that the metrics block needs are declared up here, outside the
133
+ # gotchas-processing branch, so the metrics write at the very bottom of
134
+ # the file works even when `shouldProcessGotchas` is false (empty stdin,
135
+ # invalid JSON, no error pattern, etc.).
136
+ $arkaosRuntimeDir = Join-Path $env:USERPROFILE '.arkaos'
137
+ $null = New-Item -ItemType Directory -Force -Path $arkaosRuntimeDir -ErrorAction SilentlyContinue
138
+
139
+ # Gotchas processing lives inside a `do { ... } while ($false)` single-
140
+ # iteration loop so any inner step that used to `exit 0` can now `break`
141
+ # out of the gotchas block without bypassing the metrics write.
142
+ if ($shouldProcessGotchas) { do {
143
+
144
+ # ─── Extract first meaningful error line ─────────────────────────────
145
+ $lines = $toolOutput -split "`r?`n"
146
+ $errorLineRegex = '(?i)(error|fatal|exception|failed|ENOENT|EACCES|EPERM|panic|cannot|not found|permission denied)'
147
+ $errorLine = $null
148
+ foreach ($ln in $lines) {
149
+ if ($ln -match $errorLineRegex) { $errorLine = $ln; break }
150
+ }
151
+ if ([string]::IsNullOrWhiteSpace($errorLine)) {
152
+ # Fallback: first-5-lines-tail-1, same as `head -5 | tail -1` in bash.
153
+ $firstFive = @($lines | Select-Object -First 5)
154
+ if ($firstFive.Count -gt 0) { $errorLine = $firstFive[-1] }
155
+ }
156
+ if ([string]::IsNullOrWhiteSpace($errorLine)) {
157
+ break
158
+ }
159
+
160
+ # ─── Normalize pattern (matches bash sed chain exactly) ───────────────
161
+ $pattern = $errorLine
162
+ $pattern = $pattern -replace '[0-9]{4}-[0-9]{2}-[0-9]{2}[T ][0-9]{2}:[0-9]{2}:[0-9]{2}[^ ]*', 'TIMESTAMP'
163
+ $pattern = $pattern -replace '[0-9a-f]{7,40}', 'HASH'
164
+ $pattern = $pattern -replace 'line [0-9]+', 'line N'
165
+ $pattern = $pattern -replace ':[0-9]+:', ':N:'
166
+ if ($pattern.Length -gt 200) { $pattern = $pattern.Substring(0, 200) }
167
+ if ([string]::IsNullOrWhiteSpace($pattern)) {
168
+ break
169
+ }
170
+
171
+ # ─── Categorize ───────────────────────────────────────────────────────
172
+ $category = 'general'
173
+ if ($errorLine -match '(?i)(artisan|eloquent|laravel|blade|migration|composer|php )') { $category = 'laravel' }
174
+ elseif ($errorLine -match '(?i)(npm|node|vue|react|nuxt|next|vite|webpack|typescript|tsx|jsx)') { $category = 'frontend' }
175
+ elseif ($errorLine -match '(?i)(git |merge|rebase|checkout|branch|commit|push|pull)') { $category = 'git' }
176
+ elseif ($errorLine -match '(?i)(sql|postgres|mysql|database|migration|table|column|constraint)') { $category = 'database' }
177
+ elseif ($errorLine -match '(?i)(permission|denied|EACCES|EPERM|chmod|chown|sudo)') { $category = 'permissions' }
178
+ elseif ($errorLine -match '(?i)(test|assert|expect|jest|phpunit|bats|coverage)') { $category = 'testing' }
179
+
180
+ # ─── Match fix suggestion from gotchas-fixes.json ─────────────────────
181
+ $arkaSkillRoot = if ($env:ARKA_OS) { $env:ARKA_OS } else { Join-Path $env:USERPROFILE '.claude\skills\arka' }
182
+ $fixesFile = Join-Path $arkaSkillRoot 'config\gotchas-fixes.json'
183
+ if (-not (Test-Path -LiteralPath $fixesFile)) {
184
+ $repoPathFile = Join-Path $arkaSkillRoot '.repo-path'
185
+ if (Test-Path -LiteralPath $repoPathFile) {
186
+ try {
187
+ $repoPath = (Get-Content -Raw -LiteralPath $repoPathFile -Encoding UTF8).Trim()
188
+ if ($repoPath) {
189
+ $candidate = Join-Path $repoPath 'config\gotchas-fixes.json'
190
+ if (Test-Path -LiteralPath $candidate) { $fixesFile = $candidate }
191
+ }
192
+ } catch { }
193
+ }
194
+ }
195
+
196
+ $suggestion = ''
197
+ if (Test-Path -LiteralPath $fixesFile) {
198
+ try {
199
+ $fixes = (Get-Content -Raw -LiteralPath $fixesFile -Encoding UTF8 | ConvertFrom-Json).fixes
200
+ foreach ($fix in @($fixes)) {
201
+ if ($fix.pattern_match -and ($errorLine -imatch [string]$fix.pattern_match)) {
202
+ $suggestion = [string]$fix.suggestion
203
+ break
204
+ }
205
+ }
206
+ } catch { }
207
+ }
208
+
209
+ # ─── Detect active project ────────────────────────────────────────────
210
+ $project = ''
211
+ if ($cwd) {
212
+ try {
213
+ $repoPathFile = Join-Path $arkaSkillRoot '.repo-path'
214
+ if (Test-Path -LiteralPath $repoPathFile) {
215
+ $repoPath = (Get-Content -Raw -LiteralPath $repoPathFile -Encoding UTF8).Trim()
216
+ $projectsDir = Join-Path $repoPath 'projects'
217
+ if ($repoPath -and (Test-Path -LiteralPath $projectsDir -PathType Container)) {
218
+ foreach ($projDir in (Get-ChildItem -LiteralPath $projectsDir -Directory -ErrorAction SilentlyContinue)) {
219
+ $projectPathFile = Join-Path $projDir.FullName '.project-path'
220
+ if (Test-Path -LiteralPath $projectPathFile) {
221
+ $projPath = (Get-Content -Raw -LiteralPath $projectPathFile -Encoding UTF8).Trim()
222
+ if ($projPath -and $cwd.StartsWith($projPath)) {
223
+ $project = $projDir.Name
224
+ break
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+ } catch { }
231
+ if (-not $project) { $project = Split-Path -Leaf $cwd.TrimEnd('\','/') }
232
+ }
233
+
234
+ # ─── Update ~/.arkaos/gotchas.json (best-effort under a file lock) ────
235
+ # $arkaosRuntimeDir was declared at the top of the script so the metrics
236
+ # block can always reach it.
237
+ $gotchasFile = Join-Path $arkaosRuntimeDir 'gotchas.json'
238
+ $gotchasLock = Join-Path $arkaosRuntimeDir 'gotchas.lock'
239
+
240
+ $lock = Acquire-FileLock -LockPath $gotchasLock
241
+ if ($null -ne $lock) {
242
+ try {
243
+ $now = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
244
+
245
+ $gotchas = @()
246
+ if (Test-Path -LiteralPath $gotchasFile) {
247
+ try {
248
+ $raw = Get-Content -Raw -LiteralPath $gotchasFile -Encoding UTF8
249
+ if ($raw -and $raw.Trim()) {
250
+ $parsed = $raw | ConvertFrom-Json
251
+ $gotchas = @($parsed)
252
+ }
253
+ } catch {
254
+ $gotchas = @()
255
+ }
256
+ }
257
+
258
+ $existingIdx = -1
259
+ for ($i = 0; $i -lt $gotchas.Count; $i++) {
260
+ if ([string]$gotchas[$i].pattern -eq $pattern) {
261
+ $existingIdx = $i
262
+ break
263
+ }
264
+ }
265
+
266
+ if ($existingIdx -ge 0) {
267
+ $entry = $gotchas[$existingIdx]
268
+ $entry.count = [int]$entry.count + 1
269
+ $entry.last_seen = $now
270
+ if ($project) {
271
+ $existingProjects = @()
272
+ if ($entry.projects) { $existingProjects = @($entry.projects) }
273
+ if ($existingProjects -notcontains $project) {
274
+ $existingProjects += $project
275
+ }
276
+ $entry.projects = (New-StringArrayList -Initial $existingProjects)
277
+ }
278
+ if ($suggestion -and [string]::IsNullOrEmpty([string]$entry.suggestion)) {
279
+ $entry | Add-Member -NotePropertyName suggestion -NotePropertyValue $suggestion -Force
280
+ }
281
+ $gotchas[$existingIdx] = $entry
282
+ } else {
283
+ $fullPattern = if ($errorLine.Length -gt 500) { $errorLine.Substring(0, 500) } else { $errorLine }
284
+ $projectsList = if ($project) {
285
+ New-StringArrayList -Initial @($project)
286
+ } else {
287
+ New-StringArrayList
288
+ }
289
+ $newEntry = [pscustomobject][ordered]@{
290
+ pattern = $pattern
291
+ full_pattern = $fullPattern
292
+ category = $category
293
+ tool = $toolName
294
+ count = 1
295
+ first_seen = $now
296
+ last_seen = $now
297
+ projects = $projectsList
298
+ suggestion = if ($suggestion) { $suggestion } else { $null }
299
+ }
300
+ $gotchas = @($gotchas) + $newEntry
301
+ }
302
+
303
+ # Keep top 100 by count desc. Force array with @() after the
304
+ # pipeline — PS 5.1 unwraps to a scalar when only one remains.
305
+ $gotchas = @($gotchas | Sort-Object -Property count -Descending | Select-Object -First 100)
306
+
307
+ Write-JsonAtomic -Path $gotchasFile -Json (ConvertTo-JsonArray -Items $gotchas -Depth 10)
308
+ } catch {
309
+ # Swallow — gotchas tracking is best-effort, must not fail the hook.
310
+ } finally {
311
+ $lock.Close()
312
+ $lock.Dispose()
313
+ }
314
+ }
315
+
316
+ } while ($false) } # end: do/while single-iteration + if ($shouldProcessGotchas)
317
+
318
+ # ─── Log hook metrics (best-effort) ───────────────────────────────────
319
+ $metricsFile = Join-Path $arkaosRuntimeDir 'hook-metrics.json'
320
+ $metricsLock = Join-Path $arkaosRuntimeDir 'hook-metrics.lock'
321
+ $metricsLockHandle = Acquire-FileLock -LockPath $metricsLock -TimeoutMs 2000
322
+ if ($null -ne $metricsLockHandle) {
323
+ try {
324
+ $metrics = @()
325
+ if (Test-Path -LiteralPath $metricsFile) {
326
+ try {
327
+ $raw = Get-Content -Raw -LiteralPath $metricsFile -Encoding UTF8
328
+ if ($raw -and $raw.Trim()) { $metrics = @(($raw | ConvertFrom-Json)) }
329
+ } catch { $metrics = @() }
330
+ }
331
+ $metrics = @($metrics) + ([pscustomobject]@{
332
+ hook = 'post-tool-use'
333
+ duration_ms = [int]$sw.ElapsedMilliseconds
334
+ timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
335
+ })
336
+ if ($metrics.Count -gt 500) { $metrics = @($metrics[-500..-1]) }
337
+ Write-JsonAtomic -Path $metricsFile -Json (ConvertTo-JsonArray -Items $metrics -Depth 5)
338
+ } catch {
339
+ # Metrics are non-critical.
340
+ } finally {
341
+ $metricsLockHandle.Close()
342
+ $metricsLockHandle.Dispose()
343
+ }
344
+ }
345
+
346
+ # Silent output — PostToolUse does not inject context.
347
+ '{}'
@@ -105,11 +105,11 @@ if [ -n "$CWD" ]; then
105
105
  fi
106
106
 
107
107
  # ─── Store in gotchas.json ────────────────────────────────────────────────
108
- GOTCHAS_FILE="$HOME/.arka-os/gotchas.json"
109
- mkdir -p "$HOME/.arka-os"
108
+ GOTCHAS_FILE="$HOME/.arkaos/gotchas.json"
109
+ mkdir -p "$HOME/.arkaos"
110
110
 
111
111
  # Use flock for concurrent safety (fallback if flock not available on macOS)
112
- LOCK_FILE="$HOME/.arka-os/gotchas.lock"
112
+ LOCK_FILE="$HOME/.arkaos/gotchas.lock"
113
113
  if command -v flock &>/dev/null; then
114
114
  LOCK_CMD="flock -w 3 200"
115
115
  else
@@ -172,9 +172,9 @@ fi
172
172
 
173
173
  # ─── Log Metrics ─────────────────────────────────────────────────────────
174
174
  _DURATION_MS=$(_hook_ms)
175
- METRICS_FILE="$HOME/.arka-os/hook-metrics.json"
176
- METRICS_LOCK="$HOME/.arka-os/hook-metrics.lock"
177
- mkdir -p "$HOME/.arka-os"
175
+ METRICS_FILE="$HOME/.arkaos/hook-metrics.json"
176
+ METRICS_LOCK="$HOME/.arkaos/hook-metrics.lock"
177
+ mkdir -p "$HOME/.arkaos"
178
178
  (
179
179
  if command -v flock &>/dev/null; then flock -w 2 200; else true; fi
180
180
  [ ! -f "$METRICS_FILE" ] && echo '[]' > "$METRICS_FILE"