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.
Files changed (43) hide show
  1. package/README.md +78 -0
  2. package/bin/cli.mjs +201 -1
  3. package/package.json +2 -2
  4. package/plugin/agents/kushi.agent.md +6 -2
  5. package/plugin/instructions/hooks.instructions.md +84 -0
  6. package/plugin/instructions/living-wiki.instructions.md +88 -0
  7. package/plugin/instructions/log-format.instructions.md +78 -0
  8. package/plugin/instructions/otel.instructions.md +75 -0
  9. package/plugin/instructions/parallel-execution.instructions.md +81 -0
  10. package/plugin/instructions/schema-evolve.instructions.md +73 -0
  11. package/plugin/instructions/wiki-lint.instructions.md +110 -0
  12. package/plugin/skills/_shared/Append-StateLog.ps1 +73 -0
  13. package/plugin/skills/_shared/Emit-OtelSpan.ps1 +111 -0
  14. package/plugin/skills/_shared/Invoke-Hooks.ps1 +177 -0
  15. package/plugin/skills/_shared/Update-StateIndex.ps1 +47 -0
  16. package/plugin/skills/_shared/hook-templates/console-debug.ps1 +15 -0
  17. package/plugin/skills/_shared/hook-templates/teams-notify.ps1 +47 -0
  18. package/plugin/skills/ask-project/SKILL.md +30 -0
  19. package/plugin/skills/build-state/SKILL.md +18 -2
  20. package/plugin/skills/lint-state/.created-by-skill-creator +0 -0
  21. package/plugin/skills/lint-state/SKILL.md +98 -0
  22. package/plugin/skills/lint-state/evals/evals.json +34 -0
  23. package/plugin/skills/lint-state/lint.ps1 +218 -0
  24. package/plugin/skills/refresh-project/SKILL.md +8 -4
  25. package/plugin/skills/schema-evolve/.created-by-skill-creator +0 -0
  26. package/plugin/skills/schema-evolve/SKILL.md +106 -0
  27. package/plugin/skills/schema-evolve/evals/evals.json +37 -0
  28. package/plugin/skills/self-check/SKILL.md +12 -55
  29. package/plugin/skills/self-check/references/algorithm.md +55 -0
  30. package/plugin/skills/self-check/run.ps1 +225 -3
  31. package/plugin/skills/skill-checker/check-skill.ps1 +1 -1
  32. package/plugin/skills/teach/.created-by-skill-creator +0 -0
  33. package/plugin/skills/teach/SKILL.md +77 -0
  34. package/plugin/skills/teach/evals/evals.json +37 -0
  35. package/plugin/templates/state/answers.README.md +7 -0
  36. package/plugin/templates/state/hot.template.md +12 -0
  37. package/plugin/templates/state/review-queue.template.md +10 -0
  38. package/src/eval-runner.test.mjs +1 -1
  39. package/src/hooks-dispatcher.test.mjs +135 -0
  40. package/src/otel-emit.test.mjs +73 -0
  41. package/src/parallel-refresh.test.mjs +50 -0
  42. package/src/schema-evolve.test.mjs +78 -0
  43. 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: "4.0.0"
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.
@@ -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
+ }