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
@@ -120,7 +120,7 @@ if (-not (Test-Path $pluginDir)) {
120
120
  exit 2
121
121
  }
122
122
 
123
- $skillDirs = Get-ChildItem $skillsDir -Directory | Sort-Object Name
123
+ $skillDirs = Get-ChildItem $skillsDir -Directory | Where-Object { $_.Name -notmatch '^\.' -and $_.Name -notmatch '^_' } | Sort-Object Name
124
124
  $skillNames = $skillDirs.Name
125
125
  $instructionFiles = Get-ChildItem $instructionsDir -Filter '*.instructions.md' -EA SilentlyContinue
126
126
  $promptFiles = Get-ChildItem $promptsDir -Filter '*.prompt.md' -EA SilentlyContinue
@@ -264,7 +264,7 @@ if (Test-Path $pluginJsonFile) {
264
264
  $chain += $name
265
265
  return $chain
266
266
  }
267
- $skillNames = (Get-ChildItem $skillsDir -Directory).Name
267
+ $skillNames = (Get-ChildItem $skillsDir -Directory | Where-Object { $_.Name -notmatch '^\.' -and $_.Name -notmatch '^_' }).Name
268
268
  $promptStems = (Get-ChildItem (Join-Path $pluginDir 'prompts') -File -Filter '*.prompt.md' -ErrorAction SilentlyContinue) `
269
269
  | ForEach-Object { $_.Name -replace '\.prompt\.md$','' }
270
270
  $instructionStems = (Get-ChildItem (Join-Path $pluginDir 'instructions') -File -Filter '*.instructions.md' -ErrorAction SilentlyContinue) `
@@ -1663,7 +1663,7 @@ process.stdout.write(JSON.stringify(out));
1663
1663
  }
1664
1664
 
1665
1665
  $skillsRoot = Join-Path $Root 'plugin/skills'
1666
- $skillDirs = Get-ChildItem -Path $skillsRoot -Directory | Where-Object { $_.Name -notin @('eval', 'self-check') }
1666
+ $skillDirs = Get-ChildItem -Path $skillsRoot -Directory | Where-Object { $_.Name -notin @('eval', 'self-check') -and $_.Name -notmatch '^_' }
1667
1667
  foreach ($sd in $skillDirs) {
1668
1668
  $evalsFile = Join-Path $sd.FullName 'evals/evals.json'
1669
1669
  if (-not (Test-Path $evalsFile)) {
@@ -1778,6 +1778,228 @@ process.stdout.write(JSON.stringify(out));
1778
1778
  }
1779
1779
  }
1780
1780
 
