arkaos 2.10.1 → 2.11.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 +1 -1
- package/config/hooks/cwd-changed.ps1 +144 -0
- package/config/hooks/post-tool-use.ps1 +347 -0
- package/config/hooks/post-tool-use.sh +6 -6
- package/config/hooks/pre-compact.ps1 +238 -0
- package/config/hooks/pre-compact.sh +10 -6
- package/config/hooks/session-start.ps1 +109 -0
- package/config/hooks/session-start.sh +1 -1
- package/config/hooks/user-prompt-submit.ps1 +287 -0
- package/config/hooks/user-prompt-submit.sh +5 -2
- package/config/statusline.ps1 +160 -0
- package/core/cognition/scheduler/__pycache__/daemon.cpython-313.pyc +0 -0
- package/core/cognition/scheduler/__pycache__/platform.cpython-313.pyc +0 -0
- package/core/cognition/scheduler/daemon.py +77 -21
- package/core/cognition/scheduler/platform.py +43 -12
- package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
- package/core/knowledge/vector_store.py +50 -25
- package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
- package/core/synapse/layers.py +2 -2
- package/installer/adapters/claude-code.js +72 -45
- package/installer/cli.js +19 -6
- package/installer/doctor.js +97 -4
- package/installer/index.js +378 -66
- package/installer/platform.js +20 -0
- package/installer/prompts.js +109 -5
- package/installer/python-resolver.js +45 -10
- package/installer/update.js +349 -37
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/start-dashboard.ps1 +271 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.11.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/.
|
|
109
|
-
mkdir -p "$HOME/.
|
|
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/.
|
|
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/.
|
|
176
|
-
METRICS_LOCK="$HOME/.
|
|
177
|
-
mkdir -p "$HOME/.
|
|
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"
|