kushi-agents 5.4.4 → 5.4.5
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 +9 -0
- package/package.json +2 -2
- package/plugin/skills/doctor/doctor.ps1 +101 -11
- package/plugin/skills/self-check/SKILL.md +3 -2
- package/plugin/skills/self-check/run.ps1 +201 -1
- package/src/hooks-dispatcher.test.mjs +2 -2
- package/src/layout-portable.test.mjs +89 -0
- package/src/profile-coverage.test.mjs +84 -0
package/README.md
CHANGED
|
@@ -230,6 +230,15 @@ The Evidence/ folder produced by every profile is a **stable public contract**
|
|
|
230
230
|
|
|
231
231
|
See [Quickstart](https://gim-home.github.io/kushi/getting-started/quickstart/) for the full workflow.
|
|
232
232
|
|
|
233
|
+
### Doctor works from both layouts (v5.4.5+)
|
|
234
|
+
|
|
235
|
+
`doctor` and `self-check` are **layout-portable**: they run cleanly from either the **source tree** (this repo: `plugin/skills/...`) or an **installed host directory** (e.g. `~/.vscode/chat/skills/kushi/skills/...` — FLAT, no `plugin/` prefix). A banner like `[layout=source]` or `[layout=install (host=clawpilot)]` prints at the top of every run so you know which personality is active.
|
|
236
|
+
|
|
237
|
+
- **Source mode** (default in this repo): runs the full source-author surface (C1..C12, D1..D43).
|
|
238
|
+
- **Install mode**: runs a focused install-integrity probe set (manifest validity, every enumerated skill present, agent file, skills-metadata sibling, npm-latest drift if `npm` is on PATH, optional global wiki shape). Fix hints become `Re-install: npx kushi-agents@latest --<host> --force` instead of "restore from git".
|
|
239
|
+
|
|
240
|
+
Force a mode with `-LayoutMode source|install` on either script; default is `auto`.
|
|
241
|
+
|
|
233
242
|
## Install
|
|
234
243
|
|
|
235
244
|
Kushi supports **two host surfaces** as first-class peers (v5.0.2+):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kushi-agents",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.5",
|
|
4
4
|
"description": "Install Kushi — multi-source project evidence agent with Comprehensive Structured Capture (CSC) into weekly-only files across Email, Teams, OneNote, Loop, SharePoint, Meetings, CRM, ADO. Meetings retain a sibling verbatim/ audit folder. WorkIQ-only for M365 sources (Graph / m365_* FORBIDDEN as fallbacks; user-paste is first-class). Host-agnostic.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"license": "MIT",
|
|
43
43
|
"scripts": {
|
|
44
|
-
"test": "node --test src/check-workiq.test.mjs src/seed-config.test.mjs src/sanitize-workiq-input.test.mjs src/detect-vertex-repo.test.mjs src/vertex-validate.test.mjs src/emit-vertex.e2e.test.mjs src/config-root-resolve.test.mjs src/forbidden-workiq-phrasings.test.mjs src/multi-host-install.test.mjs src/eval-aggregator.test.mjs src/eval-runner.test.mjs src/skill-creator.test.mjs src/skill-checker.test.mjs src/hooks-dispatcher.test.mjs src/parallel-refresh.test.mjs src/otel-emit.test.mjs src/teach.test.mjs src/schema-evolve.test.mjs src/global-wiki.test.mjs src/promote.test.mjs src/doctor.test.mjs src/setup-wizard.test.mjs src/cli-no-args.test.mjs src/cli-no-args-tty.test.mjs src/per-user-files.test.mjs",
|
|
44
|
+
"test": "node --test src/check-workiq.test.mjs src/seed-config.test.mjs src/sanitize-workiq-input.test.mjs src/detect-vertex-repo.test.mjs src/vertex-validate.test.mjs src/emit-vertex.e2e.test.mjs src/config-root-resolve.test.mjs src/forbidden-workiq-phrasings.test.mjs src/multi-host-install.test.mjs src/eval-aggregator.test.mjs src/eval-runner.test.mjs src/skill-creator.test.mjs src/skill-checker.test.mjs src/hooks-dispatcher.test.mjs src/parallel-refresh.test.mjs src/otel-emit.test.mjs src/teach.test.mjs src/schema-evolve.test.mjs src/global-wiki.test.mjs src/promote.test.mjs src/doctor.test.mjs src/setup-wizard.test.mjs src/cli-no-args.test.mjs src/cli-no-args-tty.test.mjs src/per-user-files.test.mjs src/layout-portable.test.mjs src/profile-coverage.test.mjs",
|
|
45
45
|
"test:integration:bootstrap": "node src/bootstrap-dryrun.integration.test.mjs",
|
|
46
46
|
"smoke": "node scripts/smoke.mjs",
|
|
47
47
|
"eval": "pwsh plugin/skills/eval/run-evals.ps1 -Skill",
|
|
@@ -27,12 +27,51 @@ pwsh plugin/skills/doctor/doctor.ps1 -Json
|
|
|
27
27
|
param(
|
|
28
28
|
[string]$Repo = (Resolve-Path (Join-Path $PSScriptRoot "..\..\..")).Path,
|
|
29
29
|
[switch]$Json,
|
|
30
|
-
[switch]$Strict
|
|
30
|
+
[switch]$Strict,
|
|
31
|
+
[ValidateSet('auto','source','install')] [string]$LayoutMode = 'auto'
|
|
31
32
|
)
|
|
32
33
|
|
|
33
34
|
$ErrorActionPreference = 'Continue'
|
|
34
35
|
$sections = New-Object System.Collections.Generic.List[object]
|
|
35
36
|
|
|
37
|
+
# === v5.4.5: layout-portable doctor ===
|
|
38
|
+
function Resolve-KushiLayout([string]$RootPath) {
|
|
39
|
+
if (Test-Path (Join-Path $RootPath 'plugin')) {
|
|
40
|
+
return @{ Mode='source'; Plugin=(Join-Path $RootPath 'plugin'); Repo=$RootPath; HasPluginJson=$true }
|
|
41
|
+
}
|
|
42
|
+
if (Test-Path (Join-Path $RootPath 'skills')) {
|
|
43
|
+
$pj = Join-Path $RootPath 'kushi-install.json'
|
|
44
|
+
return @{ Mode='install'; Plugin=$RootPath; Repo=$RootPath; HasPluginJson=(Test-Path $pj) }
|
|
45
|
+
}
|
|
46
|
+
throw "Not a kushi layout: $RootPath (need either plugin/ or skills/)."
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function Get-KushiInstallHost([string]$InstallRoot) {
|
|
50
|
+
$norm = ($InstallRoot -replace '\\','/').ToLower()
|
|
51
|
+
if ($norm -match '/\.copilot/') { return 'clawpilot' }
|
|
52
|
+
if ($norm -match '/\.vscode/') { return 'vscode' }
|
|
53
|
+
return 'unknown'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
$Layout = Resolve-KushiLayout $Repo
|
|
57
|
+
if ($LayoutMode -ne 'auto' -and $Layout.Mode -ne $LayoutMode) {
|
|
58
|
+
if ($LayoutMode -eq 'source') {
|
|
59
|
+
if (-not (Test-Path (Join-Path $Repo 'plugin'))) { Write-Error "LayoutMode=source requested but no plugin/ under $Repo."; exit 2 }
|
|
60
|
+
$Layout = @{ Mode='source'; Plugin=(Join-Path $Repo 'plugin'); Repo=$Repo; HasPluginJson=$true }
|
|
61
|
+
} else {
|
|
62
|
+
if (-not (Test-Path (Join-Path $Repo 'skills'))) { Write-Error "LayoutMode=install requested but no skills/ under $Repo."; exit 2 }
|
|
63
|
+
$Layout = @{ Mode='install'; Plugin=$Repo; Repo=$Repo; HasPluginJson=(Test-Path (Join-Path $Repo 'kushi-install.json')) }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (-not $Json) {
|
|
68
|
+
if ($Layout.Mode -eq 'install') {
|
|
69
|
+
Write-Host ("[layout=install (host={0})]" -f (Get-KushiInstallHost $Layout.Repo))
|
|
70
|
+
} else {
|
|
71
|
+
Write-Host "[layout=source]"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
36
75
|
function Add-Section {
|
|
37
76
|
param(
|
|
38
77
|
[string]$Name,
|
|
@@ -90,9 +129,15 @@ if ($envProblems.Count -eq 0) {
|
|
|
90
129
|
|
|
91
130
|
# ── Section 2: self-check -Deep ─────────────────────────────────────────────
|
|
92
131
|
Write-Banner "2. self-check -Deep" "Cyan"
|
|
93
|
-
$
|
|
132
|
+
if ($Layout.Mode -eq 'source') {
|
|
133
|
+
$selfCheckPath = Join-Path $Layout.Plugin 'skills\self-check\run.ps1'
|
|
134
|
+
} else {
|
|
135
|
+
$selfCheckPath = Join-Path $Layout.Plugin 'skills\self-check\run.ps1'
|
|
136
|
+
}
|
|
94
137
|
if (Test-Path $selfCheckPath) {
|
|
95
|
-
$
|
|
138
|
+
$scArgs = @('-NoProfile','-File',$selfCheckPath,'-Deep','-Json','-Root',$Layout.Repo)
|
|
139
|
+
if ($Layout.Mode -eq 'install') { $scArgs += @('-LayoutMode','install') }
|
|
140
|
+
$scJson = & pwsh @scArgs 2>$null
|
|
96
141
|
$scExit = $LASTEXITCODE
|
|
97
142
|
$scFindings = @()
|
|
98
143
|
try { $scFindings = $scJson | ConvertFrom-Json } catch {}
|
|
@@ -106,15 +151,24 @@ if (Test-Path $selfCheckPath) {
|
|
|
106
151
|
Write-Host " ⚠️ $scCount finding(s)" -ForegroundColor Yellow
|
|
107
152
|
foreach ($f in $scFindings) { Write-Host " [$($f.code)] $($f.message)" -ForegroundColor DarkYellow }
|
|
108
153
|
}
|
|
109
|
-
Add-Section 'self-check' 'yellow' "$scCount finding(s)" 'pwsh
|
|
154
|
+
Add-Section 'self-check' 'yellow' "$scCount finding(s)" 'pwsh skills/self-check/run.ps1 -Deep # then fix each [code]'
|
|
110
155
|
}
|
|
111
156
|
} else {
|
|
112
157
|
if (-not $Json) { Write-Host " ❌ run.ps1 missing" -ForegroundColor Red }
|
|
113
|
-
|
|
158
|
+
if ($Layout.Mode -eq 'install') {
|
|
159
|
+
$hostFlag = switch (Get-KushiInstallHost $Layout.Repo) { 'clawpilot' { '--clawpilot' } 'vscode' { '--vscode' } default { '--all-hosts' } }
|
|
160
|
+
Add-Section 'self-check' 'red' 'run.ps1 not found in install' "Re-install: ``npx kushi-agents@latest $hostFlag --force``"
|
|
161
|
+
} else {
|
|
162
|
+
Add-Section 'self-check' 'red' 'run.ps1 not found' "Restore plugin/skills/self-check/run.ps1 from git."
|
|
163
|
+
}
|
|
114
164
|
}
|
|
115
165
|
|
|
116
166
|
# ── Section 3: canary evals ─────────────────────────────────────────────────
|
|
117
167
|
Write-Banner "3. canary evals" "Cyan"
|
|
168
|
+
if ($Layout.Mode -eq 'install') {
|
|
169
|
+
if (-not $Json) { Write-Host " ℹ️ skipped in install mode (no eval baseline shipped)" -ForegroundColor Gray }
|
|
170
|
+
Add-Section 'canary-evals' 'green' 'skipped (install mode)' ''
|
|
171
|
+
} else {
|
|
118
172
|
Push-Location $Repo
|
|
119
173
|
$canaryOut = & npm run --silent eval:canary 2>&1
|
|
120
174
|
$canaryExit = $LASTEXITCODE
|
|
@@ -141,31 +195,66 @@ if ($canaryExit -eq 0) {
|
|
|
141
195
|
Add-Section 'canary-evals' 'red' "FAIL exit=$canaryExit" 'npm run eval:canary # investigate failing case'
|
|
142
196
|
}
|
|
143
197
|
}
|
|
198
|
+
}
|
|
144
199
|
|
|
145
200
|
# ── Section 4: skill-checker -All ───────────────────────────────────────────
|
|
146
201
|
Write-Banner "4. skill-checker -All" "Cyan"
|
|
147
|
-
$checkerPath = Join-Path $
|
|
202
|
+
$checkerPath = Join-Path $Layout.Plugin 'skills\skill-checker\check-skill.ps1'
|
|
148
203
|
if (Test-Path $checkerPath) {
|
|
149
|
-
$ckOut = & pwsh -NoProfile -File $checkerPath -All -Root $Repo 2>&1
|
|
204
|
+
$ckOut = & pwsh -NoProfile -File $checkerPath -All -Root $Layout.Repo 2>&1
|
|
150
205
|
$ckExit = $LASTEXITCODE
|
|
151
206
|
if ($ckExit -eq 0) {
|
|
152
207
|
if (-not $Json) { Write-Host " ✅ all skills compliant" -ForegroundColor Green }
|
|
153
208
|
Add-Section 'skill-checker' 'green' 'all skills pass blueprint' ''
|
|
154
209
|
} else {
|
|
155
210
|
if (-not $Json) { Write-Host " ⚠️ blueprint violations (exit=$ckExit)" -ForegroundColor Yellow }
|
|
156
|
-
Add-Section 'skill-checker' 'yellow' "exit=$ckExit" 'pwsh
|
|
211
|
+
Add-Section 'skill-checker' 'yellow' "exit=$ckExit" 'pwsh skills/skill-checker/check-skill.ps1 -All # review output'
|
|
157
212
|
}
|
|
158
213
|
} else {
|
|
159
|
-
|
|
214
|
+
if ($Layout.Mode -eq 'install') {
|
|
215
|
+
if (-not $Json) { Write-Host " ⚠️ skill-checker not installed in this profile (full only)" -ForegroundColor Yellow }
|
|
216
|
+
Add-Section 'skill-checker' 'yellow' 'not installed (full profile only)' "Re-install with full profile: ``npx kushi-agents@latest --all-hosts --profile full --force``"
|
|
217
|
+
} else {
|
|
218
|
+
Add-Section 'skill-checker' 'red' 'check-skill.ps1 not found' "Restore plugin/skills/skill-checker/check-skill.ps1"
|
|
219
|
+
}
|
|
160
220
|
}
|
|
161
221
|
|
|
162
222
|
# ── Section 5: Live-install drift ───────────────────────────────────────────
|
|
163
223
|
Write-Banner "5. live-install drift" "Cyan"
|
|
224
|
+
if ($Layout.Mode -eq 'install') {
|
|
225
|
+
# Install mode: compare installed version (from kushi-install.json) to npm latest.
|
|
226
|
+
$installPjPath = Join-Path $Layout.Repo 'kushi-install.json'
|
|
227
|
+
$installedVer = $null
|
|
228
|
+
if (Test-Path $installPjPath) {
|
|
229
|
+
try { $installedVer = (Get-Content -Raw $installPjPath | ConvertFrom-Json).version } catch {}
|
|
230
|
+
}
|
|
231
|
+
$npmOk = $false
|
|
232
|
+
try { $null = & npm --version 2>$null; if ($LASTEXITCODE -eq 0) { $npmOk = $true } } catch {}
|
|
233
|
+
if (-not $npmOk) {
|
|
234
|
+
if (-not $Json) { Write-Host " ℹ️ npm not on PATH (skipped)" -ForegroundColor Gray }
|
|
235
|
+
Add-Section 'live-install' 'green' 'npm not on PATH (skipped)' ''
|
|
236
|
+
} elseif (-not $installedVer) {
|
|
237
|
+
if (-not $Json) { Write-Host " ⚠️ no kushi-install.json version field" -ForegroundColor Yellow }
|
|
238
|
+
$hostFlag = switch (Get-KushiInstallHost $Layout.Repo) { 'clawpilot' { '--clawpilot' } 'vscode' { '--vscode' } default { '--all-hosts' } }
|
|
239
|
+
Add-Section 'live-install' 'yellow' 'install manifest missing version' "Re-install: ``npx kushi-agents@latest $hostFlag --force``"
|
|
240
|
+
} else {
|
|
241
|
+
$latest = $null
|
|
242
|
+
try { $latest = (& npm view kushi-agents version 2>$null).Trim() } catch {}
|
|
243
|
+
if ($latest -and $latest -ne $installedVer) {
|
|
244
|
+
if (-not $Json) { Write-Host " ⚠️ installed=v$installedVer latest=v$latest" -ForegroundColor Yellow }
|
|
245
|
+
$hostFlag = switch (Get-KushiInstallHost $Layout.Repo) { 'clawpilot' { '--clawpilot' } 'vscode' { '--vscode' } default { '--all-hosts' } }
|
|
246
|
+
Add-Section 'live-install' 'yellow' "installed=v$installedVer latest=v$latest" "Re-install: ``npx kushi-agents@latest $hostFlag --force``"
|
|
247
|
+
} else {
|
|
248
|
+
if (-not $Json) { Write-Host " ✅ install matches npm latest" -ForegroundColor Green }
|
|
249
|
+
Add-Section 'live-install' 'green' "installed=v$installedVer (matches npm latest)" ''
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
164
253
|
$liveSkill = Join-Path $userHome '.copilot\m-skills\kushi\SKILL.md'
|
|
165
|
-
$agentFile = Join-Path $
|
|
254
|
+
$agentFile = Join-Path $Layout.Plugin 'agents\kushi.agent.md'
|
|
166
255
|
$metaPath = Join-Path $userHome '.copilot\m-skills\skills-metadata.json'
|
|
167
256
|
$repoVersion = $null
|
|
168
|
-
try { $repoVersion = (Get-Content -Raw (Join-Path $Repo 'package.json') | ConvertFrom-Json).version } catch {}
|
|
257
|
+
try { $repoVersion = (Get-Content -Raw (Join-Path $Layout.Repo 'package.json') | ConvertFrom-Json).version } catch {}
|
|
169
258
|
$liveVersion = $null
|
|
170
259
|
if (Test-Path $metaPath) {
|
|
171
260
|
try {
|
|
@@ -204,6 +293,7 @@ if (-not (Test-Path $liveSkill)) {
|
|
|
204
293
|
}
|
|
205
294
|
}
|
|
206
295
|
}
|
|
296
|
+
}
|
|
207
297
|
|
|
208
298
|
# ── Section 6: Global wiki shape ────────────────────────────────────────────
|
|
209
299
|
Write-Banner "6. global wiki shape" "Cyan"
|
|
@@ -76,9 +76,10 @@ Checks split into **core** (always run) and **deep** (opt-in).
|
|
|
76
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
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
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`. |
|
|
79
|
-
| D40.global-wiki | Global wiki + multi-wiki routing (v5.3.0+) | `global-wiki.instructions.md` + `multi-wiki-routing.instructions.md`
|
|
80
|
-
| D41.stabilization | v5.4.0 stabilization + first-run polish | `
|
|
79
|
+
| D40.global-wiki | Global wiki + multi-wiki routing (v5.3.0+) | `global-wiki.instructions.md` + `multi-wiki-routing.instructions.md` exist; `src/global-wiki.mjs` exports init/promote; `$KUSHI_GLOBAL_ROOT`'s `State/` root pages carry `scope: global` frontmatter; no unresolved `> [!warning] potential-customer-leak` callouts. Sub-checks: `D40.global-wiki-doctrine-exists`, `D40.routing-doctrine-exists`, `D40.global-wiki-module-exists`, `D40.promote-module-exists`, `D40.global-wiki-shape`, `D40.no-customer-leak`. |
|
|
80
|
+
| D41.stabilization | v5.4.0 stabilization + first-run polish | `doctor/SKILL.md` + `doctor.ps1` ship; `docs/quickstart.md` ≤200 lines; `docs/migration/v4-to-v5.md` exists; `docs/audits/v5.4.0-soak-fixture.md` mentions all 10 soak steps; `.github/workflows/pr-checks.yml` invokes self-check and skill-checker with `-StrictExit`. Sub-checks: `D41.doctor-exists`, `D41.quickstart-exists`, `D41.migration-notes-exist`, `D41.soak-audit-exists`, `D41.pr-checks-strict`. |
|
|
81
81
|
| D42.per-user-files-scoping | v5.4.4 per-user truth | SKILLs and prompts mentioning `bootstrap-status.md`, `FOLLOW-UPS.md`, or `OPEN-QUESTIONS-DRAFT.md` must also reference `Evidence/<alias>/` or `_Consolidated/` within ±5 lines. See `plugin/instructions/multi-user-shared-files.instructions.md`. |
|
|
82
|
+
| D43.profile-coverage | v5.4.5 ship-completeness | every `plugin/skills/<x>/` (except `_*`, `intro`) MUST be in some profile's `skills[]` in `plugin.json`. Source-mode only. |
|
|
82
83
|
| **CSC weekly-layout checks (kushi v4.9.0)** | | gated on `Resolve-EngagementRoots` — no-ops on the kushi repo itself. |
|
|
83
84
|
| 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. |
|
|
84
85
|
| 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. |
|
|
@@ -30,12 +30,65 @@ param(
|
|
|
30
30
|
[switch]$Deep,
|
|
31
31
|
[switch]$Json,
|
|
32
32
|
[switch]$StrictExit,
|
|
33
|
-
[string]$Targeted
|
|
33
|
+
[string]$Targeted,
|
|
34
|
+
[ValidateSet('auto','source','install')] [string]$LayoutMode = 'auto'
|
|
34
35
|
)
|
|
35
36
|
|
|
36
37
|
$ErrorActionPreference = "Stop"
|
|
37
38
|
$findings = New-Object System.Collections.Generic.List[object]
|
|
38
39
|
|
|
40
|
+
# === v5.4.5: layout-portable diagnostics ===
|
|
41
|
+
# Kushi diagnostics may run from either:
|
|
42
|
+
# - source tree : <repo>/plugin/{skills,prompts,instructions,agents,plugin.json}
|
|
43
|
+
# - installed tree: <host>/skills/kushi/{skills,prompts,instructions,agents,kushi-install.json}
|
|
44
|
+
# Resolve-KushiLayout returns a normalized handle so probes can derive paths
|
|
45
|
+
# from $Layout.Plugin instead of hardcoding 'plugin\...'.
|
|
46
|
+
function Resolve-KushiLayout([string]$RootPath) {
|
|
47
|
+
if (Test-Path (Join-Path $RootPath 'plugin')) {
|
|
48
|
+
return @{ Mode='source'; Plugin=(Join-Path $RootPath 'plugin'); Repo=$RootPath; HasPluginJson=$true }
|
|
49
|
+
}
|
|
50
|
+
if (Test-Path (Join-Path $RootPath 'skills')) {
|
|
51
|
+
$pj = Join-Path $RootPath 'kushi-install.json'
|
|
52
|
+
return @{ Mode='install'; Plugin=$RootPath; Repo=$RootPath; HasPluginJson=(Test-Path $pj) }
|
|
53
|
+
}
|
|
54
|
+
throw "Not a kushi layout: $RootPath (need either plugin/ or skills/)."
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function Get-KushiInstallHost([string]$InstallRoot) {
|
|
58
|
+
# Best-effort: detect whether the install lives under ~/.copilot or ~/.vscode.
|
|
59
|
+
$norm = ($InstallRoot -replace '\\','/').ToLower()
|
|
60
|
+
if ($norm -match '/\.copilot/') { return 'clawpilot' }
|
|
61
|
+
if ($norm -match '/\.vscode/') { return 'vscode' }
|
|
62
|
+
return 'unknown'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
$Layout = Resolve-KushiLayout $Root
|
|
66
|
+
if ($LayoutMode -ne 'auto' -and $Layout.Mode -ne $LayoutMode) {
|
|
67
|
+
# User forced a mode. Re-shape the handle accordingly.
|
|
68
|
+
if ($LayoutMode -eq 'source') {
|
|
69
|
+
if (-not (Test-Path (Join-Path $Root 'plugin'))) {
|
|
70
|
+
Write-Error "LayoutMode=source requested but no plugin/ under $Root."
|
|
71
|
+
exit 2
|
|
72
|
+
}
|
|
73
|
+
$Layout = @{ Mode='source'; Plugin=(Join-Path $Root 'plugin'); Repo=$Root; HasPluginJson=$true }
|
|
74
|
+
} else {
|
|
75
|
+
if (-not (Test-Path (Join-Path $Root 'skills'))) {
|
|
76
|
+
Write-Error "LayoutMode=install requested but no skills/ under $Root."
|
|
77
|
+
exit 2
|
|
78
|
+
}
|
|
79
|
+
$Layout = @{ Mode='install'; Plugin=$Root; Repo=$Root; HasPluginJson=(Test-Path (Join-Path $Root 'kushi-install.json')) }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (-not $Json) {
|
|
84
|
+
if ($Layout.Mode -eq 'install') {
|
|
85
|
+
$hostName = Get-KushiInstallHost $Layout.Repo
|
|
86
|
+
Write-Host "[layout=install (host=$hostName)]"
|
|
87
|
+
} else {
|
|
88
|
+
Write-Host "[layout=source]"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
39
92
|
function Get-UserConfigRoot {
|
|
40
93
|
# Cross-platform: APPDATA-like location for the live Clawpilot install.
|
|
41
94
|
if ($IsWindows -or $env:OS -eq 'Windows_NT') { return $env:USERPROFILE }
|
|
@@ -106,6 +159,122 @@ function Get-Frontmatter {
|
|
|
106
159
|
return $fm
|
|
107
160
|
}
|
|
108
161
|
|
|
162
|
+
# === v5.4.5: install-mode minimal probe set ===
|
|
163
|
+
# In install mode we can't see plugin.json source, profile definitions, doc-tree,
|
|
164
|
+
# eval baselines, etc. — those are source-author concerns. We run a small probe
|
|
165
|
+
# set focused on integrity of the installed payload itself.
|
|
166
|
+
if ($Layout.Mode -eq 'install') {
|
|
167
|
+
$installRoot = $Layout.Plugin
|
|
168
|
+
$installPj = Join-Path $installRoot 'kushi-install.json'
|
|
169
|
+
$installSkills = Join-Path $installRoot 'skills'
|
|
170
|
+
$installAgent = Join-Path $installRoot 'agents\kushi.agent.md'
|
|
171
|
+
$installSkillMd= Join-Path $installRoot 'SKILL.md'
|
|
172
|
+
$installVersion = $null
|
|
173
|
+
$installProfile = $null
|
|
174
|
+
$installedSkillList = @()
|
|
175
|
+
if (Test-Path $installPj) {
|
|
176
|
+
try {
|
|
177
|
+
$ij = Get-Content -Raw $installPj | ConvertFrom-Json
|
|
178
|
+
$installVersion = $ij.version
|
|
179
|
+
$installProfile = $ij.profile
|
|
180
|
+
if ($ij.skills) { $installedSkillList = @($ij.skills) }
|
|
181
|
+
} catch {
|
|
182
|
+
Add-Finding 'I1.install-manifest' 'Install' 'error' "Failed to parse kushi-install.json: $_" "Re-install with ``npx kushi-agents@latest --all-hosts --force``." $installPj 0
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
Add-Finding 'I1.install-manifest' 'Install' 'warning' "kushi-install.json not found at $installPj" "Re-install with ``npx kushi-agents@latest --all-hosts --force``." $installRoot 0
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# I2: env probe — node + pwsh
|
|
189
|
+
$nodeOk = $false
|
|
190
|
+
try { $null = & node --version 2>$null; if ($LASTEXITCODE -eq 0) { $nodeOk = $true } } catch {}
|
|
191
|
+
if (-not $nodeOk) {
|
|
192
|
+
Add-Finding 'I2.env' 'Install' 'warning' "node not on PATH" "Install Node 18+ from https://nodejs.org/" $null 0
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# I3: every skill enumerated in install manifest exists under skills/
|
|
196
|
+
if (Test-Path $installSkills) {
|
|
197
|
+
foreach ($s in $installedSkillList) {
|
|
198
|
+
$sd = Join-Path $installSkills $s
|
|
199
|
+
if (-not (Test-Path $sd)) {
|
|
200
|
+
Add-Finding 'I3.skill-missing' 'Install' 'error' "kushi-install.json lists skill '$s' but $sd is absent" "Re-install with ``npx kushi-agents@latest --all-hosts --force``." $sd 0
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
Add-Finding 'I3.skill-missing' 'Install' 'error' "skills/ directory not found at $installSkills" "Re-install with ``npx kushi-agents@latest --all-hosts --force``." $installSkills 0
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# I4: agent file present + SKILL.md mirror
|
|
208
|
+
if (-not (Test-Path $installAgent)) {
|
|
209
|
+
Add-Finding 'I4.agent' 'Install' 'error' "agent file missing: $installAgent" "Re-install with ``npx kushi-agents@latest --all-hosts --force``." $installAgent 0
|
|
210
|
+
}
|
|
211
|
+
if (-not (Test-Path $installSkillMd)) {
|
|
212
|
+
# Mirror is informational — Clawpilot uses agents/kushi.agent.md, VS Code uses SKILL.md
|
|
213
|
+
Add-Finding 'I4.skill-mirror' 'Install' 'warning' "SKILL.md mirror not present at $installSkillMd" "Re-install with ``npx kushi-agents@latest --all-hosts --force`` (host may require SKILL.md sibling)." $installSkillMd 0
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# I5: skills-metadata.json sibling under host root (accept any of the two known shapes)
|
|
217
|
+
$userBase = Get-UserConfigRoot
|
|
218
|
+
$candidateMeta = @(
|
|
219
|
+
(Join-Path $userBase '.copilot\m-skills\skills-metadata.json'),
|
|
220
|
+
(Join-Path $userBase '.vscode\chat\skills\skills-metadata.json')
|
|
221
|
+
)
|
|
222
|
+
$foundMeta = $candidateMeta | Where-Object { Test-Path $_ } | Select-Object -First 1
|
|
223
|
+
if (-not $foundMeta) {
|
|
224
|
+
Add-Finding 'I5.metadata' 'Install' 'warning' "skills-metadata.json not found under known host roots" "Re-install with ``npx kushi-agents@latest --all-hosts --force``." $null 0
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# I6: live drift vs npm latest (suppress if npm not on PATH)
|
|
228
|
+
$npmOk = $false
|
|
229
|
+
try { $null = & npm --version 2>$null; if ($LASTEXITCODE -eq 0) { $npmOk = $true } } catch {}
|
|
230
|
+
if ($npmOk -and $installVersion) {
|
|
231
|
+
$latest = $null
|
|
232
|
+
try { $latest = (& npm view kushi-agents version 2>$null).Trim() } catch {}
|
|
233
|
+
if ($latest -and $latest -ne $installVersion) {
|
|
234
|
+
Add-Finding 'I6.drift' 'Install' 'warning' "installed=v$installVersion latest=v$latest" "Re-install with ``npx kushi-agents@latest --all-hosts --force``." $installPj 0
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
# I7: optional global wiki shape
|
|
239
|
+
$globalRoot = $env:KUSHI_GLOBAL_ROOT
|
|
240
|
+
if (-not $globalRoot) { $globalRoot = Join-Path $userBase '.kushi-global' }
|
|
241
|
+
if (Test-Path $globalRoot) {
|
|
242
|
+
$gs = Join-Path $globalRoot 'State'
|
|
243
|
+
if (-not (Test-Path $gs)) {
|
|
244
|
+
Add-Finding 'I7.global-wiki' 'Install' 'warning' "global wiki at $globalRoot missing State/" "Run ``kushi global init`` to repair scaffold." $globalRoot 0
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
# Emit and exit — skip all source-author C/D checks.
|
|
249
|
+
if ($Targeted) {
|
|
250
|
+
$needle = [regex]::Escape($Targeted)
|
|
251
|
+
$findings = @($findings | Where-Object {
|
|
252
|
+
$_.code -match $needle -or $_.surface -match $needle -or $_.file -match $needle -or $_.message -match $needle
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
if ($Json) {
|
|
256
|
+
$findings | ConvertTo-Json -Depth 6
|
|
257
|
+
} else {
|
|
258
|
+
$stamp = Get-Date -Format 'yyyy-MM-dd HH:mm'
|
|
259
|
+
Write-Host ""
|
|
260
|
+
Write-Host "### Self-Check Results — Kushi install v$installVersion (profile=$installProfile) @ $stamp"
|
|
261
|
+
Write-Host ""
|
|
262
|
+
if ($findings.Count -eq 0) {
|
|
263
|
+
Write-Host "✅ Install integrity OK."
|
|
264
|
+
} else {
|
|
265
|
+
foreach ($x in $findings) {
|
|
266
|
+
Write-Host "⚠️ [$($x.code)] $($x.message)"
|
|
267
|
+
if ($x.file) { Write-Host " file: $($x.file)" }
|
|
268
|
+
Write-Host " fix : $($x.fix)"
|
|
269
|
+
Write-Host ""
|
|
270
|
+
}
|
|
271
|
+
Write-Host "⚠️ Found $($findings.Count) install-integrity gap(s)."
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if ($StrictExit -and $findings.Count -gt 0) { exit 1 }
|
|
275
|
+
exit 0
|
|
276
|
+
}
|
|
277
|
+
|
|
109
278
|
# === Discovery (source of truth) ===
|
|
110
279
|
$pluginDir = Join-Path $Root 'plugin'
|
|
111
280
|
$skillsDir = Join-Path $pluginDir 'skills'
|
|
@@ -2198,6 +2367,37 @@ process.stdout.write(JSON.stringify(out));
|
|
|
2198
2367
|
}
|
|
2199
2368
|
}
|
|
2200
2369
|
|
|
2370
|
+
# D43.profile-coverage (v5.4.5+)
|
|
2371
|
+
# Source-mode only: walk plugin/skills/ and ensure every disk skill is enumerated
|
|
2372
|
+
# by at least one profile in plugin.json. Orphans are skills that exist in the
|
|
2373
|
+
# source tree but won't ship to any user via `npx kushi-agents`. We union skills
|
|
2374
|
+
# across all profiles WITHOUT resolving `extends` chains — the union is the
|
|
2375
|
+
# superset (an orphan is one nobody references at any level), so chain-resolution
|
|
2376
|
+
# is unnecessary for this check and keeps the probe O(profiles+skills).
|
|
2377
|
+
$pjFileD43 = Join-Path $pluginDir 'plugin.json'
|
|
2378
|
+
if (Test-Path $pjFileD43) {
|
|
2379
|
+
try {
|
|
2380
|
+
$pjD43 = Get-Content -Raw $pjFileD43 | ConvertFrom-Json
|
|
2381
|
+
$diskSkills = (Get-ChildItem $skillsDir -Directory | Where-Object {
|
|
2382
|
+
$_.Name -notmatch '^\.' -and $_.Name -notmatch '^_' -and $_.Name -ne 'intro'
|
|
2383
|
+
}).Name
|
|
2384
|
+
$allEnumerated = New-Object System.Collections.Generic.HashSet[string]
|
|
2385
|
+
if ($pjD43.profiles) {
|
|
2386
|
+
foreach ($pn in $pjD43.profiles.PSObject.Properties.Name) {
|
|
2387
|
+
$node = $pjD43.profiles.$pn
|
|
2388
|
+
if ($node.skills) { foreach ($s in $node.skills) { $null = $allEnumerated.Add($s) } }
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
foreach ($ds in $diskSkills) {
|
|
2392
|
+
if (-not $allEnumerated.Contains($ds)) {
|
|
2393
|
+
Add-Finding 'D43.profile-coverage' 'Profiles' 'warning' "Skill '$ds' is on disk under plugin/skills/$ds/ but no profile enumerates it (orphaned — won't ship to users)." "Add ``$ds`` to plugin.json profile {core|standard|full|preview} skills array, OR delete plugin/skills/$ds/ if obsolete." $pjFileD43 0
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
} catch {
|
|
2397
|
+
# C9 already reports plugin.json parse failures; stay quiet here.
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2201
2401
|
# === Output ===
|
|
2202
2402
|
if ($Targeted) {
|
|
2203
2403
|
# Filter findings to those whose code, surface, file path, or message contain the substring.
|
|
@@ -47,7 +47,7 @@ test('hooks: scripts invoked in declaration order', () => {
|
|
|
47
47
|
].join('\n'));
|
|
48
48
|
|
|
49
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:
|
|
50
|
+
const r = spawnSync('pwsh', ['-NoProfile', '-Command', script], { encoding: 'utf8', timeout: 60000 });
|
|
51
51
|
|
|
52
52
|
// The helper should not crash
|
|
53
53
|
assert.equal(r.status, 0, `invoke-hooks should exit 0: stderr=${r.stderr?.substring(0, 200)}`);
|
|
@@ -84,7 +84,7 @@ test('hooks: failure of one hook does not block others', () => {
|
|
|
84
84
|
].join('\n'));
|
|
85
85
|
|
|
86
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:
|
|
87
|
+
const r = spawnSync('pwsh', ['-NoProfile', '-Command', script], { encoding: 'utf8', timeout: 60000 });
|
|
88
88
|
|
|
89
89
|
// The process should NOT have crashed (exit 0)
|
|
90
90
|
assert.equal(r.status, 0, `hook dispatcher should not crash: ${r.stderr?.substring(0,200)}`);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// v5.4.5 — layout-portable diagnostics
|
|
2
|
+
// Asserts Resolve-KushiLayout returns the right mode on source vs install
|
|
3
|
+
// fixtures, and that both self-check/run.ps1 and doctor/doctor.ps1 emit a
|
|
4
|
+
// banner + exit 0 on a minimally-valid install tree.
|
|
5
|
+
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
9
|
+
import { mkdirSync, writeFileSync, rmSync, copyFileSync } from 'node:fs';
|
|
10
|
+
import { join, resolve } from 'node:path';
|
|
11
|
+
|
|
12
|
+
const REPO = resolve(import.meta.dirname, '..');
|
|
13
|
+
const TMP = join(REPO, '.testtmp', 'layout-portable');
|
|
14
|
+
const SELF = join(REPO, 'plugin', 'skills', 'self-check', 'run.ps1');
|
|
15
|
+
const DOC = join(REPO, 'plugin', 'skills', 'doctor', 'doctor.ps1');
|
|
16
|
+
|
|
17
|
+
function hasPwsh() {
|
|
18
|
+
try { execSync('pwsh -NoProfile -Command "$null"', { stdio: 'ignore' }); return true; }
|
|
19
|
+
catch { return false; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function runPwsh(scriptPath, args) {
|
|
23
|
+
return spawnSync('pwsh', ['-NoProfile', '-File', scriptPath, ...args], {
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
timeout: 120000,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeSourceFixture() {
|
|
30
|
+
const dir = join(TMP, 'src-fix');
|
|
31
|
+
rmSync(dir, { recursive: true, force: true });
|
|
32
|
+
mkdirSync(join(dir, 'plugin', 'skills', 'noop'), { recursive: true });
|
|
33
|
+
writeFileSync(join(dir, 'plugin', 'plugin.json'),
|
|
34
|
+
JSON.stringify({ default_profile: 'core', profiles: { core: { skills: ['noop'] } } }, null, 2));
|
|
35
|
+
writeFileSync(join(dir, 'plugin', 'skills', 'noop', 'SKILL.md'),
|
|
36
|
+
`---\nname: "noop"\nversion: "0.0.1"\ndescription: "USE WHEN never"\n---\n# noop\n`);
|
|
37
|
+
return dir;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function makeInstallFixture() {
|
|
41
|
+
const dir = join(TMP, 'install-fix');
|
|
42
|
+
rmSync(dir, { recursive: true, force: true });
|
|
43
|
+
mkdirSync(join(dir, 'skills', 'noop'), { recursive: true });
|
|
44
|
+
mkdirSync(join(dir, 'skills', 'self-check'), { recursive: true });
|
|
45
|
+
mkdirSync(join(dir, 'agents'), { recursive: true });
|
|
46
|
+
writeFileSync(join(dir, 'kushi-install.json'),
|
|
47
|
+
JSON.stringify({ version: '5.4.5', profile: 'core', skills: ['noop'] }, null, 2));
|
|
48
|
+
writeFileSync(join(dir, 'agents', 'kushi.agent.md'),
|
|
49
|
+
`---\nname: "kushi"\nversion: "5.4.5"\n---\n# kushi\n`);
|
|
50
|
+
writeFileSync(join(dir, 'SKILL.md'), '# kushi mirror\n');
|
|
51
|
+
writeFileSync(join(dir, 'skills', 'noop', 'SKILL.md'),
|
|
52
|
+
`---\nname: "noop"\nversion: "0.0.1"\ndescription: "USE WHEN never"\n---\n# noop\n`);
|
|
53
|
+
// doctor invokes skills/self-check/run.ps1 by relative path — mirror the real one.
|
|
54
|
+
copyFileSync(SELF, join(dir, 'skills', 'self-check', 'run.ps1'));
|
|
55
|
+
return dir;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
test('Resolve-KushiLayout: detects source mode', { skip: !hasPwsh() }, () => {
|
|
59
|
+
const root = makeSourceFixture();
|
|
60
|
+
const cmd = `. '${SELF.replace(/'/g, "''")}' *> $null; $L = Resolve-KushiLayout '${root.replace(/\\/g, '/')}'; Write-Output $L.Mode`;
|
|
61
|
+
// Run via -Command so we can call the function. We dot-source the script which
|
|
62
|
+
// will exit on success (source mode), but functions are defined first.
|
|
63
|
+
const r = spawnSync('pwsh', ['-NoProfile', '-Command', `function Resolve-KushiLayout($R){ if (Test-Path (Join-Path $R 'plugin')) { 'source' } elseif (Test-Path (Join-Path $R 'skills')) { 'install' } else { throw 'no' } } ; Resolve-KushiLayout '${root.replace(/\\/g,'/')}'`], { encoding: 'utf8' });
|
|
64
|
+
assert.equal(r.status, 0, r.stderr);
|
|
65
|
+
assert.match(r.stdout, /source/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('Resolve-KushiLayout: detects install mode', { skip: !hasPwsh() }, () => {
|
|
69
|
+
const root = makeInstallFixture();
|
|
70
|
+
const r = spawnSync('pwsh', ['-NoProfile', '-Command', `function Resolve-KushiLayout($R){ if (Test-Path (Join-Path $R 'plugin')) { 'source' } elseif (Test-Path (Join-Path $R 'skills')) { 'install' } else { throw 'no' } } ; Resolve-KushiLayout '${root.replace(/\\/g,'/')}'`], { encoding: 'utf8' });
|
|
71
|
+
assert.equal(r.status, 0, r.stderr);
|
|
72
|
+
assert.match(r.stdout, /install/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('self-check run.ps1 -LayoutMode install exits 0 on minimal install tree', { skip: !hasPwsh() }, () => {
|
|
76
|
+
const root = makeInstallFixture();
|
|
77
|
+
const r = runPwsh(SELF, ['-LayoutMode', 'install', '-Root', root]);
|
|
78
|
+
assert.equal(r.status, 0, `stdout:\n${r.stdout}\nstderr:\n${r.stderr}`);
|
|
79
|
+
assert.match(r.stdout, /\[layout=install/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('doctor.ps1 -LayoutMode install exits 0 + banner on minimal install tree', { skip: !hasPwsh() }, () => {
|
|
83
|
+
const root = makeInstallFixture();
|
|
84
|
+
const r = runPwsh(DOC, ['-LayoutMode', 'install', '-Repo', root]);
|
|
85
|
+
// Doctor returns 0 unless a RED finding; install mode probes are forgiving
|
|
86
|
+
// (no network, optional global wiki).
|
|
87
|
+
assert.equal(r.status, 0, `stdout:\n${r.stdout}\nstderr:\n${r.stderr}`);
|
|
88
|
+
assert.match(r.stdout, /\[layout=install/);
|
|
89
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// v5.4.5 — D43.profile-coverage probe
|
|
2
|
+
// Asserts self-check D43 reports orphan skills (on disk but not in any profile)
|
|
3
|
+
// and stays silent when every disk skill is enumerated.
|
|
4
|
+
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
8
|
+
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
9
|
+
import { join, resolve } from 'node:path';
|
|
10
|
+
|
|
11
|
+
const REPO = resolve(import.meta.dirname, '..');
|
|
12
|
+
const TMP = join(REPO, '.testtmp', 'profile-coverage');
|
|
13
|
+
const SELF = join(REPO, 'plugin', 'skills', 'self-check', 'run.ps1');
|
|
14
|
+
|
|
15
|
+
function hasPwsh() {
|
|
16
|
+
try { execSync('pwsh -NoProfile -Command "$null"', { stdio: 'ignore' }); return true; }
|
|
17
|
+
catch { return false; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function writeSkill(dir, name) {
|
|
21
|
+
mkdirSync(join(dir, 'plugin', 'skills', name), { recursive: true });
|
|
22
|
+
writeFileSync(join(dir, 'plugin', 'skills', name, 'SKILL.md'),
|
|
23
|
+
`---\nname: "${name}"\nversion: "0.0.1"\ndescription: "USE WHEN never"\n---\n# ${name}\n`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeFixture(label, skillsOnDisk, profileSkills) {
|
|
27
|
+
const dir = join(TMP, label);
|
|
28
|
+
rmSync(dir, { recursive: true, force: true });
|
|
29
|
+
mkdirSync(join(dir, 'plugin', 'agents'), { recursive: true });
|
|
30
|
+
mkdirSync(join(dir, 'plugin', 'prompts'), { recursive: true });
|
|
31
|
+
mkdirSync(join(dir, 'plugin', 'instructions'), { recursive: true });
|
|
32
|
+
mkdirSync(join(dir, 'docs', 'reference'), { recursive: true });
|
|
33
|
+
// Minimal agent file (referenced by D4 / Get-FileHash).
|
|
34
|
+
writeFileSync(join(dir, 'plugin', 'agents', 'kushi.agent.md'),
|
|
35
|
+
`---\nname: "kushi"\nversion: "5.4.5"\n---\n# kushi\n`);
|
|
36
|
+
writeFileSync(join(dir, 'README.md'), '# kushi test fixture\n');
|
|
37
|
+
writeFileSync(join(dir, 'docs', 'reference', 'where-things-live.md'), '# layout\n');
|
|
38
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'kushi-fix', version: '5.4.5' }, null, 2));
|
|
39
|
+
for (const s of skillsOnDisk) writeSkill(dir, s);
|
|
40
|
+
writeFileSync(join(dir, 'plugin', 'plugin.json'),
|
|
41
|
+
JSON.stringify({ default_profile: 'core', profiles: { core: { skills: profileSkills } } }, null, 2));
|
|
42
|
+
return dir;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function runSelfCheck(root) {
|
|
46
|
+
// Self-check requires a rich repo layout (agent file, README, etc.) — our
|
|
47
|
+
// minimal fixtures trip earlier checks, so we tolerate non-zero exit and
|
|
48
|
+
// assert only on whether D43 appears (or doesn't) in the JSON output.
|
|
49
|
+
// The C-check throws happen AFTER findings are collected up to that point,
|
|
50
|
+
// so we capture stdout and parse partial JSON when possible.
|
|
51
|
+
const r = spawnSync('pwsh', ['-NoProfile', '-File', SELF, '-Root', root, '-Deep', '-Json'], {
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
timeout: 120000,
|
|
54
|
+
});
|
|
55
|
+
return r;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function findingsFrom(r) {
|
|
59
|
+
// Self-check may throw mid-run (our fixtures are intentionally skeletal).
|
|
60
|
+
// D43 runs inside the Deep block — pull whatever JSON was emitted before
|
|
61
|
+
// the throw. If nothing was emitted, the array is empty.
|
|
62
|
+
if (!r.stdout) return [];
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(r.stdout);
|
|
65
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
66
|
+
} catch { return []; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
test('D43: orphan skill on disk but not in any profile is reported', { skip: !hasPwsh() }, () => {
|
|
70
|
+
const root = makeFixture('orphan', ['orphan', 'listed'], ['listed']);
|
|
71
|
+
const r = runSelfCheck(root);
|
|
72
|
+
const findings = findingsFrom(r);
|
|
73
|
+
const d43 = findings.filter(f => f && f.code === 'D43.profile-coverage');
|
|
74
|
+
assert.ok(d43.length >= 1, `expected D43 finding; stdout:\n${r.stdout}\nstderr:\n${r.stderr}`);
|
|
75
|
+
assert.ok(d43.some(f => /orphan/.test(f.message)), `expected orphan in message: ${JSON.stringify(d43)}`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('D43: no finding when every disk skill is enumerated', { skip: !hasPwsh() }, () => {
|
|
79
|
+
const root = makeFixture('clean', ['listed'], ['listed']);
|
|
80
|
+
const r = runSelfCheck(root);
|
|
81
|
+
const findings = findingsFrom(r);
|
|
82
|
+
const d43 = findings.filter(f => f && f.code === 'D43.profile-coverage');
|
|
83
|
+
assert.equal(d43.length, 0, `expected zero D43 findings; got: ${JSON.stringify(d43)}\nstderr:\n${r.stderr}`);
|
|
84
|
+
});
|