1781
+ # === D35.log — State/log.md format and presence (v5.1.0+) ===
1782
+ # Validates that writer-touched State dirs have log.md and that entries match canonical format.
1783
+ # Operates on .testtmp/ fixtures or real engagement roots.
1784
+ $logFixtureDir = Join-Path $Root '.testtmp'
1785
+ $logTestDirs = @()
1786
+ if (Test-Path $logFixtureDir) {
1787
+ $logTestDirs += Get-ChildItem -Path $logFixtureDir -Recurse -Directory -ErrorAction SilentlyContinue |
1788
+ Where-Object { $_.Name -eq 'State' -and (Test-Path (Join-Path $_.FullName 'log.md')) }
1789
+ }
1790
+ # Also check real engagement roots if available
1791
+ foreach ($engRoot in (Resolve-EngagementRoots -RepoRoot $Root)) {
1792
+ Get-ChildItem -Path $engRoot -Directory -ErrorAction SilentlyContinue | ForEach-Object {
1793
+ $stateDir = Join-Path $_.FullName 'State'
1794
+ if (Test-Path $stateDir) { $logTestDirs += Get-Item $stateDir }
1795
+ }
1796
+ }
1797
+
1798
+ foreach ($sd in $logTestDirs) {
1799
+ $logMd = Join-Path $sd.FullName 'log.md'
1800
+
1801
+ # D35.log-exists
1802
+ if (-not (Test-Path $logMd)) {
1803
+ Add-Finding 'D35.log-exists' 'State log' 'warning' "State directory '$($sd.FullName)' has no log.md" "Add log.md per log-format.instructions.md. Run build-state to auto-create it." $sd.FullName 0
1804
+ continue
1805
+ }
1806
+
1807
+ # D35.log-format — every ## [ heading matches canonical format
1808
+ $logLines = Get-Content -Path $logMd -ErrorAction SilentlyContinue
1809
+ $prevTs = $null
1810
+ $formatOk = $true
1811
+ $monotonicOk = $true
1812
+ foreach ($line in $logLines) {
1813
+ if ($line -match '^\#\#\s+\[') {
1814
+ if ($line -notmatch '^\#\#\s+\[\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\]\s+\S+\s+\|\s+.+$') {
1815
+ Add-Finding 'D35.log-format' 'State log' 'warning' "log.md heading does not match canonical format: $line" "Heading must be: ## [YYYY-MM-DD HH:MM] <op> | <title>. See log-format.instructions.md." $logMd 0
1816
+ $formatOk = $false
1817
+ break
1818
+ }
1819
+ # D35.log-monotonic — timestamps non-decreasing (newest first = decreasing)
1820
+ if ($line -match '^\#\#\s+\[(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})\]') {
1821
+ try {
1822
+ $ts = [datetime]::ParseExact($Matches[1], 'yyyy-MM-dd HH:mm', $null)
1823
+ if ($prevTs -and $ts -gt $prevTs) {
1824
+ Add-Finding 'D35.log-monotonic' 'State log' 'warning' "log.md timestamps not reverse-chronological: $($Matches[1]) appears after an earlier timestamp" "Entries should be prepended (newest first). Re-order or re-run build-state." $logMd 0
1825
+ $monotonicOk = $false
1826
+ break
1827
+ }
1828
+ $prevTs = $ts
1829
+ } catch {}
1830
+ }
1831
+ }
1832
+ }
1833
+ }
1834
+
1835
+ # === D36.contradictions — contradiction handling integrity (v5.1.0+) ===
1836
+ # Validates callout syntax, review-queue freshness, and build-state non-fenced preservation.
1837
+ $contradictionDirs = @()
1838
+ if (Test-Path $logFixtureDir) {
1839
+ $contradictionDirs += Get-ChildItem -Path $logFixtureDir -Recurse -Directory -ErrorAction SilentlyContinue |
1840
+ Where-Object { $_.Name -eq 'State' }
1841
+ }
1842
+ foreach ($engRoot in (Resolve-EngagementRoots -RepoRoot $Root)) {
1843
+ Get-ChildItem -Path $engRoot -Directory -ErrorAction SilentlyContinue | ForEach-Object {
1844
+ $stateDir = Join-Path $_.FullName 'State'
1845
+ if (Test-Path $stateDir) { $contradictionDirs += Get-Item $stateDir }
1846
+ }
1847
+ }
1848
+
1849
+ foreach ($sd in $contradictionDirs) {
1850
+ $mdFiles = Get-ChildItem -Path $sd.FullName -Recurse -Filter '*.md' -ErrorAction SilentlyContinue
1851
+
1852
+ # D36.callout-syntax — every > [!warning] Contradicted callout has required structure
1853
+ foreach ($mf in $mdFiles) {
1854
+ $content = Get-Content -Raw $mf.FullName -ErrorAction SilentlyContinue
1855
+ if (-not $content) { continue }
1856
+ if ($content -match '>\s*\[!warning\]\s*Contradicted') {
1857
+ # Must have 'by' clause and a following > [!info] block
1858
+ if ($content -notmatch '>\s*\[!warning\]\s*Contradicted\s+by\s+') {
1859
+ Add-Finding 'D36.callout-syntax' 'Contradictions' 'warning' "Callout in $($mf.Name) missing 'by <source>' clause" "Format: > [!warning] Contradicted by <skill> run <date>. See living-wiki.instructions.md." $mf.FullName 0
1860
+ }
1861
+ }
1862
+ }
1863
+
1864
+ # D36.review-queue-fresh — if _review-queue.md has open items, most recent log entry is within 30 days
1865
+ $reviewQueue = Join-Path $sd.FullName '_review-queue.md'
1866
+ if (Test-Path $reviewQueue) {
1867
+ $rqContent = Get-Content -Raw $reviewQueue -ErrorAction SilentlyContinue
1868
+ # Check if review-queue has data rows (not just header + separator)
1869
+ $rqLines = ($rqContent -split '\r?\n') | Where-Object { $_ -match '^\|' -and $_ -notmatch '^\|\s*-' -and $_ -notmatch '^\|\s*Entity' }
1870
+ if ($rqLines.Count -gt 0) {
1871
+ # Has table rows (open items)
1872
+ $logMd2 = Join-Path $sd.FullName 'log.md'
1873
+ if (Test-Path $logMd2) {
1874
+ $logContent2 = Get-Content -Raw $logMd2
1875
+ $recentEntry = $false
1876
+ if ($logContent2 -match '(?m)^\#\#\s+\[(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})\]') {
1877
+ try {
1878
+ $lastTs = [datetime]::ParseExact($Matches[1], 'yyyy-MM-dd HH:mm', $null)
1879
+ if (((Get-Date) - $lastTs).TotalDays -le 30) { $recentEntry = $true }
1880
+ } catch {}
1881
+ }
1882
+ if (-not $recentEntry) {
1883
+ Add-Finding 'D36.review-queue-fresh' 'Contradictions' 'warning' "_review-queue.md has open items but most recent log entry is >30 days old" "Run 'lint-state' or 'build-state' to refresh. Open contradictions need periodic review." $reviewQueue 0
1884
+ }
1885
+ }
1886
+ }
1887
+ }
1888
+
1889
+ # D36.no-silent-overwrite — build-state preserves non-fenced regions
1890
+ # This check validates the invariant by looking for the fencing pattern.
1891
+ # If State pages exist with kushi:auto fences AND content outside fences,
1892
+ # verify the file has not been fully regenerated (check mtime consistency).
1893
+ # For the fixture-based test: run build-state twice, assert non-fenced bytes unchanged.
1894
+ # In self-check, we do a structural check: if a page has content outside fences,
1895
+ # verify it also has content INSIDE fences (indicating incremental mode is active).
1896
+ foreach ($mf in $mdFiles) {
1897
+ $content = Get-Content -Raw $mf.FullName -ErrorAction SilentlyContinue
1898
+ if (-not $content) { continue }
1899
+ if ($content -notmatch 'kushi_state_page:\s*true') { continue }
1900
+ if ($content -match '<!-- kushi:auto:start') {
1901
+ # Good — page uses fencing. Verify fence pairs are balanced.
1902
+ $starts = ([regex]::Matches($content, '<!-- kushi:auto:start')).Count
1903
+ $ends = ([regex]::Matches($content, '<!-- kushi:auto:end')).Count
1904
+ if ($starts -ne $ends) {
1905
+ Add-Finding 'D36.no-silent-overwrite' 'Contradictions' 'warning' "$($mf.Name) has mismatched kushi:auto fence pairs ($starts starts, $ends ends)" "Every <!-- kushi:auto:start --> must have a matching <!-- kushi:auto:end -->. Fix the fencing." $mf.FullName 0
1906
+ }
1907
+ }
1908
+ }
1909
+ }
1910
+
1911
+ # === D37.hooks — Hooks system integrity (v5.2.0+) ===
1912
+
1913
+ # D37.hooks-doctrine-exists
1914
+ $hooksDoctrine = Join-Path $instructionsDir 'hooks.instructions.md'
1915
+ if (-not (Test-Path $hooksDoctrine)) {
1916
+ Add-Finding 'D37.hooks-doctrine-exists' 'Hooks' 'warning' "hooks.instructions.md is missing" "Create plugin/instructions/hooks.instructions.md per v5.2.0 spec." $hooksDoctrine 0
1917
+ }
1918
+
1919
+ # D37.hooks-helper-exists
1920
+ $hooksHelper = Join-Path $skillsDir '_shared\Invoke-Hooks.ps1'
1921
+ if (-not (Test-Path $hooksHelper)) {
1922
+ Add-Finding 'D37.hooks-helper-exists' 'Hooks' 'warning' "Invoke-Hooks.ps1 shared helper is missing" "Create plugin/skills/_shared/Invoke-Hooks.ps1 per hooks.instructions.md." $hooksHelper 0
1923
+ }
1924
+
1925
+ # D37.hooks-templates-exist
1926
+ $templatesDir = Join-Path $skillsDir '_shared\hook-templates'
1927
+ if (-not (Test-Path $templatesDir)) {
1928
+ Add-Finding 'D37.hooks-templates-exist' 'Hooks' 'warning' "hook-templates/ directory is missing" "Create plugin/skills/_shared/hook-templates/ with at least teams-notify.ps1 and console-debug.ps1." $templatesDir 0
1929
+ } else {
1930
+ $templates = Get-ChildItem -Path $templatesDir -Filter '*.ps1' -ErrorAction SilentlyContinue
1931
+ if ($templates.Count -lt 2) {
1932
+ Add-Finding 'D37.hooks-templates-exist' 'Hooks' 'warning' "hook-templates/ has fewer than 2 templates (found $($templates.Count))" "Add teams-notify.ps1 and console-debug.ps1 templates." $templatesDir 0
1933
+ }
1934
+ }
1935
+
1936
+ # D37.hooks-log-format: if any fixture has a hooks-log.md, headings parse correctly
1937
+ $hooksLogFixtures = @()
1938
+ if (Test-Path $logFixtureDir) {
1939
+ $hooksLogFixtures += Get-ChildItem -Path $logFixtureDir -Recurse -Filter 'hooks-log.md' -ErrorAction SilentlyContinue
1940
+ }
1941
+ foreach ($engRoot in (Resolve-EngagementRoots -RepoRoot $Root)) {
1942
+ $hooksLogFixtures += Get-ChildItem -Path $engRoot -Recurse -Filter 'hooks-log.md' -ErrorAction SilentlyContinue
1943
+ }
1944
+ foreach ($hlf in $hooksLogFixtures) {
1945
+ $hlContent = Get-Content -Path $hlf.FullName -ErrorAction SilentlyContinue
1946
+ foreach ($line in $hlContent) {
1947
+ if ($line -match '^\#\#\s+\[') {
1948
+ if ($line -notmatch '^\#\#\s+\[\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\]\s+\S+\s+\|\s+hook:\s+.+$') {
1949
+ Add-Finding 'D37.hooks-log-format' 'Hooks' 'warning' "hooks-log.md heading does not match canonical format: $line" "Heading must be: ## [YYYY-MM-DD HH:MM] <event> | hook: <name>. See hooks.instructions.md." $hlf.FullName 0
1950
+ break
1951
+ }
1952
+ }
1953
+ }
1954
+ }
1955
+
1956
+ # === D38.parallel — Parallel execution integrity (v5.2.0+) ===
1957
+
1958
+ # D38.parallel-doctrine-exists
1959
+ $parallelDoctrine = Join-Path $instructionsDir 'parallel-execution.instructions.md'
1960
+ if (-not (Test-Path $parallelDoctrine)) {
1961
+ Add-Finding 'D38.parallel-doctrine-exists' 'Parallel' 'warning' "parallel-execution.instructions.md is missing" "Create plugin/instructions/parallel-execution.instructions.md per v5.2.0 spec." $parallelDoctrine 0
1962
+ }
1963
+
1964
+ # D38.refresh-supports-parallel: grep for parallel/ThreadJob/workers in refresh-project
1965
+ $refreshSkill = Join-Path $skillsDir 'refresh-project\SKILL.md'
1966
+ if (Test-Path $refreshSkill) {
1967
+ $refreshContent = Get-Content -Raw $refreshSkill
1968
+ if ($refreshContent -notmatch 'parallel|ThreadJob|worker|concurrent' -and $refreshContent -notmatch 'parallel-execution\.instructions\.md') {
1969
+ Add-Finding 'D38.refresh-supports-parallel' 'Parallel' 'warning' "refresh-project/SKILL.md does not reference parallel dispatch" "Add parallel dispatch documentation per parallel-execution.instructions.md." $refreshSkill 0
1970
+ }
1971
+ }
1972
+
1973
+ # D38.deterministic-order: verify parallel doctrine documents deterministic ordering
1974
+ if (Test-Path $parallelDoctrine) {
1975
+ $pdContent = Get-Content -Raw $parallelDoctrine
1976
+ if ($pdContent -notmatch 'deterministic|canonical.*order') {
1977
+ Add-Finding 'D38.deterministic-order' 'Parallel' 'warning' "parallel-execution doctrine does not document deterministic output ordering" "Add a section on deterministic ordering (canonical source order regardless of completion order)." $parallelDoctrine 0
1978
+ }
1979
+ }
1980
+
1981
+ # === D39.otel — OpenTelemetry export integrity (v5.2.0+) ===
1982
+
1983
+ # D39.otel-doctrine-exists
1984
+ $otelDoctrine = Join-Path $instructionsDir 'otel.instructions.md'
1985
+ if (-not (Test-Path $otelDoctrine)) {
1986
+ Add-Finding 'D39.otel-doctrine-exists' 'OTel' 'warning' "otel.instructions.md is missing" "Create plugin/instructions/otel.instructions.md per v5.2.0 spec." $otelDoctrine 0
1987
+ }
1988
+
1989
+ # D39.otel-helper-exists
1990
+ $otelHelper = Join-Path $skillsDir '_shared\Emit-OtelSpan.ps1'
1991
+ if (-not (Test-Path $otelHelper)) {
1992
+ Add-Finding 'D39.otel-helper-exists' 'OTel' 'warning' "Emit-OtelSpan.ps1 shared helper is missing" "Create plugin/skills/_shared/Emit-OtelSpan.ps1 per otel.instructions.md." $otelHelper 0
1993
+ }
1994
+
1995
+ # D39.otel-noop-when-unset: helper exits cleanly when KUSHI_OTEL_ENDPOINT is empty
1996
+ if (Test-Path $otelHelper) {
1997
+ $otelContent = Get-Content -Raw $otelHelper
1998
+ if ($otelContent -notmatch 'KUSHI_OTEL_ENDPOINT.*return|return.*KUSHI_OTEL_ENDPOINT|\-not \$endpoint.*return') {
1999
+ Add-Finding 'D39.otel-noop-when-unset' 'OTel' 'warning' "Emit-OtelSpan.ps1 does not appear to short-circuit when KUSHI_OTEL_ENDPOINT is unset" "Add early return when endpoint is empty: if (-not \$env:KUSHI_OTEL_ENDPOINT) { return }" $otelHelper 0
2000
+ }
2001
+ }
2002
+
1781
2003
  # === Output ===
