kushi-agents 5.4.4 → 5.4.6
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 +108 -12
- package/plugin/skills/self-check/SKILL.md +3 -2
- package/plugin/skills/self-check/run.ps1 +197 -2
- 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.6",
|
|
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",
|
|
@@ -25,14 +25,59 @@ pwsh plugin/skills/doctor/doctor.ps1 -Json
|
|
|
25
25
|
#>
|
|
26
26
|
[CmdletBinding()]
|
|
27
27
|
param(
|
|
28
|
-
[string]$Repo
|
|
28
|
+
[string]$Repo,
|
|
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, script-location-based ===
|
|
38
|
+
# This script always sits at <plugin-root>/skills/doctor/doctor.ps1, where
|
|
39
|
+
# <plugin-root> = <repo>/plugin/ (source) or <host>/skills/kushi/ (install).
|
|
40
|
+
# Derive layout from $PSScriptRoot — never walk repo dirs.
|
|
41
|
+
function Resolve-KushiLayout {
|
|
42
|
+
param([string]$Override)
|
|
43
|
+
if ($Override) {
|
|
44
|
+
if (Test-Path (Join-Path $Override 'plugin')) {
|
|
45
|
+
return @{ Mode='source'; Plugin=(Join-Path $Override 'plugin'); Repo=$Override; HasPluginJson=$true }
|
|
46
|
+
}
|
|
47
|
+
if (Test-Path (Join-Path $Override 'skills')) {
|
|
48
|
+
return @{ Mode='install'; Plugin=$Override; Repo=$Override; HasPluginJson=(Test-Path (Join-Path $Override 'kushi-install.json')) }
|
|
49
|
+
}
|
|
50
|
+
throw "Not a kushi layout: $Override (need either plugin/ or skills/)."
|
|
51
|
+
}
|
|
52
|
+
$pluginRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
|
|
53
|
+
if (Test-Path (Join-Path $pluginRoot 'kushi-install.json')) {
|
|
54
|
+
return @{ Mode='install'; Plugin=$pluginRoot; Repo=$pluginRoot; HasPluginJson=$false }
|
|
55
|
+
}
|
|
56
|
+
$maybeRepo = (Resolve-Path (Join-Path $pluginRoot '..')).Path
|
|
57
|
+
return @{ Mode='source'; Plugin=$pluginRoot; Repo=$maybeRepo; HasPluginJson=(Test-Path (Join-Path $pluginRoot 'plugin.json')) }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function Get-KushiInstallHost([string]$InstallRoot) {
|
|
61
|
+
$norm = ($InstallRoot -replace '\\','/').ToLower()
|
|
62
|
+
if ($norm -match '/\.copilot/') { return 'clawpilot' }
|
|
63
|
+
if ($norm -match '/\.vscode/') { return 'vscode' }
|
|
64
|
+
return 'unknown'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
$Layout = Resolve-KushiLayout $Repo
|
|
68
|
+
if ($LayoutMode -ne 'auto' -and $Layout.Mode -ne $LayoutMode) {
|
|
69
|
+
Write-Error "LayoutMode=$LayoutMode requested but detected $($Layout.Mode) at $($Layout.Plugin)."; exit 2
|
|
70
|
+
}
|
|
71
|
+
if (-not $Repo) { $Repo = $Layout.Repo }
|
|
72
|
+
|
|
73
|
+
if (-not $Json) {
|
|
74
|
+
if ($Layout.Mode -eq 'install') {
|
|
75
|
+
Write-Host ("[layout=install (host={0})]" -f (Get-KushiInstallHost $Layout.Repo))
|
|
76
|
+
} else {
|
|
77
|
+
Write-Host "[layout=source]"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
36
81
|
function Add-Section {
|
|
37
82
|
param(
|
|
38
83
|
[string]$Name,
|
|
@@ -90,9 +135,15 @@ if ($envProblems.Count -eq 0) {
|
|
|
90
135
|
|
|
91
136
|
# ── Section 2: self-check -Deep ─────────────────────────────────────────────
|
|
92
137
|
Write-Banner "2. self-check -Deep" "Cyan"
|
|
93
|
-
$
|
|
138
|
+
if ($Layout.Mode -eq 'source') {
|
|
139
|
+
$selfCheckPath = Join-Path $Layout.Plugin 'skills\self-check\run.ps1'
|
|
140
|
+
} else {
|
|
141
|
+
$selfCheckPath = Join-Path $Layout.Plugin 'skills\self-check\run.ps1'
|
|
142
|
+
}
|
|
94
143
|
if (Test-Path $selfCheckPath) {
|
|
95
|
-
$
|
|
144
|
+
$scArgs = @('-NoProfile','-File',$selfCheckPath,'-Deep','-Json','-Root',$Layout.Repo)
|
|
145
|
+
if ($Layout.Mode -eq 'install') { $scArgs += @('-LayoutMode','install') }
|
|
146
|
+
$scJson = & pwsh @scArgs 2>$null
|
|
96
147
|
$scExit = $LASTEXITCODE
|
|
97
148
|
$scFindings = @()
|
|
98
149
|
try { $scFindings = $scJson | ConvertFrom-Json } catch {}
|
|
@@ -106,15 +157,24 @@ if (Test-Path $selfCheckPath) {
|
|
|
106
157
|
Write-Host " ⚠️ $scCount finding(s)" -ForegroundColor Yellow
|
|
107
158
|
foreach ($f in $scFindings) { Write-Host " [$($f.code)] $($f.message)" -ForegroundColor DarkYellow }
|
|
108
159
|
}
|
|
109
|
-
Add-Section 'self-check' 'yellow' "$scCount finding(s)" 'pwsh
|
|
160
|
+
Add-Section 'self-check' 'yellow' "$scCount finding(s)" 'pwsh skills/self-check/run.ps1 -Deep # then fix each [code]'
|
|
110
161
|
}
|
|
111
162
|
} else {
|
|
112
163
|
if (-not $Json) { Write-Host " ❌ run.ps1 missing" -ForegroundColor Red }
|
|
113
|
-
|
|
164
|
+
if ($Layout.Mode -eq 'install') {
|
|
165
|
+
$hostFlag = switch (Get-KushiInstallHost $Layout.Repo) { 'clawpilot' { '--clawpilot' } 'vscode' { '--vscode' } default { '--all-hosts' } }
|
|
166
|
+
Add-Section 'self-check' 'red' 'run.ps1 not found in install' "Re-install: ``npx kushi-agents@latest $hostFlag --force``"
|
|
167
|
+
} else {
|
|
168
|
+
Add-Section 'self-check' 'red' 'run.ps1 not found' "Restore plugin/skills/self-check/run.ps1 from git."
|
|
169
|
+
}
|
|
114
170
|
}
|
|
115
171
|
|
|
116
172
|
# ── Section 3: canary evals ─────────────────────────────────────────────────
|
|
117
173
|
Write-Banner "3. canary evals" "Cyan"
|
|
174
|
+
if ($Layout.Mode -eq 'install') {
|
|
175
|
+
if (-not $Json) { Write-Host " ℹ️ skipped in install mode (no eval baseline shipped)" -ForegroundColor Gray }
|
|
176
|
+
Add-Section 'canary-evals' 'green' 'skipped (install mode)' ''
|
|
177
|
+
} else {
|
|
118
178
|
Push-Location $Repo
|
|
119
179
|
$canaryOut = & npm run --silent eval:canary 2>&1
|
|
120
180
|
$canaryExit = $LASTEXITCODE
|
|
@@ -141,31 +201,66 @@ if ($canaryExit -eq 0) {
|
|
|
141
201
|
Add-Section 'canary-evals' 'red' "FAIL exit=$canaryExit" 'npm run eval:canary # investigate failing case'
|
|
142
202
|
}
|
|
143
203
|
}
|
|
204
|
+
}
|
|
144
205
|
|
|
145
206
|
# ── Section 4: skill-checker -All ───────────────────────────────────────────
|
|
146
207
|
Write-Banner "4. skill-checker -All" "Cyan"
|
|
147
|
-
$checkerPath = Join-Path $
|
|
208
|
+
$checkerPath = Join-Path $Layout.Plugin 'skills\skill-checker\check-skill.ps1'
|
|
148
209
|
if (Test-Path $checkerPath) {
|
|
149
|
-
$ckOut = & pwsh -NoProfile -File $checkerPath -All -Root $Repo 2>&1
|
|
210
|
+
$ckOut = & pwsh -NoProfile -File $checkerPath -All -Root $Layout.Repo 2>&1
|
|
150
211
|
$ckExit = $LASTEXITCODE
|
|
151
212
|
if ($ckExit -eq 0) {
|
|
152
213
|
if (-not $Json) { Write-Host " ✅ all skills compliant" -ForegroundColor Green }
|
|
153
214
|
Add-Section 'skill-checker' 'green' 'all skills pass blueprint' ''
|
|
154
215
|
} else {
|
|
155
216
|
if (-not $Json) { Write-Host " ⚠️ blueprint violations (exit=$ckExit)" -ForegroundColor Yellow }
|
|
156
|
-
Add-Section 'skill-checker' 'yellow' "exit=$ckExit" 'pwsh
|
|
217
|
+
Add-Section 'skill-checker' 'yellow' "exit=$ckExit" 'pwsh skills/skill-checker/check-skill.ps1 -All # review output'
|
|
157
218
|
}
|
|
158
219
|
} else {
|
|
159
|
-
|
|
220
|
+
if ($Layout.Mode -eq 'install') {
|
|
221
|
+
if (-not $Json) { Write-Host " ⚠️ skill-checker not installed in this profile (full only)" -ForegroundColor Yellow }
|
|
222
|
+
Add-Section 'skill-checker' 'yellow' 'not installed (full profile only)' "Re-install with full profile: ``npx kushi-agents@latest --all-hosts --profile full --force``"
|
|
223
|
+
} else {
|
|
224
|
+
Add-Section 'skill-checker' 'red' 'check-skill.ps1 not found' "Restore plugin/skills/skill-checker/check-skill.ps1"
|
|
225
|
+
}
|
|
160
226
|
}
|
|
161
227
|
|
|
162
228
|
# ── Section 5: Live-install drift ───────────────────────────────────────────
|
|
163
229
|
Write-Banner "5. live-install drift" "Cyan"
|
|
230
|
+
if ($Layout.Mode -eq 'install') {
|
|
231
|
+
# Install mode: compare installed version (from kushi-install.json) to npm latest.
|
|
232
|
+
$installPjPath = Join-Path $Layout.Repo 'kushi-install.json'
|
|
233
|
+
$installedVer = $null
|
|
234
|
+
if (Test-Path $installPjPath) {
|
|
235
|
+
try { $installedVer = (Get-Content -Raw $installPjPath | ConvertFrom-Json).version } catch {}
|
|
236
|
+
}
|
|
237
|
+
$npmOk = $false
|
|
238
|
+
try { $null = & npm --version 2>$null; if ($LASTEXITCODE -eq 0) { $npmOk = $true } } catch {}
|
|
239
|
+
if (-not $npmOk) {
|
|
240
|
+
if (-not $Json) { Write-Host " ℹ️ npm not on PATH (skipped)" -ForegroundColor Gray }
|
|
241
|
+
Add-Section 'live-install' 'green' 'npm not on PATH (skipped)' ''
|
|
242
|
+
} elseif (-not $installedVer) {
|
|
243
|
+
if (-not $Json) { Write-Host " ⚠️ no kushi-install.json version field" -ForegroundColor Yellow }
|
|
244
|
+
$hostFlag = switch (Get-KushiInstallHost $Layout.Repo) { 'clawpilot' { '--clawpilot' } 'vscode' { '--vscode' } default { '--all-hosts' } }
|
|
245
|
+
Add-Section 'live-install' 'yellow' 'install manifest missing version' "Re-install: ``npx kushi-agents@latest $hostFlag --force``"
|
|
246
|
+
} else {
|
|
247
|
+
$latest = $null
|
|
248
|
+
try { $latest = (& npm view kushi-agents version 2>$null).Trim() } catch {}
|
|
249
|
+
if ($latest -and $latest -ne $installedVer) {
|
|
250
|
+
if (-not $Json) { Write-Host " ⚠️ installed=v$installedVer latest=v$latest" -ForegroundColor Yellow }
|
|
251
|
+
$hostFlag = switch (Get-KushiInstallHost $Layout.Repo) { 'clawpilot' { '--clawpilot' } 'vscode' { '--vscode' } default { '--all-hosts' } }
|
|
252
|
+
Add-Section 'live-install' 'yellow' "installed=v$installedVer latest=v$latest" "Re-install: ``npx kushi-agents@latest $hostFlag --force``"
|
|
253
|
+
} else {
|
|
254
|
+
if (-not $Json) { Write-Host " ✅ install matches npm latest" -ForegroundColor Green }
|
|
255
|
+
Add-Section 'live-install' 'green' "installed=v$installedVer (matches npm latest)" ''
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
164
259
|
$liveSkill = Join-Path $userHome '.copilot\m-skills\kushi\SKILL.md'
|
|
165
|
-
$agentFile = Join-Path $
|
|
260
|
+
$agentFile = Join-Path $Layout.Plugin 'agents\kushi.agent.md'
|
|
166
261
|
$metaPath = Join-Path $userHome '.copilot\m-skills\skills-metadata.json'
|
|
167
262
|
$repoVersion = $null
|
|
168
|
-
try { $repoVersion = (Get-Content -Raw (Join-Path $Repo 'package.json') | ConvertFrom-Json).version } catch {}
|
|
263
|
+
try { $repoVersion = (Get-Content -Raw (Join-Path $Layout.Repo 'package.json') | ConvertFrom-Json).version } catch {}
|
|
169
264
|
$liveVersion = $null
|
|
170
265
|
if (Test-Path $metaPath) {
|
|
171
266
|
try {
|
|
@@ -204,6 +299,7 @@ if (-not (Test-Path $liveSkill)) {
|
|
|
204
299
|
}
|
|
205
300
|
}
|
|
206
301
|
}
|
|
302
|
+
}
|
|
207
303
|
|
|
208
304
|
# ── Section 6: Global wiki shape ────────────────────────────────────────────
|
|
209
305
|
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. |
|
|
@@ -26,16 +26,64 @@ pwsh plugin/skills/self-check/run.ps1 -Deep -Json | ConvertFrom-Json
|
|
|
26
26
|
#>
|
|
27
27
|
[CmdletBinding()]
|
|
28
28
|
param(
|
|
29
|
-
[string]$Root
|
|
29
|
+
[string]$Root,
|
|
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, script-location-based ===
|
|
41
|
+
# This script always sits at <plugin-root>/skills/self-check/run.ps1, where
|
|
42
|
+
# <plugin-root> = <repo>/plugin/ (source) or <host>/skills/kushi/ (install).
|
|
43
|
+
# Derive layout from $PSScriptRoot — never walk repo dirs.
|
|
44
|
+
function Resolve-KushiLayout {
|
|
45
|
+
param([string]$Override)
|
|
46
|
+
if ($Override) {
|
|
47
|
+
if (Test-Path (Join-Path $Override 'plugin')) {
|
|
48
|
+
return @{ Mode='source'; Plugin=(Join-Path $Override 'plugin'); Repo=$Override; HasPluginJson=$true }
|
|
49
|
+
}
|
|
50
|
+
if (Test-Path (Join-Path $Override 'skills')) {
|
|
51
|
+
return @{ Mode='install'; Plugin=$Override; Repo=$Override; HasPluginJson=(Test-Path (Join-Path $Override 'kushi-install.json')) }
|
|
52
|
+
}
|
|
53
|
+
throw "Not a kushi layout: $Override (need either plugin/ or skills/)."
|
|
54
|
+
}
|
|
55
|
+
$pluginRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
|
|
56
|
+
if (Test-Path (Join-Path $pluginRoot 'kushi-install.json')) {
|
|
57
|
+
return @{ Mode='install'; Plugin=$pluginRoot; Repo=$pluginRoot; HasPluginJson=$false }
|
|
58
|
+
}
|
|
59
|
+
$maybeRepo = (Resolve-Path (Join-Path $pluginRoot '..')).Path
|
|
60
|
+
return @{ Mode='source'; Plugin=$pluginRoot; Repo=$maybeRepo; HasPluginJson=(Test-Path (Join-Path $pluginRoot 'plugin.json')) }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function Get-KushiInstallHost([string]$InstallRoot) {
|
|
64
|
+
$norm = ($InstallRoot -replace '\\','/').ToLower()
|
|
65
|
+
if ($norm -match '/\.copilot/') { return 'clawpilot' }
|
|
66
|
+
if ($norm -match '/\.vscode/') { return 'vscode' }
|
|
67
|
+
return 'unknown'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
$Layout = Resolve-KushiLayout $Root
|
|
71
|
+
if ($LayoutMode -ne 'auto' -and $Layout.Mode -ne $LayoutMode) {
|
|
72
|
+
Write-Error "LayoutMode=$LayoutMode requested but detected $($Layout.Mode) at $($Layout.Plugin)."
|
|
73
|
+
exit 2
|
|
74
|
+
}
|
|
75
|
+
# Backfill $Root for legacy probes that derive paths from $Root (source-mode only).
|
|
76
|
+
if (-not $Root) { $Root = $Layout.Repo }
|
|
77
|
+
|
|
78
|
+
if (-not $Json) {
|
|
79
|
+
if ($Layout.Mode -eq 'install') {
|
|
80
|
+
$hostName = Get-KushiInstallHost $Layout.Repo
|
|
81
|
+
Write-Host "[layout=install (host=$hostName)]"
|
|
82
|
+
} else {
|
|
83
|
+
Write-Host "[layout=source]"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
39
87
|
function Get-UserConfigRoot {
|
|
40
88
|
# Cross-platform: APPDATA-like location for the live Clawpilot install.
|
|
41
89
|
if ($IsWindows -or $env:OS -eq 'Windows_NT') { return $env:USERPROFILE }
|
|
@@ -106,6 +154,122 @@ function Get-Frontmatter {
|
|
|
106
154
|
return $fm
|
|
107
155
|
}
|
|
108
156
|
|
|
157
|
+
# === v5.4.5: install-mode minimal probe set ===
|
|
158
|
+
# In install mode we can't see plugin.json source, profile definitions, doc-tree,
|
|
159
|
+
# eval baselines, etc. — those are source-author concerns. We run a small probe
|
|
160
|
+
# set focused on integrity of the installed payload itself.
|
|
161
|
+
if ($Layout.Mode -eq 'install') {
|
|
162
|
+
$installRoot = $Layout.Plugin
|
|
163
|
+
$installPj = Join-Path $installRoot 'kushi-install.json'
|
|
164
|
+
$installSkills = Join-Path $installRoot 'skills'
|
|
165
|
+
$installAgent = Join-Path $installRoot 'agents\kushi.agent.md'
|
|
166
|
+
$installSkillMd= Join-Path $installRoot 'SKILL.md'
|
|
167
|
+
$installVersion = $null
|
|
168
|
+
$installProfile = $null
|
|
169
|
+
$installedSkillList = @()
|
|
170
|
+
if (Test-Path $installPj) {
|
|
171
|
+
try {
|
|
172
|
+
$ij = Get-Content -Raw $installPj | ConvertFrom-Json
|
|
173
|
+
$installVersion = $ij.version
|
|
174
|
+
$installProfile = $ij.profile
|
|
175
|
+
if ($ij.skills) { $installedSkillList = @($ij.skills) }
|
|
176
|
+
} catch {
|
|
177
|
+
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
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
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
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# I2: env probe — node + pwsh
|
|
184
|
+
$nodeOk = $false
|
|
185
|
+
try { $null = & node --version 2>$null; if ($LASTEXITCODE -eq 0) { $nodeOk = $true } } catch {}
|
|
186
|
+
if (-not $nodeOk) {
|
|
187
|
+
Add-Finding 'I2.env' 'Install' 'warning' "node not on PATH" "Install Node 18+ from https://nodejs.org/" $null 0
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# I3: every skill enumerated in install manifest exists under skills/
|
|
191
|
+
if (Test-Path $installSkills) {
|
|
192
|
+
foreach ($s in $installedSkillList) {
|
|
193
|
+
$sd = Join-Path $installSkills $s
|
|
194
|
+
if (-not (Test-Path $sd)) {
|
|
195
|
+
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
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
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
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# I4: agent file present + SKILL.md mirror
|
|
203
|
+
if (-not (Test-Path $installAgent)) {
|
|
204
|
+
Add-Finding 'I4.agent' 'Install' 'error' "agent file missing: $installAgent" "Re-install with ``npx kushi-agents@latest --all-hosts --force``." $installAgent 0
|
|
205
|
+
}
|
|
206
|
+
if (-not (Test-Path $installSkillMd)) {
|
|
207
|
+
# Mirror is informational — Clawpilot uses agents/kushi.agent.md, VS Code uses SKILL.md
|
|
208
|
+
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
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# I5: skills-metadata.json sibling under host root (accept any of the two known shapes)
|
|
212
|
+
$userBase = Get-UserConfigRoot
|
|
213
|
+
$candidateMeta = @(
|
|
214
|
+
(Join-Path $userBase '.copilot\m-skills\skills-metadata.json'),
|
|
215
|
+
(Join-Path $userBase '.vscode\chat\skills\skills-metadata.json')
|
|
216
|
+
)
|
|
217
|
+
$foundMeta = $candidateMeta | Where-Object { Test-Path $_ } | Select-Object -First 1
|
|
218
|
+
if (-not $foundMeta) {
|
|
219
|
+
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
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# I6: live drift vs npm latest (suppress if npm not on PATH)
|
|
223
|
+
$npmOk = $false
|
|
224
|
+
try { $null = & npm --version 2>$null; if ($LASTEXITCODE -eq 0) { $npmOk = $true } } catch {}
|
|
225
|
+
if ($npmOk -and $installVersion) {
|
|
226
|
+
$latest = $null
|
|
227
|
+
try { $latest = (& npm view kushi-agents version 2>$null).Trim() } catch {}
|
|
228
|
+
if ($latest -and $latest -ne $installVersion) {
|
|
229
|
+
Add-Finding 'I6.drift' 'Install' 'warning' "installed=v$installVersion latest=v$latest" "Re-install with ``npx kushi-agents@latest --all-hosts --force``." $installPj 0
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
# I7: optional global wiki shape
|
|
234
|
+
$globalRoot = $env:KUSHI_GLOBAL_ROOT
|
|
235
|
+
if (-not $globalRoot) { $globalRoot = Join-Path $userBase '.kushi-global' }
|
|
236
|
+
if (Test-Path $globalRoot) {
|
|
237
|
+
$gs = Join-Path $globalRoot 'State'
|
|
238
|
+
if (-not (Test-Path $gs)) {
|
|
239
|
+
Add-Finding 'I7.global-wiki' 'Install' 'warning' "global wiki at $globalRoot missing State/" "Run ``kushi global init`` to repair scaffold." $globalRoot 0
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
# Emit and exit — skip all source-author C/D checks.
|
|
244
|
+
if ($Targeted) {
|
|
245
|
+
$needle = [regex]::Escape($Targeted)
|
|
246
|
+
$findings = @($findings | Where-Object {
|
|
247
|
+
$_.code -match $needle -or $_.surface -match $needle -or $_.file -match $needle -or $_.message -match $needle
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
if ($Json) {
|
|
251
|
+
$findings | ConvertTo-Json -Depth 6
|
|
252
|
+
} else {
|
|
253
|
+
$stamp = Get-Date -Format 'yyyy-MM-dd HH:mm'
|
|
254
|
+
Write-Host ""
|
|
255
|
+
Write-Host "### Self-Check Results — Kushi install v$installVersion (profile=$installProfile) @ $stamp"
|
|
256
|
+
Write-Host ""
|
|
257
|
+
if ($findings.Count -eq 0) {
|
|
258
|
+
Write-Host "✅ Install integrity OK."
|
|
259
|
+
} else {
|
|
260
|
+
foreach ($x in $findings) {
|
|
261
|
+
Write-Host "⚠️ [$($x.code)] $($x.message)"
|
|
262
|
+
if ($x.file) { Write-Host " file: $($x.file)" }
|
|
263
|
+
Write-Host " fix : $($x.fix)"
|
|
264
|
+
Write-Host ""
|
|
265
|
+
}
|
|
266
|
+
Write-Host "⚠️ Found $($findings.Count) install-integrity gap(s)."
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if ($StrictExit -and $findings.Count -gt 0) { exit 1 }
|
|
270
|
+
exit 0
|
|
271
|
+
}
|
|
272
|
+
|
|
109
273
|
# === Discovery (source of truth) ===
|
|
110
274
|
$pluginDir = Join-Path $Root 'plugin'
|
|
111
275
|
$skillsDir = Join-Path $pluginDir 'skills'
|
|
@@ -2198,6 +2362,37 @@ process.stdout.write(JSON.stringify(out));
|
|
|
2198
2362
|
}
|
|
2199
2363
|
}
|
|
2200
2364
|
|
|
2365
|
+
# D43.profile-coverage (v5.4.5+)
|
|
2366
|
+
# Source-mode only: walk plugin/skills/ and ensure every disk skill is enumerated
|
|
2367
|
+
# by at least one profile in plugin.json. Orphans are skills that exist in the
|
|
2368
|
+
# source tree but won't ship to any user via `npx kushi-agents`. We union skills
|
|
2369
|
+
# across all profiles WITHOUT resolving `extends` chains — the union is the
|
|
2370
|
+
# superset (an orphan is one nobody references at any level), so chain-resolution
|
|
2371
|
+
# is unnecessary for this check and keeps the probe O(profiles+skills).
|
|
2372
|
+
$pjFileD43 = Join-Path $pluginDir 'plugin.json'
|
|
2373
|
+
if (Test-Path $pjFileD43) {
|
|
2374
|
+
try {
|
|
2375
|
+
$pjD43 = Get-Content -Raw $pjFileD43 | ConvertFrom-Json
|
|
2376
|
+
$diskSkills = (Get-ChildItem $skillsDir -Directory | Where-Object {
|
|
2377
|
+
$_.Name -notmatch '^\.' -and $_.Name -notmatch '^_' -and $_.Name -ne 'intro'
|
|
2378
|
+
}).Name
|
|
2379
|
+
$allEnumerated = New-Object System.Collections.Generic.HashSet[string]
|
|
2380
|
+
if ($pjD43.profiles) {
|
|
2381
|
+
foreach ($pn in $pjD43.profiles.PSObject.Properties.Name) {
|
|
2382
|
+
$node = $pjD43.profiles.$pn
|
|
2383
|
+
if ($node.skills) { foreach ($s in $node.skills) { $null = $allEnumerated.Add($s) } }
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
foreach ($ds in $diskSkills) {
|
|
2387
|
+
if (-not $allEnumerated.Contains($ds)) {
|
|
2388
|
+
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
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
} catch {
|
|
2392
|
+
# C9 already reports plugin.json parse failures; stay quiet here.
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2201
2396
|
# === Output ===
|
|
2202
2397
|
if ($Targeted) {
|
|
2203
2398
|
# 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
|
+
});
|