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.
Files changed (95) hide show
  1. package/README.md +4 -0
  2. package/package.json +4 -4
  3. package/plugin/agents/kushi.agent.md +30 -14
  4. package/plugin/config/studios.json +37 -0
  5. package/plugin/config/studios.schema.json +45 -0
  6. package/plugin/instructions/auth-and-retry.instructions.md +268 -1
  7. package/plugin/instructions/engagement-root-resolution.instructions.md +5 -1
  8. package/plugin/instructions/evidence-thoroughness.instructions.md +103 -1
  9. package/plugin/instructions/fuzzy-disambiguation.instructions.md +97 -0
  10. package/plugin/instructions/identity-resolution.instructions.md +76 -0
  11. package/plugin/instructions/issue-recovery.instructions.md +58 -0
  12. package/plugin/instructions/kushi-config-root.instructions.md +66 -0
  13. package/plugin/instructions/loop-bootstrap-discovery.instructions.md +105 -0
  14. package/plugin/instructions/m365-id-registry.instructions.md +1 -1
  15. package/plugin/instructions/onedrive-pin-policy.instructions.md +132 -0
  16. package/plugin/instructions/per-source-verification-gate.instructions.md +193 -0
  17. package/plugin/instructions/sharepoint-to-onedrive-sync.instructions.md +116 -0
  18. package/plugin/instructions/status-color-rule.instructions.md +62 -0
  19. package/plugin/instructions/studio-registry.instructions.md +48 -0
  20. package/plugin/instructions/update-ledger.instructions.md +1 -1
  21. package/plugin/instructions/verbatim-by-default.instructions.md +1 -1
  22. package/plugin/instructions/vertex-emit.instructions.md +120 -0
  23. package/plugin/instructions/workiq-input-sanitization.instructions.md +43 -0
  24. package/plugin/instructions/workiq-onenote-query-shape.instructions.md +79 -0
  25. package/plugin/instructions/workiq-only.instructions.md +15 -9
  26. package/plugin/learnings/loop.md +11 -0
  27. package/plugin/learnings/onenote.md +27 -1
  28. package/plugin/lib/Get-KushiConfig.ps1 +22 -9
  29. package/plugin/lib/detect-vertex-repo.mjs +96 -0
  30. package/plugin/lib/render-vertex.mjs +249 -0
  31. package/plugin/lib/sanitize-workiq-input.mjs +72 -0
  32. package/plugin/lib/studio-registry.mjs +39 -0
  33. package/plugin/lib/vertex-validate.mjs +121 -0
  34. package/plugin/plugin.json +16 -6
  35. package/plugin/prompts/bootstrap.prompt.md +9 -7
  36. package/plugin/prompts/emit-vertex.prompt.md +33 -0
  37. package/plugin/prompts/setup.prompt.md +29 -0
  38. package/plugin/prompts/vertex-link.prompt.md +27 -0
  39. package/plugin/skills/aggregate-project/SKILL.md +24 -2
  40. package/plugin/skills/apply-ado-update/SKILL.md +9 -4
  41. package/plugin/skills/ask-project/SKILL.md +4 -0
  42. package/plugin/skills/bootstrap-project/SKILL.md +67 -41
  43. package/plugin/skills/consolidate-evidence/SKILL.md +5 -1
  44. package/plugin/skills/emit-vertex/README.md +37 -0
  45. package/plugin/skills/emit-vertex/SKILL.md +173 -0
  46. package/plugin/skills/intro/SKILL.md +2 -0
  47. package/plugin/skills/propose-ado-update/SKILL.md +8 -3
  48. package/plugin/skills/pull-ado/SKILL.md +11 -1
  49. package/plugin/skills/pull-crm/SKILL.md +12 -2
  50. package/plugin/skills/pull-email/SKILL.md +11 -1
  51. package/plugin/skills/pull-loop/README.md +64 -0
  52. package/plugin/skills/pull-loop/SKILL.md +180 -0
  53. package/plugin/skills/pull-loop/runner.mjs +261 -0
  54. package/plugin/skills/pull-loop/write-snapshot.mjs +181 -0
  55. package/plugin/skills/pull-meetings/SKILL.md +11 -1
  56. package/plugin/skills/pull-misc/README.md +4 -4
  57. package/plugin/skills/pull-misc/SKILL.md +18 -12
  58. package/plugin/skills/pull-onenote/SKILL.md +71 -19
  59. package/plugin/skills/pull-sharepoint/SKILL.md +11 -2
  60. package/plugin/skills/pull-teams/SKILL.md +11 -2
  61. package/plugin/skills/refresh-project/SKILL.md +38 -7
  62. package/plugin/skills/self-check/SKILL.md +14 -1
  63. package/plugin/skills/self-check/run.ps1 +442 -20
  64. package/plugin/skills/setup/SKILL.md +377 -0
  65. package/plugin/skills/vertex-link/SKILL.md +143 -0
  66. package/plugin/templates/init/m365-auth.template.json +10 -4
  67. package/plugin/templates/init/project-evidence.template.yml +10 -3
  68. package/plugin/templates/init/project-integrations.template.yml +5 -0
  69. package/plugin/templates/snapshot/ado-item.template.md +1 -1
  70. package/plugin/templates/snapshot/crm-record.template.md +1 -1
  71. package/plugin/templates/snapshot/meetings-series-index.template.md +1 -1
  72. package/plugin/templates/snapshot/onenote-page.template.md +1 -1
  73. package/plugin/templates/snapshot/sharepoint-file.template.md +1 -1
  74. package/plugin/templates/snapshot/sharepoint-tree.template.md +1 -1
  75. package/plugin/templates/snapshot/teams-roster.template.md +1 -1
  76. package/plugin/templates/weekly/ado-stream.template.md +1 -1
  77. package/plugin/templates/weekly/crm-stream.template.md +1 -1
  78. package/plugin/templates/weekly/email-stream.template.md +1 -1
  79. package/plugin/templates/weekly/meetings-stream.template.md +1 -1
  80. package/plugin/templates/weekly/onenote-stream.template.md +1 -1
  81. package/plugin/templates/weekly/sharepoint-stream.template.md +1 -1
  82. package/plugin/templates/weekly/teams-stream.template.md +1 -1
  83. package/src/check-workiq.mjs +109 -15
  84. package/src/config-loader.mjs +71 -13
  85. package/src/config-root-resolve.test.mjs +137 -0
  86. package/src/detect-vertex-repo.test.mjs +128 -0
  87. package/src/emit-vertex.e2e.test.mjs +308 -0
  88. package/src/forbidden-workiq-phrasings.test.mjs +111 -0
  89. package/src/main.mjs +11 -2
  90. package/src/sanitize-workiq-input.test.mjs +45 -0
  91. package/src/vertex-validate.test.mjs +142 -0
  92. package/plugin/instructions/az-auth-conditional.instructions.md +0 -39
  93. package/plugin/instructions/azure-auth-patterns.instructions.md +0 -233
  94. package/plugin/instructions/thoroughness-detector.instructions.md +0 -105
  95. 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-first.instructions.md
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 + evidence templates carry Validation block ===
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-detector\.instructions\.md') {
308
- Add-Finding C12 'Thoroughness enforcement' 'warning' "Skill $($d.Name) doesn't reference thoroughness-detector.instructions.md" "Add 'runtime detector + auto-retry + paste-prompt per ``thoroughness-detector.instructions.md``' to the skill intro." $f 0
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-detector.instructions.md so the skill can tick checks on write." $_.FullName 0
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-detector.instructions.md." $pluginDir 0
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-detector.instructions.md'
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-detector.instructions.md is missing" "Create the detector instructions file — pull-* skills cite it." $pluginDir 0
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
- $engagementRoots = @()
538
- $envRoot = $env:KUSHI_ENGAGEMENT_ROOT
539
- if ($envRoot -and (Test-Path $envRoot)) { $engagementRoots += $envRoot }
540
- $defaultEngRoot = Join-Path $env:USERPROFILE 'OneDrive - Microsoft\ISE\Engagement Assets'
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
- $layoutEngRoots = @()
631
- if ($env:KUSHI_ENGAGEMENT_ROOT -and (Test-Path $env:KUSHI_ENGAGEMENT_ROOT)) { $layoutEngRoots += $env:KUSHI_ENGAGEMENT_ROOT }
632
- $layoutDefaultRoot = Join-Path $env:USERPROFILE 'OneDrive - Microsoft\ISE\Engagement Assets'
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 ===