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.
@@ -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
@@ -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
- ### C1 Frontmatter completeness
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.