1782
2004
  if ($Targeted) {
1783
2005
  # Filter findings to those whose code, surface, file path, or message contain the substring.
@@ -88,7 +88,7 @@ function Get-TargetSkills {
88
88
  return @(Get-Item -LiteralPath $d)
89
89
  }
90
90
  if ($All) {
91
- return Get-ChildItem -Path $skillsRoot -Directory | Where-Object { $excludeFromAll -notcontains $_.Name }
91
+ return Get-ChildItem -Path $skillsRoot -Directory | Where-Object { $excludeFromAll -notcontains $_.Name -and $_.Name -notmatch '^_' }
92
92
  }
93
93
  throw "Specify -Skill <name> or -All."
94
94
  }
File without changes
@@ -0,0 +1,77 @@
1
+ ---
2
+ name: "teach"
3
+ version: "1.0.0"
4
+ description: "USE WHEN the user asks 'explain how kushi does X', 'why does refresh do Y', 'what is the difference between A and B in kushi', 'kushi explain <topic>', or wants to understand a kushi concept. DO NOT USE for modifying state/evidence (use build-state/refresh) or for project Q&A (use ask-project). Capability: pure pedagogical output — loads relevant doctrine + genealogy, explains concepts with cross-references, never modifies any State or Evidence."
5
+ ---
6
+
7
+ # Skill: teach
8
+
9
+ Pure pedagogical skill. Explains kushi concepts, architecture decisions, and operational patterns by loading relevant doctrine snippets, cross-references, and genealogy entries.
10
+
11
+ ## Triggers
12
+
13
+ - "explain how kushi does X"
14
+ - "why does refresh do Y"
15
+ - "what's the difference between A and B in kushi"
16
+ - `kushi explain <topic>` (CLI verb)
17
+ - "how does kushi handle contradictions?"
18
+ - "teach me about the CSC format"
19
+
20
+ ## Inputs
21
+
22
+ - `<topic>` — free-text topic (fuzzy-matched to doctrine clusters below).
23
+
24
+ ## Topic → Doctrine mapping
25
+
26
+ | Topic keyword(s) | Primary doctrine | Supporting files |
27
+ |------------------|-----------------|-----------------|
28
+ | contradictions, conflicts | `living-wiki.instructions.md` | `wiki-lint.instructions.md`, `State/_review-queue.md` format |
29
+ | refresh, pull, sources | `refresh-project/SKILL.md` | `verbatim-by-default.instructions.md`, `parallel-execution.instructions.md` |
30
+ | state, build-state, wiki | `living-wiki.instructions.md` | `karpathy-state-layout.instructions.md`, `log-format.instructions.md` |
31
+ | hooks, events, webhooks | `hooks.instructions.md` | `Invoke-Hooks.ps1` |
32
+ | parallel, workers, speed | `parallel-execution.instructions.md` | `auth-and-retry.instructions.md` |
33
+ | otel, telemetry, tracing | `otel.instructions.md` | `Emit-OtelSpan.ps1` |
34
+ | csc, capture, weekly | `comprehensive-structured-capture.instructions.md` | `weekly-csc.instructions.md` |
35
+ | graph, entities, links | `entity-graph.instructions.md` | `link-entities/SKILL.md` |
36
+ | workiq, m365 | `workiq-only.instructions.md` | `workiq-input-sanitization.instructions.md` |
37
+ | schema, conventions, remember | `schema-evolve.instructions.md` | `karpathy-state-layout.instructions.md` |
38
+ | install, setup, hosts | `multi-host-install.instructions.md` | `host-portability.instructions.md` |
39
+ | evals, testing | `skill-evals.instructions.md` | `skill-authoring.instructions.md` |
40
+
41
+ ## Procedure
42
+
43
+ 1. **Match topic** — fuzzy-match user's topic to doctrine cluster. If no match, list available topics with one-line descriptions.
44
+ 2. **Load doctrine** — read the primary doctrine file + first 20 lines of each supporting file.
45
+ 3. **Load genealogy** — find the release that introduced the feature in `docs/genealogy.md`.
46
+ 4. **Explain** — synthesize a clear explanation with:
47
+ - What it does (2-3 sentences)
48
+ - Why it exists (the problem it solved — from genealogy)
49
+ - How it works (key mechanics from doctrine)
50
+ - Where to look (file paths)
51
+ - Related concepts (cross-references)
52
+ 5. **Never modify** — this skill is read-only. No writes to State/, Evidence/, or any file.
53
+
54
+ ## Gotchas
55
+
56
+ 1. **Topic not found**: List available topics with `kushi explain --list`. Don't guess.
57
+ 2. **Too broad**: If topic matches multiple clusters, ask user to narrow down.
58
+ 3. **Version-specific**: Always note which version introduced the feature.
59
+ 4. **No project context needed**: teach operates on the kushi repo itself, not on engagement evidence.
60
+ 5. **Doctrine drift**: Always read the file live — don't cache doctrine content.
61
+
62
+ ## Validation loop
63
+
64
+ After generating explanation:
65
+ 1. Verify all cited file paths exist (glob check).
66
+ 2. Verify genealogy entry referenced is real.
67
+ 3. If a path doesn't exist, remove the citation rather than citing a phantom file.
68
+
69
+ ## References
70
+
71
+ - `release-genealogy.instructions.md` — genealogy format
72
+ - `skill-authoring.instructions.md` — skill structure conventions
73
+ - `docs/genealogy.md` — release lineage
74
+
75
+ ## Issue Recovery
76
+
77
+ When this skill exposes a reusable defect (doctrine gap, missing cross-reference, stale genealogy), 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": "teach",
3
+ "cases": [
4
+ {
5
+ "id": "teach-contradictions",
6
+ "name": "Explain contradictions handling",
7
+ "input": "kushi explain contradictions",
8
+ "expected_assertions": [
9
+ { "type": "contains", "value": "living-wiki" },
10
+ { "type": "contains", "value": "callout" },
11
+ { "type": "contains", "value": "_review-queue" }
12
+ ],
13
+ "grader_type": "script"
14
+ },
15
+ {
16
+ "id": "teach-parallel",
17
+ "name": "Explain parallel refresh",
18
+ "input": "explain how parallel pulls work in kushi",
19
+ "expected_assertions": [
20
+ { "type": "contains", "value": "parallel" },
21
+ { "type": "contains", "value": "worker" },
22
+ { "type": "contains", "value": "max_workers" }
23
+ ],
24
+ "grader_type": "script"
25
+ },
26
+ {
27
+ "id": "teach-unknown-topic",
28
+ "name": "Unknown topic suggests alternatives",
29
+ "input": "kushi explain quantum-entanglement",
30
+ "expected_assertions": [
31
+ { "type": "not_contains", "value": "quantum" },
32
+ { "type": "contains", "value": "available" }
33
+ ],
34
+ "grader_type": "script"
35
+ }
36
+ ]
37
+ }
@@ -0,0 +1,7 @@
1
+ # State/answers/
2
+
3
+ This folder contains filed-back Q&A answers from `ask-project --file-back`.
4
+
5
+ Each file is named `YYYY-MM-DD_<slug>.md` and contains the question, answer, and source citations.
6
+
7
+ These are durable knowledge artifacts — they persist across refreshes and serve as a queryable FAQ for the project.
@@ -0,0 +1,12 @@
1
+ ---
2
+ kushi_state_hot: true
3
+ generated_at: "{{generated_at}}"
4
+ ---
5
+
6
+ # Hot — {{project}}
7
+
8
+ > _Auto-generated. Entities touched in the last 7 days, ranked by recency. Content between fences is regenerated on every build-state run._
9
+
10
+ <!-- kushi:auto:start section="hot-entities" -->
11
+ {{hot_entities}}
12
+ <!-- kushi:auto:end section="hot-entities" -->
@@ -0,0 +1,10 @@
1
+ ---
2
+ kushi_state_review_queue: true
3
+ ---
4
+
5
+ # Review Queue
6
+
7
+ > _Open contradictions requiring human review. Updated by build-state when contradictions are detected. Cleared when resolved (auto or manual)._
8
+
9
+ | Entity | Property | Old value | New value | Flagged | Sources |
10
+ |---|---|---|---|---|---|
@@ -35,7 +35,7 @@ test('eval-runner: evals.schema.json validates structurally', () => {
35
35
  test('eval-runner: every plugin/skills/<name>/ (except eval/) has evals/evals.json that parses', () => {
36
36
  const skillsDir = path.join(repoRoot, 'plugin/skills');
37
37
  const skills = fs.readdirSync(skillsDir, { withFileTypes: true })
38
- .filter((d) => d.isDirectory() && d.name !== 'eval')
38
+ .filter((d) => d.isDirectory() && d.name !== 'eval' && !d.name.startsWith('_'))
39
39
  .map((d) => d.name);
40
40
  const missing = [];
41
41
  for (const skill of skills) {
@@ -0,0 +1,135 @@
1
+ // kushi v5.2.0 — hooks dispatcher tests.
2
+ // Validates Invoke-Hooks.ps1 behavior: ordering, failure isolation, payload format.
3
+
4
+ import test from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { spawnSync } from 'node:child_process';
10
+
11
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
12
+ const invokeHooks = path.join(repoRoot, 'plugin', 'skills', '_shared', 'Invoke-Hooks.ps1');
13
+
14
+ function setupFixture(label) {
15
+ const root = path.resolve(repoRoot, '.testtmp', `hooks-${label}-${Date.now()}`);
16
+ const hooksDir = path.join(root, '.kushi', 'hooks');
17
+ const stateDir = path.join(root, 'Evidence', 'tester', 'State');
18
+ fs.mkdirSync(hooksDir, { recursive: true });
19
+ fs.mkdirSync(stateDir, { recursive: true });
20
+ return { root, hooksDir, stateDir };
21
+ }
22
+
23
+ function cleanup(root) {
24
+ fs.rmSync(root, { recursive: true, force: true });
25
+ }
26
+
27
+ test('hooks: scripts invoked in declaration order', () => {
28
+ const { root, hooksDir, stateDir } = setupFixture('order');
29
+ try {
30
+ // Create two hook scripts that write to a shared file
31
+ const orderFile = path.join(root, 'order.txt');
32
+ const hook1 = path.join(hooksDir, 'first.ps1');
33
+ const hook2 = path.join(hooksDir, 'second.ps1');
34
+ // Use simple scripts that write via Out-File
35
+ fs.writeFileSync(hook1, `$input | Out-Null\n'first' | Out-File -Append -FilePath '${orderFile.replace(/\\/g, '/')}' -Encoding utf8`);
36
+ fs.writeFileSync(hook2, `$input | Out-Null\n'second' | Out-File -Append -FilePath '${orderFile.replace(/\\/g, '/')}' -Encoding utf8`);
37
+
38
+ // Create hooks.yml referencing both in order
39
+ const hooksYml = path.join(root, '.kushi', 'hooks.yml');
40
+ fs.writeFileSync(hooksYml, [
41
+ 'hooks:',
42
+ ' post-pull:',
43
+ ' - type: script',
44
+ ' path: .kushi/hooks/first.ps1',
45
+ ' - type: script',
46
+ ' path: .kushi/hooks/second.ps1',
47
+ ].join('\n'));
48
+
49
+ const script = `& '${invokeHooks.replace(/\\/g, '/')}' -ProjectRoot '${root.replace(/\\/g, '/')}' -Event 'post-pull' -Payload @{ project = 'test'; source = 'email'; success = $true } -StateDir '${stateDir.replace(/\\/g, '/')}'`;
50
+ const r = spawnSync('pwsh', ['-NoProfile', '-Command', script], { encoding: 'utf8', timeout: 15000 });
51
+
52
+ // The helper should not crash
53
+ assert.equal(r.status, 0, `invoke-hooks should exit 0: stderr=${r.stderr?.substring(0, 200)}`);
54
+
55
+ // Check hooks-log.md was written
56
+ const logFile = path.join(stateDir, 'hooks-log.md');
57
+ assert.ok(fs.existsSync(logFile), `hooks-log.md should be created. stdout: ${r.stdout?.substring(0,200)}`);
58
+ const logContent = fs.readFileSync(logFile, 'utf8');
59
+ assert.match(logContent, /post-pull/, 'log should mention the event');
60
+ } finally {
61
+ cleanup(root);
62
+ }
63
+ });
64
+
65
+ test('hooks: failure of one hook does not block others', () => {
66
+ const { root, hooksDir, stateDir } = setupFixture('failiso');
67
+ try {
68
+ // First hook throws, second should still run
69
+ const successFile = path.join(root, 'success.txt');
70
+ const hook1 = path.join(hooksDir, 'post-pull.ps1');
71
+ fs.writeFileSync(hook1, `throw "deliberate failure"`);
72
+
73
+ const hook2Path = path.join(hooksDir, 'ok.ps1');
74
+ fs.writeFileSync(hook2Path, `$input | Out-Null\n'ran' | Out-File -FilePath '${successFile.replace(/\\/g, '/')}' -Encoding utf8`);
75
+
76
+ const hooksYml = path.join(root, '.kushi', 'hooks.yml');
77
+ fs.writeFileSync(hooksYml, [
78
+ 'hooks:',
79
+ ' post-pull:',
80
+ ' - type: script',
81
+ ' path: .kushi/hooks/post-pull.ps1',
82
+ ' - type: script',
83
+ ' path: .kushi/hooks/ok.ps1',
84
+ ].join('\n'));
85
+
86
+ const script = `& '${invokeHooks.replace(/\\/g, '/')}' -ProjectRoot '${root.replace(/\\/g, '/')}' -Event 'post-pull' -Payload @{ project = 'test'; source = 'email'; success = $true } -StateDir '${stateDir.replace(/\\/g, '/')}'`;
87
+ const r = spawnSync('pwsh', ['-NoProfile', '-Command', script], { encoding: 'utf8', timeout: 15000 });
88
+
89
+ // The process should NOT have crashed (exit 0)
90
+ assert.equal(r.status, 0, `hook dispatcher should not crash: ${r.stderr?.substring(0,200)}`);
91
+ // Second hook should have run
92
+ assert.ok(fs.existsSync(successFile), `second hook should still run after first fails. stdout=${r.stdout?.substring(0,200)}`);
93
+ } finally {
94
+ cleanup(root);
95
+ }
96
+ });
97
+
98
+ test('hooks: webhook payload has correct JSON structure', () => {
99
+ // We can't test real webhooks, but we can verify the payload format
100
+ // by checking the Invoke-Hooks.ps1 script constructs proper JSON
101
+ const content = fs.readFileSync(invokeHooks, 'utf8');
102
+ // Verify it converts payload to JSON
103
+ assert.match(content, /ConvertTo-Json/, 'must serialize payload to JSON');
104
+ // Verify it uses Invoke-RestMethod for webhooks
105
+ assert.match(content, /Invoke-RestMethod/, 'must use Invoke-RestMethod for webhooks');
106
+ // Verify Content-Type header
107
+ assert.match(content, /application\/json/, 'must set JSON content type');
108
+ // Verify timeout
109
+ assert.match(content, /TimeoutSec/, 'must set a timeout for webhooks');
110
+ });
111
+
112
+ test('hooks: hooks-log.md format has correct heading structure', () => {
113
+ const { root, hooksDir, stateDir } = setupFixture('logfmt');
114
+ try {
115
+ const hook = path.join(hooksDir, 'post-pull.ps1');
116
+ fs.writeFileSync(hook, `$input | Out-Null; Write-Output 'ok'`);
117
+
118
+ const script = `
119
+ $payload = @{ project = 'Acme'; source = 'teams'; success = $true; duration_ms = 150 }
120
+ & '${invokeHooks.replace(/\\/g, '\\\\')}' -ProjectRoot '${root.replace(/\\/g, '\\\\')}' -Event 'post-pull' -Payload $payload -StateDir '${stateDir.replace(/\\/g, '\\\\')}'
121
+ `;
122
+ spawnSync('pwsh', ['-NoProfile', '-Command', script], { encoding: 'utf8' });
123
+
124
+ const logFile = path.join(stateDir, 'hooks-log.md');
125
+ if (fs.existsSync(logFile)) {
126
+ const content = fs.readFileSync(logFile, 'utf8');
127
+ // Check heading format matches: ## [YYYY-MM-DD HH:MM] <event> | hook: <name>
128
+ assert.match(content, /## \[\d{4}-\d{2}-\d{2} \d{2}:\d{2}\] post-pull \| hook:/, 'heading format must be canonical');
129
+ assert.match(content, /- status:/, 'must include status field');
130
+ assert.match(content, /- duration_ms:/, 'must include duration field');
131
+ }
132
+ } finally {
133
+ cleanup(root);
134
+ }
135
+ });
@@ -0,0 +1,73 @@
1
+ // kushi v5.2.0 — OTel emit tests.
2
+ // Validates no-op behavior, OTLP payload format, and privacy contract.
3
+
4
+ import test from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { spawnSync } from 'node:child_process';
10
+
11
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
12
+ const emitOtel = path.join(repoRoot, 'plugin', 'skills', '_shared', 'Emit-OtelSpan.ps1');
13
+
14
+ test('otel: no-op when KUSHI_OTEL_ENDPOINT is unset', () => {
15
+ // Run the helper with no endpoint — should exit cleanly with no output
16
+ const script = `
17
+ $env:KUSHI_OTEL_ENDPOINT = ''
18
+ & '${emitOtel.replace(/\\/g, '\\\\')}' -SpanName 'kushi.pull' -Attributes @{ project='test'; success=$true }
19
+ Write-Output 'DONE'
20
+ `;
21
+ const r = spawnSync('pwsh', ['-NoProfile', '-Command', script], {
22
+ encoding: 'utf8',
23
+ env: { ...process.env, KUSHI_OTEL_ENDPOINT: '' }
24
+ });
25
+ assert.equal(r.status, 0, `should exit cleanly: ${r.stderr}`);
26
+ assert.match(r.stdout, /DONE/, 'script should complete without error');
27
+ // Should NOT contain any HTTP-related output
28
+ assert.ok(!r.stdout.includes('Invoke-RestMethod'), 'should not attempt HTTP when unset');
29
+ });
30
+
31
+ test('otel: helper script has correct OTLP payload structure', () => {
32
+ const content = fs.readFileSync(emitOtel, 'utf8');
33
+ // Verify OTLP JSON structure elements
34
+ assert.match(content, /resourceSpans/, 'must build resourceSpans');
35
+ assert.match(content, /scopeSpans/, 'must build scopeSpans');
36
+ assert.match(content, /traceId/, 'must include traceId');
37
+ assert.match(content, /spanId/, 'must include spanId');
38
+ assert.match(content, /startTimeUnixNano/, 'must include start time in nanos');
39
+ assert.match(content, /endTimeUnixNano/, 'must include end time in nanos');
40
+ assert.match(content, /service\.name.*kushi/, 'service.name must be kushi');
41
+ });
42
+
43
+ test('otel: privacy — only allowed attribute keys pass through', () => {
44
+ const content = fs.readFileSync(emitOtel, 'utf8');
45
+ // Verify allowlist exists
46
+ assert.match(content, /allowedKeys/, 'must have an allowedKeys filter');
47
+ // Verify common safe keys are in the list
48
+ assert.match(content, /'project'/, 'project must be allowed');
49
+ assert.match(content, /'source'/, 'source must be allowed');
50
+ assert.match(content, /'success'/, 'success must be allowed');
51
+ // Verify filtering logic — keys not in allowed are skipped
52
+ assert.match(content, /\-notin \$allowedKeys/, 'must skip keys not in allowlist');
53
+ });
54
+
55
+ test('otel: emits correct span kind (INTERNAL)', () => {
56
+ const content = fs.readFileSync(emitOtel, 'utf8');
57
+ // SPAN_KIND_INTERNAL = 1
58
+ assert.match(content, /kind\s*=\s*1/, 'span kind must be INTERNAL (1)');
59
+ });
60
+
61
+ test('otel: handles missing endpoint gracefully without errors', () => {
62
+ // Run with explicitly empty endpoint
63
+ const script = `
64
+ Remove-Item Env:KUSHI_OTEL_ENDPOINT -ErrorAction SilentlyContinue
65
+ & '${emitOtel.replace(/\\/g, '\\\\')}' -SpanName 'kushi.lint' -Attributes @{ findings_count=5 }
66
+ exit $LASTEXITCODE
67
+ `;
68
+ const r = spawnSync('pwsh', ['-NoProfile', '-Command', script], {
69
+ encoding: 'utf8',
70
+ env: { ...process.env, KUSHI_OTEL_ENDPOINT: undefined }
71
+ });
72
+ assert.equal(r.status, 0, 'must not fail when endpoint is missing');
73
+ });