kushi-agents 4.4.3 → 4.7.4
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 +4 -0
- package/package.json +4 -4
- package/plugin/agents/kushi.agent.md +30 -14
- package/plugin/config/studios.json +37 -0
- package/plugin/config/studios.schema.json +45 -0
- package/plugin/instructions/auth-and-retry.instructions.md +268 -1
- package/plugin/instructions/engagement-root-resolution.instructions.md +5 -1
- package/plugin/instructions/evidence-thoroughness.instructions.md +103 -1
- package/plugin/instructions/fuzzy-disambiguation.instructions.md +97 -0
- package/plugin/instructions/identity-resolution.instructions.md +76 -0
- package/plugin/instructions/issue-recovery.instructions.md +58 -0
- package/plugin/instructions/kushi-config-root.instructions.md +66 -0
- package/plugin/instructions/loop-bootstrap-discovery.instructions.md +105 -0
- package/plugin/instructions/m365-id-registry.instructions.md +1 -1
- package/plugin/instructions/onedrive-pin-policy.instructions.md +132 -0
- package/plugin/instructions/per-source-verification-gate.instructions.md +193 -0
- package/plugin/instructions/sharepoint-to-onedrive-sync.instructions.md +116 -0
- package/plugin/instructions/status-color-rule.instructions.md +62 -0
- package/plugin/instructions/studio-registry.instructions.md +48 -0
- package/plugin/instructions/update-ledger.instructions.md +1 -1
- package/plugin/instructions/verbatim-by-default.instructions.md +1 -1
- package/plugin/instructions/vertex-emit.instructions.md +120 -0
- package/plugin/instructions/workiq-input-sanitization.instructions.md +43 -0
- package/plugin/instructions/workiq-onenote-query-shape.instructions.md +79 -0
- package/plugin/instructions/workiq-only.instructions.md +15 -9
- package/plugin/learnings/loop.md +11 -0
- package/plugin/learnings/onenote.md +27 -1
- package/plugin/lib/Get-KushiConfig.ps1 +22 -9
- package/plugin/lib/detect-vertex-repo.mjs +96 -0
- package/plugin/lib/render-vertex.mjs +249 -0
- package/plugin/lib/sanitize-workiq-input.mjs +72 -0
- package/plugin/lib/studio-registry.mjs +39 -0
- package/plugin/lib/vertex-validate.mjs +121 -0
- package/plugin/plugin.json +16 -6
- package/plugin/prompts/bootstrap.prompt.md +9 -7
- package/plugin/prompts/emit-vertex.prompt.md +33 -0
- package/plugin/prompts/setup.prompt.md +29 -0
- package/plugin/prompts/vertex-link.prompt.md +27 -0
- package/plugin/skills/aggregate-project/SKILL.md +24 -2
- package/plugin/skills/apply-ado-update/SKILL.md +9 -4
- package/plugin/skills/ask-project/SKILL.md +4 -0
- package/plugin/skills/bootstrap-project/SKILL.md +67 -41
- package/plugin/skills/consolidate-evidence/SKILL.md +5 -1
- package/plugin/skills/emit-vertex/README.md +37 -0
- package/plugin/skills/emit-vertex/SKILL.md +173 -0
- package/plugin/skills/intro/SKILL.md +2 -0
- package/plugin/skills/propose-ado-update/SKILL.md +8 -3
- package/plugin/skills/pull-ado/SKILL.md +11 -1
- package/plugin/skills/pull-crm/SKILL.md +12 -2
- package/plugin/skills/pull-email/SKILL.md +11 -1
- package/plugin/skills/pull-loop/README.md +64 -0
- package/plugin/skills/pull-loop/SKILL.md +180 -0
- package/plugin/skills/pull-loop/runner.mjs +261 -0
- package/plugin/skills/pull-loop/write-snapshot.mjs +181 -0
- package/plugin/skills/pull-meetings/SKILL.md +11 -1
- package/plugin/skills/pull-misc/README.md +4 -4
- package/plugin/skills/pull-misc/SKILL.md +18 -12
- package/plugin/skills/pull-onenote/SKILL.md +71 -19
- package/plugin/skills/pull-sharepoint/SKILL.md +11 -2
- package/plugin/skills/pull-teams/SKILL.md +11 -2
- package/plugin/skills/refresh-project/SKILL.md +38 -7
- package/plugin/skills/self-check/SKILL.md +14 -1
- package/plugin/skills/self-check/run.ps1 +442 -20
- package/plugin/skills/setup/SKILL.md +377 -0
- package/plugin/skills/vertex-link/SKILL.md +143 -0
- package/plugin/templates/init/m365-auth.template.json +10 -4
- package/plugin/templates/init/project-evidence.template.yml +10 -3
- package/plugin/templates/init/project-integrations.template.yml +5 -0
- package/plugin/templates/snapshot/ado-item.template.md +1 -1
- package/plugin/templates/snapshot/crm-record.template.md +1 -1
- package/plugin/templates/snapshot/meetings-series-index.template.md +1 -1
- package/plugin/templates/snapshot/onenote-page.template.md +1 -1
- package/plugin/templates/snapshot/sharepoint-file.template.md +1 -1
- package/plugin/templates/snapshot/sharepoint-tree.template.md +1 -1
- package/plugin/templates/snapshot/teams-roster.template.md +1 -1
- package/plugin/templates/weekly/ado-stream.template.md +1 -1
- package/plugin/templates/weekly/crm-stream.template.md +1 -1
- package/plugin/templates/weekly/email-stream.template.md +1 -1
- package/plugin/templates/weekly/meetings-stream.template.md +1 -1
- package/plugin/templates/weekly/onenote-stream.template.md +1 -1
- package/plugin/templates/weekly/sharepoint-stream.template.md +1 -1
- package/plugin/templates/weekly/teams-stream.template.md +1 -1
- package/src/check-workiq.mjs +109 -15
- package/src/config-loader.mjs +71 -13
- package/src/config-root-resolve.test.mjs +137 -0
- package/src/detect-vertex-repo.test.mjs +128 -0
- package/src/emit-vertex.e2e.test.mjs +308 -0
- package/src/forbidden-workiq-phrasings.test.mjs +111 -0
- package/src/main.mjs +11 -2
- package/src/sanitize-workiq-input.test.mjs +45 -0
- package/src/vertex-validate.test.mjs +142 -0
- package/plugin/instructions/az-auth-conditional.instructions.md +0 -39
- package/plugin/instructions/azure-auth-patterns.instructions.md +0 -233
- package/plugin/instructions/thoroughness-detector.instructions.md +0 -105
- package/plugin/instructions/workiq-first.instructions.md +0 -31
|
@@ -35,6 +35,51 @@ param(
|
|
|
35
35
|
$ErrorActionPreference = "Stop"
|
|
36
36
|
$findings = New-Object System.Collections.Generic.List[object]
|
|
37
37
|
|
|
38
|
+
function Get-UserConfigRoot {
|
|
39
|
+
# Cross-platform: APPDATA-like location for the live Clawpilot install.
|
|
40
|
+
if ($IsWindows -or $env:OS -eq 'Windows_NT') { return $env:USERPROFILE }
|
|
41
|
+
if ($env:HOME) { return $env:HOME }
|
|
42
|
+
return [Environment]::GetFolderPath('UserProfile')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Get-ProjectsRootFromYaml {
|
|
46
|
+
# Read projects_root: from a YAML file without a yaml dep. Tolerates quoted/unquoted values.
|
|
47
|
+
param([string]$YamlPath)
|
|
48
|
+
if (-not (Test-Path $YamlPath)) { return $null }
|
|
49
|
+
try {
|
|
50
|
+
$lines = Get-Content -Path $YamlPath -ErrorAction SilentlyContinue
|
|
51
|
+
foreach ($l in $lines) {
|
|
52
|
+
if ($l -match '^\s*projects_root\s*:\s*["'']?([^"''#]+?)["'']?\s*(?:#.*)?$') {
|
|
53
|
+
$val = $Matches[1].Trim()
|
|
54
|
+
if ($val) { return $val }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch {}
|
|
58
|
+
return $null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function Resolve-EngagementRoots {
|
|
62
|
+
# Returns the union of resolvable engagement roots, in priority order:
|
|
63
|
+
# 1. $env:KUSHI_ENGAGEMENT_ROOT
|
|
64
|
+
# 2. <repo-root>/.kushi/config/user/project-evidence.yml#projects_root
|
|
65
|
+
# 3. <user-config-root>/.copilot/m-skills/kushi/config/user/project-evidence.yml#projects_root
|
|
66
|
+
# If none resolve, returns an empty array. Self-check D13/D14 will skip cleanly on machines
|
|
67
|
+
# without an engagement root configured (e.g. CI, contributors who haven't run setup yet).
|
|
68
|
+
param([string]$RepoRoot)
|
|
69
|
+
$roots = New-Object System.Collections.Generic.List[string]
|
|
70
|
+
if ($env:KUSHI_ENGAGEMENT_ROOT -and (Test-Path $env:KUSHI_ENGAGEMENT_ROOT)) {
|
|
71
|
+
[void]$roots.Add($env:KUSHI_ENGAGEMENT_ROOT)
|
|
72
|
+
}
|
|
73
|
+
$workspaceCfg = Join-Path $RepoRoot '.kushi\config\user\project-evidence.yml'
|
|
74
|
+
$wsRoot = Get-ProjectsRootFromYaml -YamlPath $workspaceCfg
|
|
75
|
+
if ($wsRoot -and (Test-Path $wsRoot)) { [void]$roots.Add($wsRoot) }
|
|
76
|
+
$userBase = Get-UserConfigRoot
|
|
77
|
+
$liveCfg = Join-Path $userBase '.copilot\m-skills\kushi\config\user\project-evidence.yml'
|
|
78
|
+
$liveRoot = Get-ProjectsRootFromYaml -YamlPath $liveCfg
|
|
79
|
+
if ($liveRoot -and (Test-Path $liveRoot)) { [void]$roots.Add($liveRoot) }
|
|
80
|
+
return ($roots | Select-Object -Unique)
|
|
81
|
+
}
|
|
82
|
+
|
|
38
83
|
function Add-Finding {
|
|
39
84
|
param($Code, $Surface, $Severity, $Message, $Fix, $File, $Line)
|
|
40
85
|
$findings.Add([PSCustomObject]@{
|
|
@@ -127,7 +172,7 @@ foreach ($p in $promptFiles) {
|
|
|
127
172
|
|
|
128
173
|
# === C5: instructions referenced somewhere ===
|
|
129
174
|
foreach ($inst in $instructionFiles) {
|
|
130
|
-
$needle = $inst.Name # e.g. workiq-
|
|
175
|
+
$needle = $inst.Name # e.g. workiq-only.instructions.md
|
|
131
176
|
$referenced = $false
|
|
132
177
|
foreach ($key in $mdText.Keys) {
|
|
133
178
|
if ($key -eq $inst.FullName) { continue }
|
|
@@ -299,13 +344,13 @@ foreach ($name in $fdeSkills) {
|
|
|
299
344
|
}
|
|
300
345
|
}
|
|
301
346
|
|
|
302
|
-
# === C12: pull-* skills must reference thoroughness-detector
|
|
347
|
+
# === C12: pull-* skills must reference evidence-thoroughness (merged thoroughness-detector v4.4.9) ===
|
|
303
348
|
foreach ($d in $skillDirs | Where-Object { $_.Name -like 'pull-*' }) {
|
|
304
349
|
$f = Join-Path $d.FullName 'SKILL.md'
|
|
305
350
|
$text = $mdText[$f]
|
|
306
351
|
if (-not $text) { continue }
|
|
307
|
-
if ($text -notmatch 'thoroughness
|
|
308
|
-
Add-Finding C12 'Thoroughness enforcement' 'warning' "Skill $($d.Name) doesn't reference thoroughness
|
|
352
|
+
if ($text -notmatch 'evidence-thoroughness\.instructions\.md') {
|
|
353
|
+
Add-Finding C12 'Thoroughness enforcement' 'warning' "Skill $($d.Name) doesn't reference evidence-thoroughness.instructions.md" "Add 'runtime detector + auto-retry + paste-prompt per ``evidence-thoroughness.instructions.md``' to the skill intro." $f 0
|
|
309
354
|
}
|
|
310
355
|
}
|
|
311
356
|
$evidenceTemplateDirs = @('templates\weekly','templates\snapshot')
|
|
@@ -317,17 +362,17 @@ foreach ($td in $evidenceTemplateDirs) {
|
|
|
317
362
|
if ($nonEvidenceTemplates -contains $_.Name) { return }
|
|
318
363
|
$tx = Get-Content -Raw $_.FullName
|
|
319
364
|
if ($tx -notmatch '##\s+Validation') {
|
|
320
|
-
Add-Finding C12 'Thoroughness enforcement' 'warning' "Evidence template $($_.Name) is missing '## Validation' block" "Append a Validation checklist per thoroughness
|
|
365
|
+
Add-Finding C12 'Thoroughness enforcement' 'warning' "Evidence template $($_.Name) is missing '## Validation' block" "Append a Validation checklist per evidence-thoroughness.instructions.md so the skill can tick checks on write." $_.FullName 0
|
|
321
366
|
}
|
|
322
367
|
}
|
|
323
368
|
}
|
|
324
369
|
$pasteTpl = Join-Path $pluginDir 'templates\paste-prompt.md'
|
|
325
370
|
if (-not (Test-Path $pasteTpl)) {
|
|
326
|
-
Add-Finding C12 'Thoroughness enforcement' 'warning' "plugin/templates/paste-prompt.md is missing" "Create the paste-prompt template referenced by thoroughness
|
|
371
|
+
Add-Finding C12 'Thoroughness enforcement' 'warning' "plugin/templates/paste-prompt.md is missing" "Create the paste-prompt template referenced by evidence-thoroughness.instructions.md." $pluginDir 0
|
|
327
372
|
}
|
|
328
|
-
$detectorFile = Join-Path $pluginDir 'instructions\thoroughness
|
|
373
|
+
$detectorFile = Join-Path $pluginDir 'instructions\evidence-thoroughness.instructions.md'
|
|
329
374
|
if (-not (Test-Path $detectorFile)) {
|
|
330
|
-
Add-Finding C12 'Thoroughness enforcement' 'warning' "plugin/instructions/thoroughness
|
|
375
|
+
Add-Finding C12 'Thoroughness enforcement' 'warning' "plugin/instructions/evidence-thoroughness.instructions.md is missing" "Create the detector instructions file — pull-* skills cite it." $pluginDir 0
|
|
331
376
|
}
|
|
332
377
|
|
|
333
378
|
# === Deep checks ===
|
|
@@ -533,13 +578,11 @@ if ($Deep) {
|
|
|
533
578
|
if (-not (Test-Path $verbatimInst)) {
|
|
534
579
|
Add-Finding D13 'Meetings verbatim doctrine' 'warning' "plugin/instructions/meetings-verbatim-required.instructions.md is missing" "Restore the meetings-verbatim-required instruction from kushi v3.10.0." $verbatimInst 0
|
|
535
580
|
}
|
|
536
|
-
# Walk known engagement roots to find Evidence/*/meetings/stream/*.md files
|
|
537
|
-
$
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
$
|
|
541
|
-
if (Test-Path $defaultEngRoot) { $engagementRoots += $defaultEngRoot }
|
|
542
|
-
foreach ($engRoot in ($engagementRoots | Select-Object -Unique)) {
|
|
581
|
+
# Walk known engagement roots to find Evidence/*/meetings/stream/*.md files.
|
|
582
|
+
# Resolution order: $env:KUSHI_ENGAGEMENT_ROOT → workspace .kushi config → live install config.
|
|
583
|
+
# No hardcoded personal paths.
|
|
584
|
+
$engagementRoots = Resolve-EngagementRoots -RepoRoot $Root
|
|
585
|
+
foreach ($engRoot in $engagementRoots) {
|
|
543
586
|
$streamFiles = Get-ChildItem -Path $engRoot -Recurse -Filter '*_meetings-stream.md' -ErrorAction SilentlyContinue |
|
|
544
587
|
Where-Object { $_.FullName -match '\\Evidence\\[^\\]+\\(meetings|Meetings)\\stream\\' }
|
|
545
588
|
foreach ($sf in $streamFiles) {
|
|
@@ -627,11 +670,9 @@ if ($Deep) {
|
|
|
627
670
|
'_Weekly Summaries','_WeeklySummaries','weekly-summaries',
|
|
628
671
|
'email','teams','meetings','onenote','sharepoint','crm','ado'
|
|
629
672
|
)
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
$
|
|
633
|
-
if (Test-Path $layoutDefaultRoot) { $layoutEngRoots += $layoutDefaultRoot }
|
|
634
|
-
foreach ($engRoot in ($layoutEngRoots | Select-Object -Unique)) {
|
|
673
|
+
# Engagement-root resolution (no hardcoded personal paths) — see Resolve-EngagementRoots.
|
|
674
|
+
$layoutEngRoots = Resolve-EngagementRoots -RepoRoot $Root
|
|
675
|
+
foreach ($engRoot in $layoutEngRoots) {
|
|
635
676
|
$projectDirs = Get-ChildItem -Path $engRoot -Directory -ErrorAction SilentlyContinue |
|
|
636
677
|
Where-Object { Test-Path (Join-Path $_.FullName 'Evidence') }
|
|
637
678
|
foreach ($pd in $projectDirs) {
|
|
@@ -678,6 +719,387 @@ if ($Deep) {
|
|
|
678
719
|
Add-Finding D15 'Legacy paths' 'warning' $msg $fix $m.Path $m.LineNumber
|
|
679
720
|
}
|
|
680
721
|
}
|
|
722
|
+
|
|
723
|
+
# D16: cross-platform parity (Windows + macOS) — kushi v4.4.6+
|
|
724
|
+
# Contract: every contributor-facing install/setup surface must address BOTH Windows and macOS.
|
|
725
|
+
$d16Touchpoints = @(
|
|
726
|
+
@{ Path = 'plugin/skills/setup/SKILL.md'; MustContain = @('winget', 'brew install --cask'); Why = 'setup recovery prompts must include both Windows (winget) and macOS (brew) install paths' }
|
|
727
|
+
@{ Path = 'docs/getting-started/install-workiq.md'; MustContain = @('winget', 'brew', '### macOS'); Why = 'install-workiq doc must have explicit Windows + macOS sections' }
|
|
728
|
+
@{ Path = 'src/check-workiq.mjs'; MustContain = @("'win32'", "'darwin'"); Why = 'check-workiq must branch install hints by process.platform for Windows + macOS' }
|
|
729
|
+
)
|
|
730
|
+
foreach ($tp in $d16Touchpoints) {
|
|
731
|
+
$tpPath = Join-Path $Root $tp.Path
|
|
732
|
+
if (-not (Test-Path $tpPath)) {
|
|
733
|
+
Add-Finding D16 'Cross-platform parity' 'warning' "Required parity touchpoint missing: $($tp.Path)" "Create the file or update D16 in self-check if intentionally removed." $tpPath 0
|
|
734
|
+
continue
|
|
735
|
+
}
|
|
736
|
+
$body = Get-Content -Raw $tpPath
|
|
737
|
+
foreach ($needle in $tp.MustContain) {
|
|
738
|
+
if ($body -notmatch [regex]::Escape($needle)) {
|
|
739
|
+
Add-Finding D16 'Cross-platform parity' 'warning' "$($tp.Path) missing '$needle' — $($tp.Why)" "Add a macOS/Windows-branched block. Windows uses winget install Microsoft.WorkIQ; macOS uses brew install --cask microsoft-workiq + (optionally) brew install --cask powershell for pwsh." $tpPath 0
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
$shPath = Join-Path $Root 'plugin/skills/self-check/run.sh'
|
|
744
|
+
$ps1Path = Join-Path $Root 'plugin/skills/self-check/run.ps1'
|
|
745
|
+
if (-not (Test-Path $shPath)) {
|
|
746
|
+
Add-Finding D16 'Cross-platform parity' 'warning' "self-check is missing run.sh (macOS/Linux entrypoint)" "Restore plugin/skills/self-check/run.sh — pwsh-7 wrapper for non-Windows contributors." $shPath 0
|
|
747
|
+
}
|
|
748
|
+
if (-not (Test-Path $ps1Path)) {
|
|
749
|
+
Add-Finding D16 'Cross-platform parity' 'warning' "self-check is missing run.ps1 (Windows entrypoint)" "Restore plugin/skills/self-check/run.ps1." $ps1Path 0
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
# D17: fuzzy-disambiguation doctrine cited by skills that resolve names -> IDs (v4.4.7+)
|
|
753
|
+
$fuzzyInst = Join-Path $instructionsDir 'fuzzy-disambiguation.instructions.md'
|
|
754
|
+
if (-not (Test-Path $fuzzyInst)) {
|
|
755
|
+
Add-Finding D17 'Fuzzy disambiguation' 'warning' "plugin/instructions/fuzzy-disambiguation.instructions.md is missing" "Restore the v4.4.7 fuzzy doctrine." $fuzzyInst 0
|
|
756
|
+
} else {
|
|
757
|
+
$fuzzyCallers = @(
|
|
758
|
+
'pull-onenote','pull-sharepoint','pull-teams','pull-crm','pull-ado','pull-email',
|
|
759
|
+
'engagement-root-resolution.instructions.md','ask-project'
|
|
760
|
+
)
|
|
761
|
+
foreach ($caller in $fuzzyCallers) {
|
|
762
|
+
$candidate = if ($caller -like '*.instructions.md') {
|
|
763
|
+
Join-Path $instructionsDir $caller
|
|
764
|
+
} else {
|
|
765
|
+
Join-Path (Join-Path $skillsDir $caller) 'SKILL.md'
|
|
766
|
+
}
|
|
767
|
+
if (Test-Path $candidate) {
|
|
768
|
+
$body = Get-Content -Raw $candidate
|
|
769
|
+
if ($body -notmatch 'fuzzy-disambiguation\.instructions\.md') {
|
|
770
|
+
Add-Finding D17 'Fuzzy disambiguation' 'warning' "$caller does not reference fuzzy-disambiguation.instructions.md" "Add a one-line cite: 'Name → ID resolution follows fuzzy-disambiguation.instructions.md (universal flow).' " $candidate 0
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
# D18: per-source-verification-gate cited by pull-* + orchestrators (v4.4.7+)
|
|
777
|
+
$gateInst = Join-Path $instructionsDir 'per-source-verification-gate.instructions.md'
|
|
778
|
+
if (-not (Test-Path $gateInst)) {
|
|
779
|
+
Add-Finding D18 'Verification gate' 'warning' "plugin/instructions/per-source-verification-gate.instructions.md is missing" "Restore the v4.4.7 verification-gate doctrine." $gateInst 0
|
|
780
|
+
} else {
|
|
781
|
+
$gateCallers = @('bootstrap-project','refresh-project','aggregate-project') + (Get-ChildItem $skillsDir -Directory | Where-Object { $_.Name -like 'pull-*' } | Select-Object -ExpandProperty Name)
|
|
782
|
+
foreach ($caller in $gateCallers) {
|
|
783
|
+
$sf = Join-Path (Join-Path $skillsDir $caller) 'SKILL.md'
|
|
784
|
+
if (Test-Path $sf) {
|
|
785
|
+
$body = Get-Content -Raw $sf
|
|
786
|
+
if ($body -notmatch 'per-source-verification-gate\.instructions\.md') {
|
|
787
|
+
Add-Finding D18 'Verification gate' 'warning' "$caller/SKILL.md does not reference per-source-verification-gate.instructions.md" "Add a one-line cite in the front blockquote or References section." $sf 0
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
# D19: FOLLOW-UPS.md format compliance — when present, every Open entry has 5 required fields
|
|
794
|
+
$followUpsFiles = Get-ChildItem -Path $Root -Recurse -Filter 'FOLLOW-UPS.md' -ErrorAction SilentlyContinue | Where-Object { $_.FullName -notmatch '[\\/](node_modules|\.git|customer_docs)[\\/]' } -ErrorAction SilentlyContinue
|
|
795
|
+
foreach ($fu in $followUpsFiles) {
|
|
796
|
+
$body = Get-Content -Raw $fu.FullName
|
|
797
|
+
if ($body -match '## Open follow-ups([\s\S]*?)(##|\Z)') {
|
|
798
|
+
$openBlock = $Matches[1]
|
|
799
|
+
# Each ### sub-entry should mention the 5 anchors
|
|
800
|
+
$entries = [regex]::Matches($openBlock, '(?m)^### .+$')
|
|
801
|
+
foreach ($entry in $entries) {
|
|
802
|
+
$entryStart = $entry.Index + $entry.Length
|
|
803
|
+
$next = $openBlock.IndexOf("`n### ", $entryStart)
|
|
804
|
+
$entryEnd = if ($next -lt 0) { $openBlock.Length } else { $next }
|
|
805
|
+
$entryText = $openBlock.Substring($entryStart, $entryEnd - $entryStart)
|
|
806
|
+
$required = @('Gate failures','Next-time-do','Tracking','Run-log entry','Status')
|
|
807
|
+
foreach ($r in $required) {
|
|
808
|
+
if ($entryText -notmatch [regex]::Escape($r)) {
|
|
809
|
+
Add-Finding D19 'FOLLOW-UPS format' 'warning' "$($fu.FullName): an Open follow-up entry is missing the '$r' field" "Per per-source-verification-gate.instructions.md, every Open entry must have all 5 fields (Gate failures, Next-time-do, Tracking, Run-log entry, Status)." $fu.FullName 0
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
# D20: README verbs table coverage — every prompt has a row + every row has a prompt
|
|
817
|
+
if (Test-Path $readmeFile) {
|
|
818
|
+
$readme = Get-Content -Raw $readmeFile
|
|
819
|
+
if ($readme -match '## Verbs\s*\r?\n([\s\S]*?)(?=\r?\n##\s)') {
|
|
820
|
+
$verbsBlock = $Matches[1]
|
|
821
|
+
# Verb rows: lines starting with | `verb`
|
|
822
|
+
$verbRowMatches = [regex]::Matches($verbsBlock, '\|\s*`([a-z0-9-]+)`')
|
|
823
|
+
$documentedVerbs = $verbRowMatches | ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique
|
|
824
|
+
$promptStems = $promptFiles | ForEach-Object { $_.BaseName -replace '\.prompt$','' }
|
|
825
|
+
foreach ($p in $promptStems) {
|
|
826
|
+
if ($documentedVerbs -notcontains $p) {
|
|
827
|
+
Add-Finding D20 'README verbs coverage' 'warning' "Verb '$p' has a prompt file but no row in README ## Verbs table" "Add ``| ``$p`` | <profile> | <window> | <description> |`` to the README verbs table." $readmeFile 0
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
foreach ($v in $documentedVerbs) {
|
|
831
|
+
if ($promptStems -notcontains $v -and $v -notin @('setup','aggregate','bootstrap','refresh','state','consolidate','status','ask','propose-ado','apply-ado')) {
|
|
832
|
+
# Already-aliased verbs are fine; only flag entries with no underlying prompt + no known alias.
|
|
833
|
+
Add-Finding D20 'README verbs coverage' 'warning' "README ## Verbs row '$v' has no matching prompt file under plugin/prompts/" "Either rename the row to match an existing prompt or add plugin/prompts/$v.prompt.md." $readmeFile 0
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
} else {
|
|
837
|
+
Add-Finding D20 'README verbs coverage' 'warning' "README.md is missing a '## Verbs' section" "Add the Verbs table per the skill coverage doctrine — every plugin/prompts/<verb>.prompt.md must have a row." $readmeFile 0
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
# D21: pwsh-friendly scripts — every .ps1 in the repo must be runnable on pwsh 7 cross-platform
|
|
842
|
+
# Heuristic: flag any .ps1 that uses Windows-only APIs (Get-CimInstance Win32_*, [Environment]::SpecialFolder.Desktop, registry, com objects, Get-WmiObject)
|
|
843
|
+
$crossPlatScripts = Get-ChildItem -Path $Root -Recurse -Include '*.ps1' -ErrorAction SilentlyContinue | Where-Object { $_.FullName -notmatch '[\\/]node_modules[\\/]' }
|
|
844
|
+
$winOnlyPatterns = @(
|
|
845
|
+
@{ Pattern = 'Get-WmiObject'; Hint = 'use Get-CimInstance (cross-platform) or remove' }
|
|
846
|
+
@{ Pattern = 'Win32_'; Hint = 'WMI/CIM Win32_* classes are Windows-only — gate with if ($IsWindows)' }
|
|
847
|
+
@{ Pattern = 'New-Object -ComObject'; Hint = 'COM is Windows-only — gate with if ($IsWindows)' }
|
|
848
|
+
@{ Pattern = 'Get-ItemProperty -Path HK'; Hint = 'registry access is Windows-only — gate with if ($IsWindows)' }
|
|
849
|
+
)
|
|
850
|
+
foreach ($s in $crossPlatScripts) {
|
|
851
|
+
$body = Get-Content -Raw $s.FullName
|
|
852
|
+
foreach ($p in $winOnlyPatterns) {
|
|
853
|
+
if ($body -match [regex]::Escape($p.Pattern)) {
|
|
854
|
+
if ($body -notmatch '\$IsWindows') {
|
|
855
|
+
Add-Finding D21 'Cross-platform scripts' 'warning' "$($s.FullName -replace [regex]::Escape($Root),'') uses Windows-only API '$($p.Pattern)' without an `$IsWindows` guard" "Gate the call with ``if (`$IsWindows) { ... }`` so the script works on macOS/Linux contributors. $($p.Hint)" $s.FullName 0
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
# D22: Duplicate / overlap detection — flag instruction files whose description frontmatter overlaps too much
|
|
862
|
+
$instFilesD22 = Get-ChildItem $instructionsDir -Filter '*.instructions.md' -ErrorAction SilentlyContinue
|
|
863
|
+
$instDescs = @{}
|
|
864
|
+
foreach ($inst in $instFilesD22) {
|
|
865
|
+
$body = Get-Content -Raw $inst.FullName
|
|
866
|
+
if ($body -match '(?ms)^---.*?description:\s*"([^"]+)".*?---') {
|
|
867
|
+
$desc = $Matches[1].ToLower()
|
|
868
|
+
# Tokenize — keep word stems > 4 chars
|
|
869
|
+
$tokens = ([regex]::Matches($desc, '[a-z]{5,}') | ForEach-Object { $_.Value }) | Sort-Object -Unique
|
|
870
|
+
$instDescs[$inst.Name] = $tokens
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
$instNames = @($instDescs.Keys)
|
|
874
|
+
for ($i = 0; $i -lt $instNames.Count; $i++) {
|
|
875
|
+
for ($j = $i+1; $j -lt $instNames.Count; $j++) {
|
|
876
|
+
$a = $instNames[$i]; $b = $instNames[$j]
|
|
877
|
+
$tokA = $instDescs[$a]; $tokB = $instDescs[$b]
|
|
878
|
+
if ($tokA.Count -lt 4 -or $tokB.Count -lt 4) { continue }
|
|
879
|
+
$shared = @($tokA | Where-Object { $tokB -contains $_ }).Count
|
|
880
|
+
$minLen = [Math]::Min($tokA.Count, $tokB.Count)
|
|
881
|
+
$overlap = if ($minLen -gt 0) { $shared / $minLen } else { 0 }
|
|
882
|
+
if ($overlap -ge 0.6) {
|
|
883
|
+
Add-Finding D22 'Duplicate detection' 'warning' "Instructions $a and $b have $([math]::Round($overlap*100))% description-word overlap" "Read both — consider merging into one file, or add a clarifying scope line to each so callers can distinguish them. See docs/reference/instructions-map.md for current categorization." (Join-Path $instructionsDir $a) 0
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
# D23: instructions-map.md parity — every instruction file is listed in the map
|
|
889
|
+
$mapFile = Join-Path $Root 'docs\reference\instructions-map.md'
|
|
890
|
+
if (-not (Test-Path $mapFile)) {
|
|
891
|
+
Add-Finding D23 'Instructions map' 'warning' "docs/reference/instructions-map.md is missing" "Generate the map: lists every plugin/instructions/*.instructions.md by category with caller counts." $mapFile 0
|
|
892
|
+
} else {
|
|
893
|
+
$mapBody = Get-Content -Raw $mapFile
|
|
894
|
+
foreach ($inst in $instFilesD22) {
|
|
895
|
+
$stem = $inst.Name -replace '\.instructions\.md$',''
|
|
896
|
+
# Check for the stem mentioned anywhere in the map (as `stem` or in a table cell)
|
|
897
|
+
if ($mapBody -notmatch [regex]::Escape($stem)) {
|
|
898
|
+
Add-Finding D23 'Instructions map' 'warning' "Instruction '$stem' is not listed in docs/reference/instructions-map.md" "Regenerate the map (every plugin/instructions/*.instructions.md must appear in a category table + the Callers section)." $mapFile 0
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
# Reverse: every stem mentioned in the map exists on disk
|
|
902
|
+
$mapStems = [regex]::Matches($mapBody, '\| \x60([a-z0-9-]+)\x60 \|') | ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique
|
|
903
|
+
foreach ($s in $mapStems) {
|
|
904
|
+
$expected = Join-Path $instructionsDir "$s.instructions.md"
|
|
905
|
+
if (-not (Test-Path $expected)) {
|
|
906
|
+
Add-Finding D23 'Instructions map' 'warning' "instructions-map.md lists '$s' but plugin/instructions/$s.instructions.md does not exist" "Remove the row from instructions-map.md (and verify no caller still references the deleted instruction)." $mapFile 0
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
# D24: OneDrive pin policy applicable but own evidence appears cloud-only
|
|
912
|
+
# Only meaningful on a real machine — we look at the live config, not the repo.
|
|
913
|
+
$userCfg = Join-Path $env:USERPROFILE '.copilot\m-skills\kushi\config\user\project-evidence.yml'
|
|
914
|
+
if (Test-Path $userCfg) {
|
|
915
|
+
$cfgText = Get-Content -Raw $userCfg
|
|
916
|
+
$projectsRoot = $null
|
|
917
|
+
$myAlias = $null
|
|
918
|
+
if ($cfgText -match '(?m)^\s*projects_root:\s*"?([^"\r\n]+)"?') { $projectsRoot = $Matches[1].Trim() }
|
|
919
|
+
if ($cfgText -match '(?m)^\s*alias:\s*"?([^"\r\n]+)"?') { $myAlias = $Matches[1].Trim() }
|
|
920
|
+
if ($projectsRoot -and $myAlias -and (Test-Path $projectsRoot)) {
|
|
921
|
+
$inOD = $false
|
|
922
|
+
if ($IsWindows -or $env:OS -eq 'Windows_NT') {
|
|
923
|
+
if ($projectsRoot -like (Join-Path $env:USERPROFILE '*')) { $inOD = $true }
|
|
924
|
+
} else {
|
|
925
|
+
if ($projectsRoot -like "$env:HOME/Library/CloudStorage/*" -or $projectsRoot -like "$env:HOME/OneDrive*") { $inOD = $true }
|
|
926
|
+
}
|
|
927
|
+
if ($inOD) {
|
|
928
|
+
Get-ChildItem -Directory $projectsRoot -ErrorAction SilentlyContinue | ForEach-Object {
|
|
929
|
+
$myEv = Join-Path $_.FullName "Evidence/$myAlias"
|
|
930
|
+
if (Test-Path $myEv) {
|
|
931
|
+
if ($IsWindows -or $env:OS -eq 'Windows_NT') {
|
|
932
|
+
$attr = (& attrib $myEv 2>$null) -join ''
|
|
933
|
+
if ($attr -match '\bU\b' -and $attr -notmatch '\bP\b') {
|
|
934
|
+
Add-Finding D24 'OneDrive pin' 'warning' "Own evidence folder $myEv is cloud-only (U set, P not set) — may slow writes during refresh" "Re-run '@Kushi setup --reconfigure' to re-apply pin policy, or right-click the folder in Explorer -> 'Always keep on device'. See docs/concepts/sync-and-pinning.md." $myEv 0
|
|
935
|
+
}
|
|
936
|
+
} else {
|
|
937
|
+
$pinned = (& xattr -p com.microsoft.OneDrive.PinnedToDevice $myEv 2>$null)
|
|
938
|
+
if (-not $pinned) {
|
|
939
|
+
Add-Finding D24 'OneDrive pin' 'warning' "Own evidence folder $myEv is not pinned — may slow writes during refresh" "Re-run '@Kushi setup --reconfigure' to re-apply pin policy, or run: xattr -w com.microsoft.OneDrive.PinnedToDevice 1 '$myEv'. See docs/concepts/sync-and-pinning.md." $myEv 0
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
# D25: Documentation coverage — every instructions file is referenced from at least one user-facing doc
|
|
949
|
+
$docFiles = @()
|
|
950
|
+
$docFiles += Get-ChildItem (Join-Path $Root 'docs') -Recurse -Filter '*.md' -ErrorAction SilentlyContinue
|
|
951
|
+
$changelogFile = Join-Path $Root 'CHANGELOG.md'
|
|
952
|
+
if (Test-Path $changelogFile) { $docFiles += Get-Item $changelogFile }
|
|
953
|
+
$docCorpus = ''
|
|
954
|
+
foreach ($d in $docFiles) {
|
|
955
|
+
if ($d.FullName -eq (Join-Path $Root 'docs\reference\instructions-map.md')) { continue } # D23 already checks this
|
|
956
|
+
try { $docCorpus += [string](Get-Content -Raw $d.FullName) + "`n" } catch {}
|
|
957
|
+
}
|
|
958
|
+
foreach ($inst in $instFilesD22) {
|
|
959
|
+
$stem = $inst.Name -replace '\.instructions\.md$',''
|
|
960
|
+
if ($docCorpus -notmatch [regex]::Escape($stem)) {
|
|
961
|
+
Add-Finding D25 'Documentation coverage' 'warning' "Instruction '$stem' is not mentioned in CHANGELOG.md or any docs/concepts|reference|how-to doc" "Add a one-line CHANGELOG entry for the release that introduced it AND either (a) extend an existing doc to mention it, or (b) add a new doc under docs/concepts/ or docs/how-to/. See docs/concepts/sync-and-pinning.md for the v4.5.0 example." (Join-Path $instructionsDir $inst.Name) 0
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
# D26: Issue Recovery Rule cite — every skill that hits an external service OR consumes its evidence MUST cite issue-recovery.instructions.md.
|
|
966
|
+
$issueRecInst = Join-Path $instructionsDir 'issue-recovery.instructions.md'
|
|
967
|
+
if (-not (Test-Path $issueRecInst)) {
|
|
968
|
+
Add-Finding D26 'Issue Recovery cite' 'warning' "plugin/instructions/issue-recovery.instructions.md is missing" "Restore the Issue Recovery doctrine from kushi v4.5.1." $issueRecInst 0
|
|
969
|
+
} else {
|
|
970
|
+
$recoveryRequired = @(
|
|
971
|
+
'pull-email','pull-teams','pull-meetings','pull-onenote','pull-loop','pull-sharepoint',
|
|
972
|
+
'pull-crm','pull-ado','pull-misc',
|
|
973
|
+
'aggregate-project','consolidate-evidence',
|
|
974
|
+
'bootstrap-project','refresh-project',
|
|
975
|
+
'apply-ado-update','propose-ado-update'
|
|
976
|
+
)
|
|
977
|
+
foreach ($skillName in $recoveryRequired) {
|
|
978
|
+
$skillFile = Join-Path $skillsDir "$skillName\SKILL.md"
|
|
979
|
+
if (-not (Test-Path $skillFile)) { continue }
|
|
980
|
+
$txt = $mdText[$skillFile]
|
|
981
|
+
if (-not $txt) { try { $txt = Get-Content -Raw $skillFile } catch { $txt = '' } }
|
|
982
|
+
if ($txt -notmatch 'issue-recovery') {
|
|
983
|
+
Add-Finding D26 'Issue Recovery cite' 'warning' "Skill '$skillName' does not cite issue-recovery.instructions.md" "Add a 'References' bullet linking to ../../instructions/issue-recovery.instructions.md so the Issue Recovery Rule applies when external service failures expose doctrine gaps." $skillFile 0
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
# D27: Personal-path leakage — no contributor-specific paths committed to plugin/ or docs/getting-started/.
|
|
989
|
+
# The repo is shared across contributors and across OSes. Examples must use placeholders.
|
|
990
|
+
$personalPatterns = @(
|
|
991
|
+
@{ Pattern = 'OneDrive - Microsoft\\ISE\\Engagement Assets'; Hint = "Use placeholder '<engagement-root>' or '`$env:KUSHI_ENGAGEMENT_ROOT'." },
|
|
992
|
+
@{ Pattern = 'C:\\Users\\ushak'; Hint = "Use placeholder '<your-home>' or '`$env:USERPROFILE'." },
|
|
993
|
+
@{ Pattern = 'C:\\Usha\\ISERepos\\kushi'; Hint = "Use placeholder '<kushi-repo-root>' or relative path." },
|
|
994
|
+
@{ Pattern = 'ushak@microsoft\.com'; Hint = "Use placeholder '<your-email>' or 'you@example.com'." }
|
|
995
|
+
)
|
|
996
|
+
$leakTargets = @(
|
|
997
|
+
(Join-Path $Root 'plugin'),
|
|
998
|
+
(Join-Path $Root 'docs\getting-started'),
|
|
999
|
+
(Join-Path $Root 'README.md')
|
|
1000
|
+
)
|
|
1001
|
+
# Exempt files: CHANGELOG, learnings (history), example outputs explicitly marked as sample
|
|
1002
|
+
$leakExempt = @(
|
|
1003
|
+
'CHANGELOG.md', 'plugin\learnings', 'plugin\reference-packs',
|
|
1004
|
+
'plugin\skills\self-check'
|
|
1005
|
+
)
|
|
1006
|
+
foreach ($target in $leakTargets) {
|
|
1007
|
+
if (-not (Test-Path $target)) { continue }
|
|
1008
|
+
$leakFiles = if ((Get-Item $target).PSIsContainer) {
|
|
1009
|
+
Get-ChildItem -Path $target -Recurse -File -Include '*.md','*.ps1','*.mjs','*.json','*.yml','*.yaml' -ErrorAction SilentlyContinue
|
|
1010
|
+
} else { @(Get-Item $target) }
|
|
1011
|
+
foreach ($f in $leakFiles) {
|
|
1012
|
+
$skip = $false
|
|
1013
|
+
foreach ($ex in $leakExempt) { if ($f.FullName -like "*$ex*") { $skip = $true; break } }
|
|
1014
|
+
if ($skip) { continue }
|
|
1015
|
+
$content = $null
|
|
1016
|
+
try { $content = Get-Content -Raw $f.FullName } catch { continue }
|
|
1017
|
+
if (-not $content) { continue }
|
|
1018
|
+
foreach ($p in $personalPatterns) {
|
|
1019
|
+
if ($content -match $p.Pattern) {
|
|
1020
|
+
$rel = $f.FullName -replace [regex]::Escape($Root + '\'),''
|
|
1021
|
+
Add-Finding D27 'Personal-path leakage' 'warning' "$rel contains personal path/email pattern '$($p.Pattern)'" $p.Hint $f.FullName 0
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
# D28: Predecessor-agent leakage — kushi documentation should not reference predecessor agents by name.
|
|
1028
|
+
# The historical predecessor was an internal agent ("nova"); current docs must use neutral language so
|
|
1029
|
+
# documentation stays portable. Exemptions: CHANGELOG.md (historical commit notes), plugin/learnings (records
|
|
1030
|
+
# of past defects), plugin/reference-packs (bundled doctrine that may legitimately reference predecessors).
|
|
1031
|
+
$leakageWord = 'nova'
|
|
1032
|
+
$novaTargets = @(
|
|
1033
|
+
(Join-Path $Root 'plugin'),
|
|
1034
|
+
(Join-Path $Root 'docs')
|
|
1035
|
+
)
|
|
1036
|
+
$novaExempt = @(
|
|
1037
|
+
'CHANGELOG.md', 'plugin\learnings', 'plugin\reference-packs', 'docs\reference\instructions-map.md',
|
|
1038
|
+
'plugin\skills\self-check'
|
|
1039
|
+
)
|
|
1040
|
+
foreach ($target in $novaTargets) {
|
|
1041
|
+
if (-not (Test-Path $target)) { continue }
|
|
1042
|
+
$novaFiles = Get-ChildItem -Path $target -Recurse -File -Include '*.md','*.ps1','*.mjs','*.json','*.yml','*.yaml' -ErrorAction SilentlyContinue
|
|
1043
|
+
foreach ($f in $novaFiles) {
|
|
1044
|
+
$skip = $false
|
|
1045
|
+
foreach ($ex in $novaExempt) { if ($f.FullName -like "*$ex*") { $skip = $true; break } }
|
|
1046
|
+
if ($skip) { continue }
|
|
1047
|
+
$content = $null
|
|
1048
|
+
try { $content = Get-Content -Raw $f.FullName } catch { continue }
|
|
1049
|
+
if (-not $content) { continue }
|
|
1050
|
+
# Match standalone word 'nova' (case-insensitive), not 'innovation' / 'novanet' etc.
|
|
1051
|
+
if ($content -match '(?i)\bnova\b') {
|
|
1052
|
+
$rel = $f.FullName -replace [regex]::Escape($Root + '\'),''
|
|
1053
|
+
Add-Finding D28 'Predecessor-agent leakage' 'warning' "$rel references predecessor agent by name" "Replace with neutral language ('predecessor agent' / 'another internal agent') or remove the reference. Exemptions live in CHANGELOG.md and plugin/learnings/." $f.FullName 0
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
# D29: Vertex integration integrity — validate studios.json, verify detect/validate libs parse,
|
|
1059
|
+
# verify emit-vertex / vertex-link skill files reference only in-place vertex paths (no vendored schema folder).
|
|
1060
|
+
$studiosJson = Join-Path $Root 'plugin\config\studios.json'
|
|
1061
|
+
$studiosSchema = Join-Path $Root 'plugin\config\studios.schema.json'
|
|
1062
|
+
if (-not (Test-Path $studiosJson)) {
|
|
1063
|
+
Add-Finding D29 'Vertex integration integrity' 'error' 'plugin/config/studios.json missing' 'Restore the studio registry packaged file.' $studiosJson 0
|
|
1064
|
+
} elseif (-not (Test-Path $studiosSchema)) {
|
|
1065
|
+
Add-Finding D29 'Vertex integration integrity' 'error' 'plugin/config/studios.schema.json missing' 'Restore the studio registry schema.' $studiosSchema 0
|
|
1066
|
+
} else {
|
|
1067
|
+
try { $null = Get-Content -Raw $studiosJson | ConvertFrom-Json } catch {
|
|
1068
|
+
Add-Finding D29 'Vertex integration integrity' 'error' 'plugin/config/studios.json is not valid JSON' $_.Exception.Message $studiosJson 0
|
|
1069
|
+
}
|
|
1070
|
+
try { $null = Get-Content -Raw $studiosSchema | ConvertFrom-Json } catch {
|
|
1071
|
+
Add-Finding D29 'Vertex integration integrity' 'error' 'plugin/config/studios.schema.json is not valid JSON' $_.Exception.Message $studiosSchema 0
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
$detectLib = Join-Path $Root 'plugin\lib\detect-vertex-repo.mjs'
|
|
1076
|
+
$validateLib = Join-Path $Root 'plugin\lib\vertex-validate.mjs'
|
|
1077
|
+
foreach ($lib in @($detectLib, $validateLib)) {
|
|
1078
|
+
if (-not (Test-Path $lib)) {
|
|
1079
|
+
Add-Finding D29 'Vertex integration integrity' 'error' "$([System.IO.Path]::GetFileName($lib)) missing in plugin/lib/" 'Restore the helper module.' $lib 0
|
|
1080
|
+
} else {
|
|
1081
|
+
$node = Get-Command node -ErrorAction SilentlyContinue
|
|
1082
|
+
if ($node) {
|
|
1083
|
+
$check = & node --check $lib 2>&1
|
|
1084
|
+
if ($LASTEXITCODE -ne 0) {
|
|
1085
|
+
Add-Finding D29 'Vertex integration integrity' 'error' "$([System.IO.Path]::GetFileName($lib)) failed node --check" "$check" $lib 0
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
# Detect-don't-vendor invariant: ensure no plugin/templates/vertex-schemas/ exists.
|
|
1092
|
+
$vendoredSchemas = Join-Path $Root 'plugin\templates\vertex-schemas'
|
|
1093
|
+
if (Test-Path $vendoredSchemas) {
|
|
1094
|
+
Add-Finding D29 'Vertex integration integrity' 'error' 'plugin/templates/vertex-schemas/ exists — vertex schemas must not be vendored' 'Delete plugin/templates/vertex-schemas/. Schemas are detected in place from the configured vertex repo via vertex.json + .vertex/scripts/validation/.' $vendoredSchemas 0
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
foreach ($s in @('emit-vertex','vertex-link')) {
|
|
1098
|
+
$skillPath = Join-Path $Root "plugin\skills\$s\SKILL.md"
|
|
1099
|
+
if (-not (Test-Path $skillPath)) {
|
|
1100
|
+
Add-Finding D29 'Vertex integration integrity' 'error' "plugin/skills/$s/SKILL.md missing" 'The vertex integration skills must ship with the package.' $skillPath 0
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
681
1103
|
}
|
|
682
1104
|
|
|
683
1105
|
# === Output ===
|