kushi-agents 5.0.4 → 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 +78 -0
- package/bin/cli.mjs +201 -1
- package/package.json +2 -2
- package/plugin/agents/kushi.agent.md +6 -2
- package/plugin/instructions/hooks.instructions.md +84 -0
- package/plugin/instructions/living-wiki.instructions.md +88 -0
- package/plugin/instructions/log-format.instructions.md +78 -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/instructions/wiki-lint.instructions.md +110 -0
- package/plugin/skills/_shared/Append-StateLog.ps1 +73 -0
- package/plugin/skills/_shared/Emit-OtelSpan.ps1 +111 -0
- package/plugin/skills/_shared/Invoke-Hooks.ps1 +177 -0
- package/plugin/skills/_shared/Update-StateIndex.ps1 +47 -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/ask-project/SKILL.md +30 -0
- package/plugin/skills/build-state/SKILL.md +18 -2
- package/plugin/skills/lint-state/.created-by-skill-creator +0 -0
- package/plugin/skills/lint-state/SKILL.md +98 -0
- package/plugin/skills/lint-state/evals/evals.json +34 -0
- package/plugin/skills/lint-state/lint.ps1 +218 -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 +12 -55
- package/plugin/skills/self-check/references/algorithm.md +55 -0
- package/plugin/skills/self-check/run.ps1 +225 -3
- package/plugin/skills/skill-checker/check-skill.ps1 +1 -1
- 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/plugin/templates/state/answers.README.md +7 -0
- package/plugin/templates/state/hot.template.md +12 -0
- package/plugin/templates/state/review-queue.template.md +10 -0
- package/src/eval-runner.test.mjs +1 -1
- 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,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,47 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Updates the "Last touched" pointer at the top of State/index.md per log-format.instructions.md.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Idempotent helper. Updates or inserts the "Last touched" line immediately after
|
|
7
|
+
the front-matter block in index.md.
|
|
8
|
+
|
|
9
|
+
.PARAMETER StateDir
|
|
10
|
+
Path to the State/ directory.
|
|
11
|
+
|
|
12
|
+
.PARAMETER Op
|
|
13
|
+
Operation name (same taxonomy as Append-StateLog).
|
|
14
|
+
#>
|
|
15
|
+
[CmdletBinding()]
|
|
16
|
+
param(
|
|
17
|
+
[Parameter(Mandatory)][string]$StateDir,
|
|
18
|
+
[Parameter(Mandatory)][string]$Op
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
$ErrorActionPreference = 'Stop'
|
|
22
|
+
|
|
23
|
+
$indexFile = Join-Path $StateDir 'index.md'
|
|
24
|
+
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm'
|
|
25
|
+
$pointer = "> Last touched: $timestamp by $Op ([log](log.md))"
|
|
26
|
+
|
|
27
|
+
if (-not (Test-Path $indexFile)) {
|
|
28
|
+
Write-Warning "index.md not found at $indexFile — skipping Update-StateIndex."
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
$content = Get-Content -Raw $indexFile
|
|
33
|
+
$pointerPattern = '(?m)^> Last touched:.*$'
|
|
34
|
+
|
|
35
|
+
if ($content -match $pointerPattern) {
|
|
36
|
+
$content = $content -replace $pointerPattern, $pointer
|
|
37
|
+
} else {
|
|
38
|
+
# Insert after front-matter closing ---
|
|
39
|
+
if ($content -match '(?ms)(^---\r?\n.*?\r?\n---\r?\n)(.*)$') {
|
|
40
|
+
$content = $Matches[1] + "`n" + $pointer + "`n" + $Matches[2]
|
|
41
|
+
} else {
|
|
42
|
+
$content = $pointer + "`n`n" + $content
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Set-Content -Path $indexFile -Value $content -Encoding utf8NoBOM
|
|
47
|
+
Write-Verbose "Updated index.md: Last touched $timestamp by $Op"
|
|
@@ -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)"
|
|
@@ -42,6 +42,36 @@ The user does NOT need to type `/ask-project` or `@Kushi ask`. If the message:
|
|
|
42
42
|
- **Freshness gate, not freshness auto-fix.** If the freshest source relevant to the question is older than `chat.freshness_warn_days` (default 14), warn the user and offer `@Kushi refresh <project>` — but NEVER auto-refresh.
|
|
43
43
|
- **Reference packs may be consulted for domain doctrine.** When the question maps to a known reference-pack domain (currently only `fde/` — FDE stages, fitness, CRM status meanings, intake gates, risk categories, "MACC", "is this FDE-fit"), ALSO load the matching reference pack as additional grounding using the 3-layer override order (project → user → packaged). Cite reference-pack assertions with `[source: reference-packs/<pack>/<file>.md · <layer>]` where layer is `packaged` / `user-override` / `project-override`. Reference-pack content NEVER overrides project Evidence/ for facts about *this* project; it only provides definitions, gates, and rubrics.
|
|
44
44
|
|
|
45
|
+
## --file-back option (v5.1.0+)
|
|
46
|
+
|
|
47
|
+
When the user passes `--file-back` (or says "file this answer back", "save this answer"):
|
|
48
|
+
|
|
49
|
+
1. After answering normally, write the Q+A to `Evidence/<alias>/State/answers/YYYY-MM-DD_<slug>.md`:
|
|
50
|
+
```markdown
|
|
51
|
+
---
|
|
52
|
+
question: "<the user's original question>"
|
|
53
|
+
asked_at: <ISO-8601 timestamp>
|
|
54
|
+
sources: [<list of cited source paths>]
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Answer
|
|
58
|
+
|
|
59
|
+
<the answer text, with citations preserved>
|
|
60
|
+
|
|
61
|
+
## Sources
|
|
62
|
+
|
|
63
|
+
- <source 1>
|
|
64
|
+
- <source 2>
|
|
65
|
+
```
|
|
66
|
+
2. The `<slug>` is derived from the question: lowercase, alphanumeric + hyphens, ≤ 60 chars.
|
|
67
|
+
3. Append `ask-fileback` entry to `State/log.md` via `Append-StateLog.ps1`:
|
|
68
|
+
- Title: `Filed answer: <slug>`
|
|
69
|
+
- Summary: one-line description of the question.
|
|
70
|
+
4. Update `State/index.md` via `Update-StateIndex.ps1 -Op ask-fileback`.
|
|
71
|
+
5. If `State/answers/` does not exist, create it.
|
|
72
|
+
|
|
73
|
+
The `--file-back` flag is OPTIONAL. Without it, ask-project behaves exactly as before (read-only, no writes).
|
|
74
|
+
|
|
45
75
|
## Inputs
|
|
46
76
|
|
|
47
77
|
- `<project>` — fuzzy-matched project name. If multiple plausible matches, ask the user.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "build-state"
|
|
3
|
-
version: "
|
|
3
|
+
version: "5.0.0"
|
|
4
4
|
description: "USE WHEN the user says \"regenerate State for <X>\", \"rebuild State\", or \"@Kushi state <X>\" AND the project already has Evidence/ populated. DO NOT USE to pull new evidence (use refresh-project or pull-*). Capability: pure re-render — reads Evidence/_index/entities.yml + weekly CSC + legacy fallback, writes <project>/State/ in BOTH legacy 00–09 synthesis (full profile) AND Karpathy layout (index.md + log.md + per-entity pages + CLAUDE.md/AGENTS.md). Plan-validate-execute writer."
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -56,7 +56,23 @@ runs — transient working artifact, not authoritative truth.
|
|
|
56
56
|
|
|
57
57
|
## Steps
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
|
|
60
|
+
## v5.1.0 — Incremental mode (HARD RULE; per `living-wiki.instructions.md`)
|
|
61
|
+
|
|
62
|
+
Build-state is now incremental by default:
|
|
63
|
+
|
|
64
|
+
1. **Read existing State/ pages.** Before regenerating, load every existing `State/**/*.md`.
|
|
65
|
+
2. **Preserve human edits.** Content OUTSIDE `<!-- kushi:auto:start -->` / `<!-- kushi:auto:end -->` fences is NEVER modified.
|
|
66
|
+
3. **Re-derive fenced regions only.** For each `<!-- kushi:auto:start section="<id>" -->` block, re-derive content from current evidence and replace the block contents.
|
|
67
|
+
4. **Contradiction detection.** When a re-derived fact contradicts an existing fact (different value for same entity property):
|
|
68
|
+
- Wrap both in `> [!warning] Contradicted` / `> [!info] New value` callouts per `living-wiki.instructions.md`.
|
|
69
|
+
- Add entity to `_review-queue.md`.
|
|
70
|
+
5. **Auto-resolve.** Apply the auto-resolve threshold from `living-wiki.instructions.md` (≥3 sources + ≥30 days old + no human override).
|
|
71
|
+
6. **Log + index.** Append `build-state` entry to `State/log.md` via `Append-StateLog.ps1`. Update `State/index.md` via `Update-StateIndex.ps1`.
|
|
72
|
+
|
|
73
|
+
When creating NEW pages (entity not previously in State/), wrap all auto-derived content in `<!-- kushi:auto -->` fences.
|
|
74
|
+
|
|
75
|
+
|
|
60
76
|
## Step checklist
|
|
61
77
|
|
|
62
78
|
Progress-trackable view of the steps below. Each `### Step` block expands the corresponding checkbox.
|
|
File without changes
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "lint-state"
|
|
3
|
+
version: "1.0.0"
|
|
4
|
+
description: "USE WHEN the user says 'lint state', 'check state health', 'wiki lint', 'kushi lint <project>', or 'find stale/orphan/contradictions in State'. DO NOT USE for evidence-level validation (use self-check) or for rebuilding State (use build-state). Capability: runs wiki-lint finding classes against Evidence/<alias>/State/, writes dated lint report, updates log.md + index.md."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Skill: lint-state
|
|
8
|
+
|
|
9
|
+
Run wiki-lint checks against a project's `State/` directory per `wiki-lint.instructions.md`. Reports contradictions, stale claims, orphan pages, missing cross-refs, and data gaps — each with a ready-to-paste fix snippet.
|
|
10
|
+
|
|
11
|
+
## Triggers
|
|
12
|
+
|
|
13
|
+
- "lint state for `<project>`"
|
|
14
|
+
- "check state health"
|
|
15
|
+
- "wiki lint `<project>`"
|
|
16
|
+
- `kushi lint <project>` (CLI verb)
|
|
17
|
+
- "find contradictions in State"
|
|
18
|
+
- "any stale claims in `<project>`?"
|
|
19
|
+
|
|
20
|
+
## Inputs
|
|
21
|
+
|
|
22
|
+
- `<project>` — engagement name (fuzzy-matched per `engagement-root-resolution.instructions.md`).
|
|
23
|
+
|
|
24
|
+
## Step checklist
|
|
25
|
+
|
|
26
|
+
- [ ] Step 1 — Resolve project root + State/ path
|
|
27
|
+
- [ ] Step 2 — Run finding-class checks
|
|
28
|
+
- [ ] Step 3 — Generate lint report
|
|
29
|
+
- [ ] Step 4 — Update log.md + index.md
|
|
30
|
+
- [ ] Step 5 — Print summary
|
|
31
|
+
|
|
32
|
+
### Step 1 — Resolve project root
|
|
33
|
+
|
|
34
|
+
Resolve `<engagement-root>/<project>/Evidence/<alias>/State/` using standard engagement-root resolution. Verify `State/` exists; if not, advise running `build-state` first.
|
|
35
|
+
|
|
36
|
+
### Step 2 — Run finding-class checks
|
|
37
|
+
|
|
38
|
+
Per `wiki-lint.instructions.md`, execute each finding class:
|
|
39
|
+
|
|
40
|
+
1. **contradiction-flagged**: scan `State/**/*.md` for `> [!warning] Contradicted` callouts.
|
|
41
|
+
2. **stale-claim**: parse citations, flag entities with newest citation > 60 days old.
|
|
42
|
+
3. **orphan-page**: find pages with `kushi_state_page: true` not linked from index.md or siblings.
|
|
43
|
+
4. **missing-cross-ref**: cross-reference entity mentions against frontmatter `entity_ids`/`related`.
|
|
44
|
+
5. **data-gap**: find section headers with empty/placeholder bodies.
|
|
45
|
+
|
|
46
|
+
### Step 3 — Generate lint report
|
|
47
|
+
|
|
48
|
+
Write `Evidence/<alias>/State/reports/lint-YYYY-MM-DD.md`:
|
|
49
|
+
|
|
50
|
+
```markdown
|
|
51
|
+
---
|
|
52
|
+
generated_at: "YYYY-MM-DDTHH:MM:SSZ"
|
|
53
|
+
generated_by: "lint-state v1.0.0"
|
|
54
|
+
findings_count: N
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
# Lint Report — YYYY-MM-DD
|
|
58
|
+
|
|
59
|
+
## Summary
|
|
60
|
+
|
|
61
|
+
| Class | Count | Severity |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| contradiction-flagged | N | warning |
|
|
64
|
+
| stale-claim | N | warning |
|
|
65
|
+
| orphan-page | N | warning |
|
|
66
|
+
| missing-cross-ref | N | info |
|
|
67
|
+
| data-gap | N | info |
|
|
68
|
+
|
|
69
|
+
**Total findings: N**
|
|
70
|
+
|
|
71
|
+
## Findings
|
|
72
|
+
|
|
73
|
+
### contradiction-flagged: <entity> — <description>
|
|
74
|
+
...per wiki-lint.instructions.md format...
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Step 4 — Update log.md + index.md
|
|
78
|
+
|
|
79
|
+
Call shared helpers:
|
|
80
|
+
- `Append-StateLog.ps1 -Op lint-state -Title "Lint pass (<N> findings)"`
|
|
81
|
+
- `Update-StateIndex.ps1 -Op lint-state`
|
|
82
|
+
|
|
83
|
+
### Step 5 — Print summary
|
|
84
|
+
|
|
85
|
+
One-line console output: `lint-state: <N> findings (<breakdown by class>). Report: State/reports/lint-YYYY-MM-DD.md`
|
|
86
|
+
|
|
87
|
+
## Validation loop
|
|
88
|
+
|
|
89
|
+
After writing outputs:
|
|
90
|
+
1. Verify `State/reports/lint-YYYY-MM-DD.md` exists and has valid frontmatter.
|
|
91
|
+
2. Verify `State/log.md` has the new entry at top.
|
|
92
|
+
3. Verify `State/index.md` "Last touched" pointer is updated.
|
|
93
|
+
|
|
94
|
+
## References
|
|
95
|
+
|
|
96
|
+
- `../../instructions/wiki-lint.instructions.md`
|
|
97
|
+
- `../../instructions/log-format.instructions.md`
|
|
98
|
+
- `../../instructions/living-wiki.instructions.md`
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill": "lint-state",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Wiki-lint checks against State/ — contradictions, stale claims, orphans, cross-refs, data gaps.",
|
|
5
|
+
"cases": [
|
|
6
|
+
{
|
|
7
|
+
"id": "lint-state-clean",
|
|
8
|
+
"name": "Lint a clean State/ with no findings",
|
|
9
|
+
"input": "Run lint against a well-maintained State/ directory with no contradictions, stale claims, or orphans.",
|
|
10
|
+
"expected_assertions": [
|
|
11
|
+
{ "type": "regex-match", "target": "stdout", "pattern": "lint-state: 0 findings" }
|
|
12
|
+
],
|
|
13
|
+
"grader_type": "script"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "lint-state-contradiction",
|
|
17
|
+
"name": "Lint detects contradiction callouts",
|
|
18
|
+
"input": "Run lint against a State/ directory with one unresolved > [!warning] Contradicted callout.",
|
|
19
|
+
"expected_assertions": [
|
|
20
|
+
{ "type": "regex-match", "target": "stdout", "pattern": "contradiction-flagged=1" }
|
|
21
|
+
],
|
|
22
|
+
"grader_type": "script"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "lint-state-orphan",
|
|
26
|
+
"name": "Lint detects orphan pages",
|
|
27
|
+
"input": "Run lint against a State/ directory with a page that has kushi_state_page: true but is not linked from index.md.",
|
|
28
|
+
"expected_assertions": [
|
|
29
|
+
{ "type": "regex-match", "target": "stdout", "pattern": "orphan-page=1" }
|
|
30
|
+
],
|
|
31
|
+
"grader_type": "script"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|