kushi-agents 5.1.0 → 5.2.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/README.md +61 -0
- package/bin/cli.mjs +138 -0
- package/package.json +2 -2
- package/plugin/agents/kushi.agent.md +3 -0
- package/plugin/instructions/hooks.instructions.md +84 -0
- package/plugin/instructions/otel.instructions.md +75 -0
- package/plugin/instructions/parallel-execution.instructions.md +81 -0
- package/plugin/instructions/schema-evolve.instructions.md +73 -0
- package/plugin/skills/_shared/Emit-OtelSpan.ps1 +111 -0
- package/plugin/skills/_shared/Invoke-Hooks.ps1 +177 -0
- package/plugin/skills/_shared/hook-templates/console-debug.ps1 +15 -0
- package/plugin/skills/_shared/hook-templates/teams-notify.ps1 +47 -0
- package/plugin/skills/refresh-project/SKILL.md +8 -4
- package/plugin/skills/schema-evolve/.created-by-skill-creator +0 -0
- package/plugin/skills/schema-evolve/SKILL.md +106 -0
- package/plugin/skills/schema-evolve/evals/evals.json +37 -0
- package/plugin/skills/self-check/SKILL.md +9 -54
- package/plugin/skills/self-check/references/algorithm.md +55 -0
- package/plugin/skills/self-check/run.ps1 +92 -0
- package/plugin/skills/teach/.created-by-skill-creator +0 -0
- package/plugin/skills/teach/SKILL.md +77 -0
- package/plugin/skills/teach/evals/evals.json +37 -0
- package/src/hooks-dispatcher.test.mjs +135 -0
- package/src/otel-emit.test.mjs +73 -0
- package/src/parallel-refresh.test.mjs +50 -0
- package/src/schema-evolve.test.mjs +78 -0
- package/src/teach.test.mjs +45 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Emits an OpenTelemetry span via OTLP/HTTP JSON.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Opt-in OTel export. If KUSHI_OTEL_ENDPOINT is unset or empty, this is a
|
|
7
|
+
complete no-op (no HTTP calls, no allocations). Pure PowerShell — no
|
|
8
|
+
external dependencies.
|
|
9
|
+
|
|
10
|
+
.PARAMETER SpanName
|
|
11
|
+
The span name (e.g., 'kushi.pull', 'kushi.build_state').
|
|
12
|
+
|
|
13
|
+
.PARAMETER Attributes
|
|
14
|
+
Hashtable of span attributes. Only metadata — never evidence content.
|
|
15
|
+
|
|
16
|
+
.PARAMETER DurationMs
|
|
17
|
+
Duration in milliseconds (optional — computed from Start if provided).
|
|
18
|
+
|
|
19
|
+
.PARAMETER Start
|
|
20
|
+
Optional start time [datetime]. Defaults to (now - DurationMs).
|
|
21
|
+
#>
|
|
22
|
+
[CmdletBinding()]
|
|
23
|
+
param(
|
|
24
|
+
[Parameter(Mandatory)][string]$SpanName,
|
|
25
|
+
[hashtable]$Attributes = @{},
|
|
26
|
+
[int]$DurationMs = 0,
|
|
27
|
+
[datetime]$Start = [datetime]::MinValue
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# No-op guard — zero overhead when endpoint is unset
|
|
31
|
+
$endpoint = $env:KUSHI_OTEL_ENDPOINT
|
|
32
|
+
if (-not $endpoint) { return }
|
|
33
|
+
|
|
34
|
+
$ErrorActionPreference = 'Continue'
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
# Compute timestamps (nanoseconds since epoch)
|
|
38
|
+
$now = [DateTimeOffset]::UtcNow
|
|
39
|
+
if ($Start -eq [datetime]::MinValue) {
|
|
40
|
+
$startOffset = $now.AddMilliseconds(-$DurationMs)
|
|
41
|
+
} else {
|
|
42
|
+
$startOffset = [DateTimeOffset]::new($Start.ToUniversalTime())
|
|
43
|
+
}
|
|
44
|
+
$endOffset = $now
|
|
45
|
+
|
|
46
|
+
$startNs = ($startOffset.ToUnixTimeMilliseconds() * 1000000).ToString()
|
|
47
|
+
$endNs = ($endOffset.ToUnixTimeMilliseconds() * 1000000).ToString()
|
|
48
|
+
|
|
49
|
+
# Generate trace/span IDs
|
|
50
|
+
$traceId = [System.Guid]::NewGuid().ToString('N')
|
|
51
|
+
$spanId = [System.Guid]::NewGuid().ToString('N').Substring(0, 16)
|
|
52
|
+
|
|
53
|
+
# Build attributes array
|
|
54
|
+
$attrArray = @()
|
|
55
|
+
# Allowed keys only (privacy contract)
|
|
56
|
+
$allowedKeys = @('project','alias','source','duration_ms','success','evidence_count',
|
|
57
|
+
'pages_written','contradictions_flagged','findings_count','entity',
|
|
58
|
+
'field','event','hook_type','target','report_path','items_pulled')
|
|
59
|
+
foreach ($key in $Attributes.Keys) {
|
|
60
|
+
if ($key -notin $allowedKeys) { continue }
|
|
61
|
+
$val = $Attributes[$key]
|
|
62
|
+
$valObj = if ($val -is [bool]) {
|
|
63
|
+
@{ boolValue = $val }
|
|
64
|
+
} elseif ($val -is [int] -or $val -is [long] -or $val -is [double]) {
|
|
65
|
+
@{ intValue = [string]$val }
|
|
66
|
+
} else {
|
|
67
|
+
@{ stringValue = [string]$val }
|
|
68
|
+
}
|
|
69
|
+
$attrArray += @{ key = $key; value = $valObj }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Get service version
|
|
73
|
+
$version = '5.2.0'
|
|
74
|
+
$pkgJson = Join-Path $PSScriptRoot '..\..\..\..\package.json'
|
|
75
|
+
if (Test-Path $pkgJson) {
|
|
76
|
+
try { $version = (Get-Content -Raw $pkgJson | ConvertFrom-Json).version } catch {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Build OTLP payload
|
|
80
|
+
$payload = @{
|
|
81
|
+
resourceSpans = @(@{
|
|
82
|
+
resource = @{
|
|
83
|
+
attributes = @(
|
|
84
|
+
@{ key = 'service.name'; value = @{ stringValue = 'kushi' } }
|
|
85
|
+
@{ key = 'service.version'; value = @{ stringValue = $version } }
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
scopeSpans = @(@{
|
|
89
|
+
scope = @{ name = 'kushi'; version = $version }
|
|
90
|
+
spans = @(@{
|
|
91
|
+
traceId = $traceId
|
|
92
|
+
spanId = $spanId
|
|
93
|
+
name = $SpanName
|
|
94
|
+
kind = 1 # SPAN_KIND_INTERNAL
|
|
95
|
+
startTimeUnixNano = $startNs
|
|
96
|
+
endTimeUnixNano = $endNs
|
|
97
|
+
attributes = $attrArray
|
|
98
|
+
status = @{
|
|
99
|
+
code = if ($Attributes['success'] -eq $false) { 2 } else { 1 }
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
$json = $payload | ConvertTo-Json -Depth 20 -Compress
|
|
107
|
+
Invoke-RestMethod -Uri $endpoint -Method POST -Body $json `
|
|
108
|
+
-ContentType 'application/json' -TimeoutSec 5 -ErrorAction Stop | Out-Null
|
|
109
|
+
} catch {
|
|
110
|
+
Write-Warning "OTel export failed (non-blocking): $($_.Exception.Message)"
|
|
111
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Dispatches hooks for a kushi pipeline event.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Idempotent hook dispatcher. Reads .kushi/hooks.yml + .kushi/hooks/*.ps1,
|
|
7
|
+
invokes each configured hook for the given event, captures output/duration,
|
|
8
|
+
and logs results to Evidence/<alias>/State/hooks-log.md.
|
|
9
|
+
|
|
10
|
+
Failures are warn-only — never blocks the pipeline.
|
|
11
|
+
|
|
12
|
+
.PARAMETER ProjectRoot
|
|
13
|
+
Path to the project root (contains .kushi/).
|
|
14
|
+
|
|
15
|
+
.PARAMETER Event
|
|
16
|
+
One of: post-pull, post-state, post-contradiction, post-lint.
|
|
17
|
+
|
|
18
|
+
.PARAMETER Payload
|
|
19
|
+
Hashtable of event data (serialized to JSON for hooks).
|
|
20
|
+
|
|
21
|
+
.PARAMETER StateDir
|
|
22
|
+
Path to Evidence/<alias>/State/ for logging.
|
|
23
|
+
#>
|
|
24
|
+
[CmdletBinding()]
|
|
25
|
+
param(
|
|
26
|
+
[Parameter(Mandatory)][string]$ProjectRoot,
|
|
27
|
+
[Parameter(Mandatory)][ValidateSet('post-pull','post-state','post-contradiction','post-lint')][string]$Event,
|
|
28
|
+
[Parameter(Mandatory)][hashtable]$Payload,
|
|
29
|
+
[string]$StateDir
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
$ErrorActionPreference = 'Continue'
|
|
33
|
+
|
|
34
|
+
$hooksYml = Join-Path $ProjectRoot '.kushi/hooks.yml'
|
|
35
|
+
$hooksDir = Join-Path $ProjectRoot '.kushi/hooks'
|
|
36
|
+
$payloadJson = $Payload | ConvertTo-Json -Depth 10 -Compress
|
|
37
|
+
|
|
38
|
+
# Collect hook definitions
|
|
39
|
+
$hookDefs = @()
|
|
40
|
+
|
|
41
|
+
# Parse hooks.yml if present
|
|
42
|
+
if (Test-Path $hooksYml) {
|
|
43
|
+
$lines = Get-Content -Path $hooksYml -ErrorAction SilentlyContinue
|
|
44
|
+
$inEvent = $false
|
|
45
|
+
$eventIndent = 0
|
|
46
|
+
foreach ($line in $lines) {
|
|
47
|
+
if ($line -match "^\s*$Event\s*:") {
|
|
48
|
+
$inEvent = $true
|
|
49
|
+
$eventIndent = ($line -replace '[^\s].*','').Length
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
if ($inEvent) {
|
|
53
|
+
# Determine current indent
|
|
54
|
+
$currentIndent = if ($line -match '^(\s*)') { $Matches[1].Length } else { 0 }
|
|
55
|
+
# Exit if we hit a line at same/lower indent that's not empty
|
|
56
|
+
if ($line.Trim() -ne '' -and $currentIndent -le $eventIndent -and $line -notmatch '^\s*#') {
|
|
57
|
+
$inEvent = $false
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
if ($line -match '^\s*-\s*type:\s*(\w+)') { $currentType = $Matches[1]; continue }
|
|
61
|
+
if ($line -match '^\s*url:\s*(.+)') {
|
|
62
|
+
$hookDefs += @{ type = 'webhook'; target = $Matches[1].Trim() }
|
|
63
|
+
}
|
|
64
|
+
if ($line -match '^\s*path:\s*(.+)') {
|
|
65
|
+
$hookDefs += @{ type = 'script'; target = $Matches[1].Trim() }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Also check for event-named script in hooks dir
|
|
72
|
+
$eventScript = Join-Path $hooksDir "$Event.ps1"
|
|
73
|
+
if ((Test-Path $eventScript) -and -not ($hookDefs | Where-Object { $_.target -eq $eventScript })) {
|
|
74
|
+
$hookDefs += @{ type = 'script'; target = $eventScript }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if ($hookDefs.Count -eq 0) {
|
|
78
|
+
Write-Verbose "No hooks configured for event '$Event'"
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Emit OTel span for each hook invocation
|
|
83
|
+
$emitOtel = Join-Path $PSScriptRoot 'Emit-OtelSpan.ps1'
|
|
84
|
+
|
|
85
|
+
$results = @()
|
|
86
|
+
foreach ($hook in $hookDefs) {
|
|
87
|
+
$sw = [System.Diagnostics.Stopwatch]::StartNew()
|
|
88
|
+
$status = 'success'
|
|
89
|
+
$output = ''
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
if ($hook.type -eq 'webhook') {
|
|
93
|
+
$resp = Invoke-RestMethod -Uri $hook.target -Method POST -Body $payloadJson `
|
|
94
|
+
-ContentType 'application/json' -TimeoutSec 5 -ErrorAction Stop
|
|
95
|
+
} elseif ($hook.type -eq 'script') {
|
|
96
|
+
$scriptPath = if ([System.IO.Path]::IsPathRooted($hook.target)) {
|
|
97
|
+
$hook.target
|
|
98
|
+
} else {
|
|
99
|
+
Join-Path (Resolve-Path $ProjectRoot).Path $hook.target
|
|
100
|
+
}
|
|
101
|
+
if (Test-Path $scriptPath) {
|
|
102
|
+
try {
|
|
103
|
+
$output = $payloadJson | & pwsh -NoProfile -Command "try { & '$scriptPath' } catch { Write-Output `$_.Exception.Message; exit 1 }" 2>&1 | Out-String
|
|
104
|
+
if ($LASTEXITCODE -ne 0) {
|
|
105
|
+
$status = 'failed'
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
$status = 'failed'
|
|
109
|
+
$output = $_.Exception.Message
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
$status = 'skipped'
|
|
113
|
+
$output = "Script not found: $scriptPath"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
$status = 'failed'
|
|
118
|
+
$output = $_.Exception.Message
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
$sw.Stop()
|
|
122
|
+
$results += @{
|
|
123
|
+
hook = $hook
|
|
124
|
+
status = $status
|
|
125
|
+
duration_ms = $sw.ElapsedMilliseconds
|
|
126
|
+
output = $output
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Emit OTel span if helper exists
|
|
130
|
+
if (Test-Path $emitOtel) {
|
|
131
|
+
$otelAttrs = @{
|
|
132
|
+
project = $Payload.project ?? ''
|
|
133
|
+
alias = $Payload.alias ?? ''
|
|
134
|
+
event = $Event
|
|
135
|
+
hook_type = $hook.type
|
|
136
|
+
target = $hook.target
|
|
137
|
+
duration_ms = $sw.ElapsedMilliseconds
|
|
138
|
+
success = ($status -eq 'success')
|
|
139
|
+
}
|
|
140
|
+
& $emitOtel -SpanName 'kushi.hook.invoked' -Attributes $otelAttrs
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Write hooks-log.md
|
|
145
|
+
if ($StateDir -and (Test-Path $StateDir)) {
|
|
146
|
+
$logFile = Join-Path $StateDir 'hooks-log.md'
|
|
147
|
+
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm'
|
|
148
|
+
|
|
149
|
+
$entries = @()
|
|
150
|
+
foreach ($r in $results) {
|
|
151
|
+
$entry = @"
|
|
152
|
+
|
|
153
|
+
## [$timestamp] $Event | hook: $($r.hook.target | Split-Path -Leaf)
|
|
154
|
+
|
|
155
|
+
- status: $($r.status)
|
|
156
|
+
- duration_ms: $($r.duration_ms)
|
|
157
|
+
- target: $($r.hook.target)
|
|
158
|
+
"@
|
|
159
|
+
if ($r.status -eq 'failed') {
|
|
160
|
+
$entry += "`n- error: $($r.output)"
|
|
161
|
+
}
|
|
162
|
+
$entries += $entry
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
$newContent = $entries -join "`n"
|
|
166
|
+
|
|
167
|
+
if (Test-Path $logFile) {
|
|
168
|
+
$existing = Get-Content -Raw $logFile
|
|
169
|
+
Set-Content -Path $logFile -Value ($existing + "`n" + $newContent) -Encoding utf8NoBOM
|
|
170
|
+
} else {
|
|
171
|
+
$header = "---`nkushi_hooks_log: true`n---`n`n# Hooks Log`n"
|
|
172
|
+
Set-Content -Path $logFile -Value ($header + $newContent) -Encoding utf8NoBOM
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# Return results for caller inspection
|
|
177
|
+
$results
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Hook template: Debug output — prints event payload to console.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Copy this file to .kushi/hooks/<event>.ps1 for debugging.
|
|
7
|
+
Prints the full event payload to stdout with timestamp.
|
|
8
|
+
#>
|
|
9
|
+
|
|
10
|
+
$payload = $input | Out-String
|
|
11
|
+
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
|
12
|
+
|
|
13
|
+
Write-Output "=== KUSHI HOOK DEBUG [$timestamp] ==="
|
|
14
|
+
Write-Output $payload
|
|
15
|
+
Write-Output "=== END HOOK DEBUG ==="
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Hook template: Posts event to a Microsoft Teams webhook.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Copy this file to .kushi/hooks/post-pull.ps1 (or any event name) and
|
|
7
|
+
set $WebhookUrl to your Teams Incoming Webhook URL.
|
|
8
|
+
|
|
9
|
+
Receives event payload as JSON on stdin.
|
|
10
|
+
#>
|
|
11
|
+
|
|
12
|
+
# === CONFIGURE THIS ===
|
|
13
|
+
$WebhookUrl = $env:KUSHI_TEAMS_WEBHOOK_URL # Or hard-code your URL
|
|
14
|
+
|
|
15
|
+
if (-not $WebhookUrl) {
|
|
16
|
+
Write-Warning "KUSHI_TEAMS_WEBHOOK_URL not set — skipping Teams notification"
|
|
17
|
+
exit 0
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# Read event payload from stdin
|
|
21
|
+
$payload = $input | Out-String | ConvertFrom-Json
|
|
22
|
+
|
|
23
|
+
# Build Teams Adaptive Card message
|
|
24
|
+
$card = @{
|
|
25
|
+
type = 'message'
|
|
26
|
+
attachments = @(@{
|
|
27
|
+
contentType = 'application/vnd.microsoft.card.adaptive'
|
|
28
|
+
content = @{
|
|
29
|
+
'$schema' = 'http://adaptivecards.io/schemas/adaptive-card.json'
|
|
30
|
+
type = 'AdaptiveCard'
|
|
31
|
+
version = '1.4'
|
|
32
|
+
body = @(
|
|
33
|
+
@{ type = 'TextBlock'; text = "🔔 Kushi: $($payload.event ?? 'event')"; weight = 'Bolder'; size = 'Medium' }
|
|
34
|
+
@{ type = 'FactSet'; facts = @(
|
|
35
|
+
@{ title = 'Project'; value = $payload.project ?? 'unknown' }
|
|
36
|
+
@{ title = 'Source'; value = $payload.source ?? 'n/a' }
|
|
37
|
+
@{ title = 'Status'; value = if ($payload.success) { '✅ Success' } else { '⚠️ Issue' } }
|
|
38
|
+
@{ title = 'Duration'; value = "$($payload.duration_ms ?? 0)ms" }
|
|
39
|
+
)}
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
$json = $card | ConvertTo-Json -Depth 20 -Compress
|
|
46
|
+
Invoke-RestMethod -Uri $WebhookUrl -Method POST -Body $json -ContentType 'application/json' | Out-Null
|
|
47
|
+
Write-Output "Teams notification sent for $($payload.project)"
|
|
@@ -91,6 +91,8 @@ After drain, proceed to Step 2 even if some markers remain — the orchestrator
|
|
|
91
91
|
|
|
92
92
|
### Step 2 — Per-source dispatch
|
|
93
93
|
|
|
94
|
+
> **v5.2.0 parallel dispatch**: By default, enabled sources run in parallel (up to `parallel.max_workers`, default 4). Pass `--sequential` to force serial execution. See `parallel-execution.instructions.md`.
|
|
95
|
+
|
|
94
96
|
For each enabled source (or just the requested one), call its `pull-<source>` skill with the effective window.
|
|
95
97
|
|
|
96
98
|
**OneNote pre-dispatch gate (kushi v3.10.0+, HARD):** Before invoking `pull-onenote`, run the browser-URL completeness check:
|
|
@@ -200,10 +202,12 @@ Re-running refresh with the same window is safe:
|
|
|
200
202
|
- "pull `<X>` since `<date>`"
|
|
201
203
|
- "refresh `<X>` last `<N>` days"
|
|
202
204
|
- "backfill `<X>` from `<from>` to `<to>`"
|
|
203
|
-
## References (v4.4.7)
|
|
204
|
-
|
|
205
|
-
- Name → ID resolution (any source) follows `..\..\instructions\fuzzy-disambiguation.instructions.md`.
|
|
206
|
-
- After each per-source pull, run the gate per `..\..\instructions\per-source-verification-gate.instructions.md` (retry once → FOLLOW-UPS.md on failure).
|
|
205
|
+
## References (v4.4.7)
|
|
206
|
+
|
|
207
|
+
- Name → ID resolution (any source) follows `..\..\instructions\fuzzy-disambiguation.instructions.md`.
|
|
208
|
+
- After each per-source pull, run the gate per `..\..\instructions\per-source-verification-gate.instructions.md` (retry once → FOLLOW-UPS.md on failure).
|
|
209
|
+
- Parallel dispatch follows `..\..\instructions\parallel-execution.instructions.md` (v5.2.0+).
|
|
210
|
+
- Post-pull hooks fired per source via `..\..\skills\_shared\Invoke-Hooks.ps1` (v5.2.0+).
|
|
207
211
|
|
|
208
212
|
|
|
209
213
|
## Issue Recovery
|
|
File without changes
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "schema-evolve"
|
|
3
|
+
version: "1.0.0"
|
|
4
|
+
description: "USE WHEN the user says 'from now on always do X', 'remember this rule', 'kushi remember <rule>', or wants to teach kushi a project-specific convention that persists across runs. DO NOT USE for one-time instructions (just answer inline) or for modifying doctrine (edit the .instructions.md file directly). Capability: captures user-stated conventions to Evidence/<alias>/State/CLAUDE.md, reads them back at the start of build-state/ask-project/refresh runs."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Skill: schema-evolve
|
|
8
|
+
|
|
9
|
+
Captures user-stated project conventions and persists them to `Evidence/<alias>/State/CLAUDE.md` so future runs respect them automatically.
|
|
10
|
+
|
|
11
|
+
## Triggers
|
|
12
|
+
|
|
13
|
+
- "from now on always do X for this project"
|
|
14
|
+
- "remember that Y means Z"
|
|
15
|
+
- `kushi remember <rule>` (CLI verb)
|
|
16
|
+
- "for this project, always treat A as B"
|
|
17
|
+
- "never include X in summaries for this project"
|
|
18
|
+
|
|
19
|
+
## Inputs
|
|
20
|
+
|
|
21
|
+
- `<rule>` — free-text convention statement.
|
|
22
|
+
- `<project>` — engagement name (resolved from context or explicit).
|
|
23
|
+
|
|
24
|
+
## Procedure
|
|
25
|
+
|
|
26
|
+
1. **Resolve project** — identify the target project via fuzzy match or current context.
|
|
27
|
+
2. **Parse rule** — extract the convention from user's statement. Normalize to declarative form.
|
|
28
|
+
3. **Validate against doctrine** — check if the rule conflicts with hard doctrine (WorkIQ-only, CSC format, etc.). If conflict: warn user, explain why, do NOT persist.
|
|
29
|
+
4. **Write to CLAUDE.md** — append the rule with timestamp + scope metadata to `Evidence/<alias>/State/CLAUDE.md`.
|
|
30
|
+
5. **Confirm** — echo back the persisted rule and explain when it will be applied.
|
|
31
|
+
|
|
32
|
+
## Step checklist
|
|
33
|
+
|
|
34
|
+
- [ ] Step 1 — Resolve project + State path
|
|
35
|
+
- [ ] Step 2 — Parse + validate rule
|
|
36
|
+
- [ ] Step 3 — Append to CLAUDE.md
|
|
37
|
+
- [ ] Step 4 — Confirm to user
|
|
38
|
+
|
|
39
|
+
### Step 1 — Resolve project
|
|
40
|
+
|
|
41
|
+
Resolve `<engagement-root>/<project>/Evidence/<alias>/State/`. If CLAUDE.md doesn't exist, create it with the v5.0.0 header:
|
|
42
|
+
|
|
43
|
+
```markdown
|
|
44
|
+
---
|
|
45
|
+
kushi_state_page: true
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
# CLAUDE.md — Project Conventions
|
|
49
|
+
|
|
50
|
+
Rules captured via `schema-evolve` skill. Read by build-state, ask-project, refresh-project at run start.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Step 2 — Parse + validate
|
|
54
|
+
|
|
55
|
+
Normalize user's statement:
|
|
56
|
+
- "from now on always X" → "Always X"
|
|
57
|
+
- "never do Y" → "Never Y"
|
|
58
|
+
- "treat A as B" → "Treat A as B"
|
|
59
|
+
|
|
60
|
+
Validate: if the rule would override hard doctrine (`workiq-only`, `verbatim-by-default`, `csc` format), reject with explanation.
|
|
61
|
+
|
|
62
|
+
### Step 3 — Append to CLAUDE.md
|
|
63
|
+
|
|
64
|
+
Append after existing content:
|
|
65
|
+
|
|
66
|
+
```markdown
|
|
67
|
+
## Rule: <short-title>
|
|
68
|
+
|
|
69
|
+
- **Added**: <ISO-8601 timestamp>
|
|
70
|
+
- **Scope**: project
|
|
71
|
+
- **Source**: user (natural language)
|
|
72
|
+
|
|
73
|
+
<normalized rule text>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Step 4 — Confirm
|
|
77
|
+
|
|
78
|
+
Print:
|
|
79
|
+
> ✅ Rule captured: "<rule>"
|
|
80
|
+
> Applied to: <project> (project scope)
|
|
81
|
+
> Active in: build-state, ask-project, refresh-project
|
|
82
|
+
|
|
83
|
+
## Gotchas
|
|
84
|
+
|
|
85
|
+
1. **Hard doctrine override**: Rules CANNOT override WorkIQ-only, CSC format, verbatim-by-default, or evidence layout. Warn and refuse.
|
|
86
|
+
2. **Duplicate rules**: If a near-identical rule exists, update the existing one (bump timestamp) rather than duplicating.
|
|
87
|
+
3. **CLAUDE.md missing**: Create it with proper frontmatter on first rule capture.
|
|
88
|
+
4. **No evidence modification**: Rules affect interpretation/presentation only — never modify raw evidence files.
|
|
89
|
+
5. **Scope creep**: v5.2.0 is project-scope only. Don't promise global scope.
|
|
90
|
+
|
|
91
|
+
## Validation loop
|
|
92
|
+
|
|
93
|
+
After writing:
|
|
94
|
+
1. Re-read `CLAUDE.md` — verify the new rule appears.
|
|
95
|
+
2. Parse all rules — verify valid structure (heading, metadata, text).
|
|
96
|
+
3. Run self-check: `pwsh plugin/skills/self-check/run.ps1 -Targeted schema-evolve`
|
|
97
|
+
|
|
98
|
+
## References
|
|
99
|
+
|
|
100
|
+
- `schema-evolve.instructions.md` — doctrine for convention persistence
|
|
101
|
+
- `karpathy-state-layout.instructions.md` — CLAUDE.md role in State layout
|
|
102
|
+
- `living-wiki.instructions.md` — incremental State maintenance
|
|
103
|
+
|
|
104
|
+
## Issue Recovery
|
|
105
|
+
|
|
106
|
+
When this skill exposes a reusable defect (doctrine gap, missing cross-reference), apply the [Issue Recovery Rule](../../instructions/issue-recovery.instructions.md): fix the smallest correct repo-owned artifact first, prefer durable fixes over per-run workarounds, then re-run the narrowest failed check. Do NOT use memory as a substitute for correcting the workflow surface.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill": "schema-evolve",
|
|
3
|
+
"cases": [
|
|
4
|
+
{
|
|
5
|
+
"id": "schema-evolve-capture",
|
|
6
|
+
"name": "Captures a user rule to CLAUDE.md",
|
|
7
|
+
"input": "kushi remember always use 'HCA' not 'Healthcare Accelerator' in summaries",
|
|
8
|
+
"expected_assertions": [
|
|
9
|
+
{ "type": "contains", "value": "CLAUDE.md" },
|
|
10
|
+
{ "type": "contains", "value": "Rule" },
|
|
11
|
+
{ "type": "contains", "value": "HCA" }
|
|
12
|
+
],
|
|
13
|
+
"grader_type": "script"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "schema-evolve-doctrine-conflict",
|
|
17
|
+
"name": "Rejects rule that conflicts with hard doctrine",
|
|
18
|
+
"input": "kushi remember never use WorkIQ, always use Graph API directly",
|
|
19
|
+
"expected_assertions": [
|
|
20
|
+
{ "type": "contains", "value": "conflict" },
|
|
21
|
+
{ "type": "contains", "value": "WorkIQ" },
|
|
22
|
+
{ "type": "not_contains", "value": "Rule captured" }
|
|
23
|
+
],
|
|
24
|
+
"grader_type": "script"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "schema-evolve-natural-language",
|
|
28
|
+
"name": "Auto-detects convention from natural language",
|
|
29
|
+
"input": "from now on for this project always treat John as the EM",
|
|
30
|
+
"expected_assertions": [
|
|
31
|
+
{ "type": "contains", "value": "John" },
|
|
32
|
+
{ "type": "contains", "value": "rule" }
|
|
33
|
+
],
|
|
34
|
+
"grader_type": "script"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
@@ -73,6 +73,9 @@ Checks split into **core** (always run) and **deep** (opt-in).
|
|
|
73
73
|
| D34.creator-conformance | skill-creator + skill-checker harness integrity (v5.0.4+) | validates `scaffold.ps1` + `check-skill.ps1` ship and are parseable; every skill carrying the `.created-by-skill-creator` marker passes `check-skill --lint` clean; `check-skill --all --retrofit --dry-run` shows no non-additive gaps; the dogfood report at `docs/audits/v5.0.4-skill-creator-dogfood.md` is fresh (≤14 days). Five sub-checks: `D34.skill-creator-exists`, `D34.skill-checker-exists`, `D34.creator-output-conforms`, `D34.retrofit-clean`, `D34.dogfood-report-fresh`. See `skill-authoring.instructions.md`. |
|
|
74
74
|
| D35.log | State log (v5.1.0+) | `State/log.md` exists, `## [` headings match canonical format, timestamps reverse-chronological. Sub-checks: `D35.log-exists`, `D35.log-format`, `D35.log-monotonic`. |
|
|
75
75
|
| D36.contradictions | Contradictions (v5.1.0+) | `> [!warning] Contradicted` callouts well-formed, `_review-queue.md` fresh, `<!-- kushi:auto -->` fences balanced. Sub-checks: `D36.callout-syntax`, `D36.review-queue-fresh`, `D36.no-silent-overwrite`. |
|
|
76
|
+
| D37.hooks | Hooks system (v5.2.0+) | `hooks.instructions.md` exists, `Invoke-Hooks.ps1` helper exists, hook-templates/ has ≥2 templates, any `hooks-log.md` uses canonical heading format. Sub-checks: `D37.hooks-doctrine-exists`, `D37.hooks-helper-exists`, `D37.hooks-templates-exist`, `D37.hooks-log-format`. |
|
|
77
|
+
| D38.parallel | Parallel execution (v5.2.0+) | `parallel-execution.instructions.md` exists, `refresh-project/SKILL.md` references parallel dispatch, doctrine documents deterministic output ordering. Sub-checks: `D38.parallel-doctrine-exists`, `D38.refresh-supports-parallel`, `D38.deterministic-order`. |
|
|
78
|
+
| D39.otel | OpenTelemetry export (v5.2.0+) | `otel.instructions.md` exists, `Emit-OtelSpan.ps1` helper exists, helper short-circuits when `KUSHI_OTEL_ENDPOINT` is unset. Sub-checks: `D39.otel-doctrine-exists`, `D39.otel-helper-exists`, `D39.otel-noop-when-unset`. |
|
|
76
79
|
| **CSC weekly-layout checks (kushi v4.9.0)** | | gated on `Resolve-EngagementRoots` — no-ops on the kushi repo itself. |
|
|
77
80
|
| D11.csc | CSC entity coverage + depth | every `Evidence/<alias>/<source>/weekly/*-csc.md` has ≥ 1 entity heading; per-source minimum bullet count + populated-section count (meetings 25/6, email 8/4, teams 6/3, onenote 10/4, sharepoint 8/3, crm 12/5, ado 8/4). Coverage-Notes-only blocks (low-signal escape) are exempt. |
|
|
78
81
|
| D12.csc | CSC section order | every entity block's `###` section headings appear in the canonical order: Participants → Topics → Q&A → Who Said What → Decisions → Dates & Numbers → Action Items → Next Steps → Open Questions → Risks → Customer Asks → Artifacts → Coverage Notes. |
|
|
@@ -107,59 +110,7 @@ Checks split into **core** (always run) and **deep** (opt-in).
|
|
|
107
110
|
|
|
108
111
|
## Algorithm (per check)
|
|
109
112
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
For each `plugin/skills/<dir>/SKILL.md`: parse YAML frontmatter (text between leading `---` lines). Required keys: `name`, `version`, `description`.
|
|
113
|
-
|
|
114
|
-
> ⚠️ self-check C1 — Skill `<dir>` is missing required frontmatter key `<key>`. Add it before the first `---` separator.
|
|
115
|
-
|
|
116
|
-
### C2 — Skill name matches directory
|
|
117
|
-
|
|
118
|
-
For each skill: `frontmatter.name` (case-insensitive) must equal directory name.
|
|
119
|
-
|
|
120
|
-
> ⚠️ self-check C2 — Directory `<dir>/` declares `name: "<name>"`. Rename one or the other so they match.
|
|
121
|
-
|
|
122
|
-
### C3 — Skills inventoried in agent
|
|
123
|
-
|
|
124
|
-
`plugin/agents/kushi.agent.md` must contain a backtick-wrapped reference to every skill directory name. Look in any markdown table column or any inline `\`<name>\`` token.
|
|
125
|
-
|
|
126
|
-
> ⚠️ self-check C3 — Skill `<dir>` is not listed in the kushi agent. Suggested row for the routing table:
|
|
127
|
-
>
|
|
128
|
-
> `| <verb> | <dir> | <one-line description from frontmatter> |`
|
|
129
|
-
|
|
130
|
-
### C4 — Prompts route to real skills
|
|
131
|
-
|
|
132
|
-
For each `plugin/prompts/*.prompt.md`: extract any reference of the form `` `<skill-name>` `` or `delegates to <skill-name>` (case-insensitive). Each must exist as a directory under `plugin/skills/`.
|
|
133
|
-
|
|
134
|
-
> ⚠️ self-check C4 — Prompt `<file>` references skill `<name>` which does not exist under `plugin/skills/`. Either create the skill or update the prompt.
|
|
135
|
-
|
|
136
|
-
### C5 — Instructions referenced
|
|
137
|
-
|
|
138
|
-
For each `plugin/instructions/*.instructions.md`: at least one SKILL.md OR `plugin/agents/kushi.agent.md` must reference it by filename (with or without path).
|
|
139
|
-
|
|
140
|
-
> ⚠️ self-check C5 — Instruction `<file>` is not referenced anywhere. Either delete it or add a "## References" line to the relevant skill(s).
|
|
141
|
-
|
|
142
|
-
### C6 — Cross-links resolve
|
|
143
|
-
|
|
144
|
-
For every markdown link with a relative target (`](./...)`, `](../...)`, or `](path/...)` not starting with `http`) inside `plugin/**/*.md`: target must exist on disk relative to the file containing it.
|
|
145
|
-
|
|
146
|
-
> ⚠️ self-check C6 — `<source-file>:<line>` links to `<target>` which does not exist. Update or remove the link.
|
|
147
|
-
|
|
148
|
-
### C7 — Verbs table matches prompts
|
|
149
|
-
|
|
150
|
-
`README.md` contains a "Verbs" / "Quick start" table. Every `plugin/prompts/<verb>.prompt.md` must have a row in that table; every row in the table must have a corresponding prompt file.
|
|
151
|
-
|
|
152
|
-
> ⚠️ self-check C7 — Verb `<v>` exists as `plugin/prompts/<v>.prompt.md` but is missing from README's verbs table. Suggested row:
|
|
153
|
-
>
|
|
154
|
-
> `| \`<v>\` | <default window> | <one-line description> |`
|
|
155
|
-
|
|
156
|
-
### C8 — Distribution layout matches docs
|
|
157
|
-
|
|
158
|
-
`docs/reference/where-things-live.md` contains an ASCII tree of `plugin/`. Every top-level subdirectory and notable file under `plugin/` must appear; every entry in the tree must exist on disk.
|
|
159
|
-
|
|
160
|
-
> ⚠️ self-check C8 — `plugin/<x>` exists but is missing from the docs/reference/where-things-live.md tree. Add an entry under section "1. This repo".
|
|
161
|
-
|
|
162
|
-
### D-checks: see `run.ps1` for the exact regexes.
|
|
113
|
+
> Load on trigger: see [`references/algorithm.md`](references/algorithm.md) for per-check pseudocode and finding templates (C1–C8, D-checks).
|
|
163
114
|
|
|
164
115
|
## Validation rules
|
|
165
116
|
|
|
@@ -204,4 +155,8 @@ This skill is not invoked by `@Kushi <verb>` — it's a meta-skill. Triggers:
|
|
|
204
155
|
## References
|
|
205
156
|
|
|
206
157
|
- `instructions/citation-ledger.instructions.md` (findings cite source files / line numbers)
|
|
207
|
-
- `instructions/side-by-side-config.instructions.md` (D6 enforces this rule for skills that read/write user config)
|
|
158
|
+
- `instructions/side-by-side-config.instructions.md` (D6 enforces this rule for skills that read/write user config)
|
|
159
|
+
- `instructions/hooks.instructions.md` (D37 validates hooks system)
|
|
160
|
+
- `instructions/parallel-execution.instructions.md` (D38 validates parallel dispatch)
|
|
161
|
+
- `instructions/otel.instructions.md` (D39 validates OTel export)
|
|
162
|
+
- `instructions/schema-evolve.instructions.md` (D39 validates schema evolution)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Algorithm (per check)
|
|
2
|
+
|
|
3
|
+
### C1 — Frontmatter completeness
|
|
4
|
+
|
|
5
|
+
For each `plugin/skills/<dir>/SKILL.md`: parse YAML frontmatter (text between leading `---` lines). Required keys: `name`, `version`, `description`.
|
|
6
|
+
|
|
7
|
+
> ⚠️ self-check C1 — Skill `<dir>` is missing required frontmatter key `<key>`. Add it before the first `---` separator.
|
|
8
|
+
|
|
9
|
+
### C2 — Skill name matches directory
|
|
10
|
+
|
|
11
|
+
For each skill: `frontmatter.name` (case-insensitive) must equal directory name.
|
|
12
|
+
|
|
13
|
+
> ⚠️ self-check C2 — Directory `<dir>/` declares `name: "<name>"`. Rename one or the other so they match.
|
|
14
|
+
|
|
15
|
+
### C3 — Skills inventoried in agent
|
|
16
|
+
|
|
17
|
+
`plugin/agents/kushi.agent.md` must contain a backtick-wrapped reference to every skill directory name. Look in any markdown table column or any inline `` `<name>` `` token.
|
|
18
|
+
|
|
19
|
+
> ⚠️ self-check C3 — Skill `<dir>` is not listed in the kushi agent. Suggested row for the routing table:
|
|
20
|
+
>
|
|
21
|
+
> `| <verb> | <dir> | <one-line description from frontmatter> |`
|
|
22
|
+
|
|
23
|
+
### C4 — Prompts route to real skills
|
|
24
|
+
|
|
25
|
+
For each `plugin/prompts/*.prompt.md`: extract any reference of the form `` `<skill-name>` `` or `delegates to <skill-name>` (case-insensitive). Each must exist as a directory under `plugin/skills/`.
|
|
26
|
+
|
|
27
|
+
> ⚠️ self-check C4 — Prompt `<file>` references skill `<name>` which does not exist under `plugin/skills/`. Either create the skill or update the prompt.
|
|
28
|
+
|
|
29
|
+
### C5 — Instructions referenced
|
|
30
|
+
|
|
31
|
+
For each `plugin/instructions/*.instructions.md`: at least one SKILL.md OR `plugin/agents/kushi.agent.md` must reference it by filename (with or without path).
|
|
32
|
+
|
|
33
|
+
> ⚠️ self-check C5 — Instruction `<file>` is not referenced anywhere. Either delete it or add a "## References" line to the relevant skill(s).
|
|
34
|
+
|
|
35
|
+
### C6 — Cross-links resolve
|
|
36
|
+
|
|
37
|
+
For every markdown link with a relative target (`](./...)`, `](../...)`, or `](path/...)` not starting with `http`) inside `plugin/**/*.md`: target must exist on disk relative to the file containing it.
|
|
38
|
+
|
|
39
|
+
> ⚠️ self-check C6 — `<source-file>:<line>` links to `<target>` which does not exist. Update or remove the link.
|
|
40
|
+
|
|
41
|
+
### C7 — Verbs table matches prompts
|
|
42
|
+
|
|
43
|
+
`README.md` contains a "Verbs" / "Quick start" table. Every `plugin/prompts/<verb>.prompt.md` must have a row in that table; every row in the table must have a corresponding prompt file.
|
|
44
|
+
|
|
45
|
+
> ⚠️ self-check C7 — Verb `<v>` exists as `plugin/prompts/<v>.prompt.md` but is missing from README's verbs table. Suggested row:
|
|
46
|
+
>
|
|
47
|
+
> `| \`<v>\` | <default window> | <one-line description> |`
|
|
48
|
+
|
|
49
|
+
### C8 — Distribution layout matches docs
|
|
50
|
+
|
|
51
|
+
`docs/reference/where-things-live.md` contains an ASCII tree of `plugin/`. Every top-level subdirectory and notable file under `plugin/` must appear; every entry in the tree must exist on disk.
|
|
52
|
+
|
|
53
|
+
> ⚠️ self-check C8 — `plugin/<x>` exists but is missing from the docs/reference/where-things-live.md tree. Add an entry under section "1. This repo".
|
|
54
|
+
|
|
55
|
+
### D-checks: see `run.ps1` for the exact regexes.
|