kushi-agents 3.4.1

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 (111) hide show
  1. package/.github/config/m365-auth.json.example +56 -0
  2. package/.github/config/m365-mutable.json.example +11 -0
  3. package/LICENSE +201 -0
  4. package/README.md +159 -0
  5. package/bin/cli.mjs +75 -0
  6. package/package.json +35 -0
  7. package/plugin/agents/kushi.agent.md +147 -0
  8. package/plugin/instructions/answer-from-evidence.instructions.md +73 -0
  9. package/plugin/instructions/auth-and-retry.instructions.md +116 -0
  10. package/plugin/instructions/az-auth-conditional.instructions.md +39 -0
  11. package/plugin/instructions/azure-auth-patterns.instructions.md +226 -0
  12. package/plugin/instructions/citation-ledger.instructions.md +52 -0
  13. package/plugin/instructions/engagement-root-resolution.instructions.md +82 -0
  14. package/plugin/instructions/evidence-thoroughness.instructions.md +62 -0
  15. package/plugin/instructions/side-by-side-config.instructions.md +56 -0
  16. package/plugin/instructions/snapshot-vs-stream.instructions.md +87 -0
  17. package/plugin/instructions/thoroughness-detector.instructions.md +105 -0
  18. package/plugin/instructions/workiq-first.instructions.md +47 -0
  19. package/plugin/plugin.json +96 -0
  20. package/plugin/prompts/aggregate.prompt.md +24 -0
  21. package/plugin/prompts/ask.prompt.md +16 -0
  22. package/plugin/prompts/bootstrap.prompt.md +23 -0
  23. package/plugin/prompts/consolidate.prompt.md +21 -0
  24. package/plugin/prompts/fde-intake.prompt.md +41 -0
  25. package/plugin/prompts/fde-report.prompt.md +46 -0
  26. package/plugin/prompts/fde-triage.prompt.md +46 -0
  27. package/plugin/prompts/refresh.prompt.md +17 -0
  28. package/plugin/prompts/state.prompt.md +17 -0
  29. package/plugin/prompts/status.prompt.md +17 -0
  30. package/plugin/reference-packs/README.md +74 -0
  31. package/plugin/reference-packs/fde/README.md +62 -0
  32. package/plugin/reference-packs/fde/core-fde-reference.md +427 -0
  33. package/plugin/reference-packs/fde/intake-questions.md +168 -0
  34. package/plugin/reference-packs/fde/report-doctrine.md +189 -0
  35. package/plugin/skills/aggregate-project/SKILL.md +72 -0
  36. package/plugin/skills/ask-project/SKILL.md +162 -0
  37. package/plugin/skills/bootstrap-project/SKILL.md +129 -0
  38. package/plugin/skills/build-state/SKILL.md +69 -0
  39. package/plugin/skills/consolidate-evidence/SKILL.md +47 -0
  40. package/plugin/skills/fde-intake/SKILL.md +147 -0
  41. package/plugin/skills/fde-report/SKILL.md +159 -0
  42. package/plugin/skills/fde-triage/SKILL.md +114 -0
  43. package/plugin/skills/intro/SKILL.md +449 -0
  44. package/plugin/skills/project-status/SKILL.md +61 -0
  45. package/plugin/skills/pull-ado/SKILL.md +77 -0
  46. package/plugin/skills/pull-crm/SKILL.md +75 -0
  47. package/plugin/skills/pull-email/SKILL.md +75 -0
  48. package/plugin/skills/pull-meetings/SKILL.md +77 -0
  49. package/plugin/skills/pull-onenote/SKILL.md +82 -0
  50. package/plugin/skills/pull-sharepoint/SKILL.md +85 -0
  51. package/plugin/skills/pull-teams/SKILL.md +75 -0
  52. package/plugin/skills/refresh-project/SKILL.md +89 -0
  53. package/plugin/skills/self-check/SKILL.md +166 -0
  54. package/plugin/skills/self-check/run.ps1 +517 -0
  55. package/plugin/skills/self-check/run.sh +33 -0
  56. package/plugin/templates/fde/intake.md +114 -0
  57. package/plugin/templates/fde/report-fitness.md +151 -0
  58. package/plugin/templates/fde/report-long.md +109 -0
  59. package/plugin/templates/fde/report-short.md +45 -0
  60. package/plugin/templates/fde/report-stage-readiness.md +70 -0
  61. package/plugin/templates/fde/report-weekly.md +73 -0
  62. package/plugin/templates/fde/triage-00-fde-analysis.md +78 -0
  63. package/plugin/templates/fde/triage-02-risk-analysis.md +76 -0
  64. package/plugin/templates/fde/triage-03-6Q.md +40 -0
  65. package/plugin/templates/fde/triage-04-readiness-checklist.md +82 -0
  66. package/plugin/templates/fde/triage-05-executive-consolidated.md +78 -0
  67. package/plugin/templates/fde/triage-06-global-opportunity.md +70 -0
  68. package/plugin/templates/fde/triage-07-validation-warnings.md +60 -0
  69. package/plugin/templates/init/ado-config.template.yml +21 -0
  70. package/plugin/templates/init/crm-config.template.yml +16 -0
  71. package/plugin/templates/init/kushi-projects.template.json +14 -0
  72. package/plugin/templates/init/m365-auth.template.json +67 -0
  73. package/plugin/templates/init/m365-mutable.template.json +19 -0
  74. package/plugin/templates/init/project-contributors.template.yml +27 -0
  75. package/plugin/templates/init/project-evidence.template.yml +32 -0
  76. package/plugin/templates/init/project-integrations.template.yml +34 -0
  77. package/plugin/templates/init/project-user-settings.template.yml +71 -0
  78. package/plugin/templates/paste-prompt.md +35 -0
  79. package/plugin/templates/snapshot/ado-item.template.md +45 -0
  80. package/plugin/templates/snapshot/crm-record.template.md +34 -0
  81. package/plugin/templates/snapshot/meetings-series-index.template.md +32 -0
  82. package/plugin/templates/snapshot/onenote-page.template.md +28 -0
  83. package/plugin/templates/snapshot/sharepoint-file.template.md +31 -0
  84. package/plugin/templates/snapshot/sharepoint-tree.template.md +39 -0
  85. package/plugin/templates/snapshot/teams-roster.template.md +27 -0
  86. package/plugin/templates/state/00_overview.template.md +44 -0
  87. package/plugin/templates/state/01_decisions.template.md +41 -0
  88. package/plugin/templates/state/02_stakeholders.template.md +48 -0
  89. package/plugin/templates/state/03_architecture-and-solution.template.md +56 -0
  90. package/plugin/templates/state/04_workshops-and-key-meetings.template.md +43 -0
  91. package/plugin/templates/state/05_action-items.template.md +29 -0
  92. package/plugin/templates/state/06_risks-and-issues.template.md +43 -0
  93. package/plugin/templates/state/07_timeline-and-milestones.template.md +45 -0
  94. package/plugin/templates/state/08_artifacts-and-deliverables.template.md +55 -0
  95. package/plugin/templates/state/09_open-questions.template.md +62 -0
  96. package/plugin/templates/state/README.md +41 -0
  97. package/plugin/templates/weekly/ado-stream.template.md +71 -0
  98. package/plugin/templates/weekly/consolidated.template.md +98 -0
  99. package/plugin/templates/weekly/crm-stream.template.md +74 -0
  100. package/plugin/templates/weekly/email-stream.template.md +103 -0
  101. package/plugin/templates/weekly/meetings-stream.template.md +182 -0
  102. package/plugin/templates/weekly/onenote-stream.template.md +106 -0
  103. package/plugin/templates/weekly/run-log.template.md +88 -0
  104. package/plugin/templates/weekly/sharepoint-stream.template.md +121 -0
  105. package/plugin/templates/weekly/teams-stream.template.md +121 -0
  106. package/src/constants.mjs +49 -0
  107. package/src/copy-assets.mjs +183 -0
  108. package/src/main.mjs +262 -0
  109. package/src/profile-resolver.mjs +168 -0
  110. package/src/prompt.mjs +42 -0
  111. package/src/settings.mjs +77 -0
@@ -0,0 +1,517 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Kushi self-check — validates docs/skill consistency against the file system.
4
+
5
+ .DESCRIPTION
6
+ Source of truth is plugin/skills/. Reports gaps as warnings with concrete fixes.
7
+ Returns exit code 0 always (warnings don't fail the run); use -StrictExit to flip
8
+ that behaviour for CI.
9
+
10
+ .PARAMETER Root
11
+ Repo root (default: two levels above this script — i.e. kushi repo root).
12
+
13
+ .PARAMETER Deep
14
+ Also run D-checks (template references, snapshot/stream, WorkIQ hygiene, live install sync).
15
+
16
+ .PARAMETER Json
17
+ Emit findings as JSON (for CI / agent consumption) instead of human prose.
18
+
19
+ .PARAMETER StrictExit
20
+ Exit with code 1 if any findings are produced.
21
+
22
+ .EXAMPLE
23
+ pwsh plugin/skills/self-check/run.ps1
24
+ pwsh plugin/skills/self-check/run.ps1 -Deep
25
+ pwsh plugin/skills/self-check/run.ps1 -Deep -Json | ConvertFrom-Json
26
+ #>
27
+ [CmdletBinding()]
28
+ param(
29
+ [string]$Root = (Resolve-Path (Join-Path $PSScriptRoot "..\..\..")).Path,
30
+ [switch]$Deep,
31
+ [switch]$Json,
32
+ [switch]$StrictExit
33
+ )
34
+
35
+ $ErrorActionPreference = "Stop"
36
+ $findings = New-Object System.Collections.Generic.List[object]
37
+
38
+ function Add-Finding {
39
+ param($Code, $Surface, $Severity, $Message, $Fix, $File, $Line)
40
+ $findings.Add([PSCustomObject]@{
41
+ code = $Code; surface = $Surface; severity = $Severity
42
+ message = $Message; fix = $Fix; file = $File; line = $Line
43
+ })
44
+ }
45
+
46
+ function Get-Frontmatter {
47
+ param([string]$Path)
48
+ $text = Get-Content -Raw -Path $Path
49
+ if ($text -notmatch '^(?s)---\r?\n(.*?)\r?\n---') { return @{} }
50
+ $yaml = $Matches[1]
51
+ $fm = @{}
52
+ foreach ($line in $yaml -split "`n") {
53
+ if ($line -match '^\s*([a-zA-Z0-9_-]+)\s*:\s*"?([^"]*?)"?\s*$') {
54
+ $fm[$Matches[1]] = $Matches[2]
55
+ }
56
+ }
57
+ return $fm
58
+ }
59
+
60
+ # === Discovery (source of truth) ===
61
+ $pluginDir = Join-Path $Root 'plugin'
62
+ $skillsDir = Join-Path $pluginDir 'skills'
63
+ $instructionsDir = Join-Path $pluginDir 'instructions'
64
+ $promptsDir = Join-Path $pluginDir 'prompts'
65
+ $agentFile = Join-Path $pluginDir 'agents\kushi.agent.md'
66
+ $readmeFile = Join-Path $Root 'README.md'
67
+ $wtlFile = Join-Path $Root 'docs\reference\where-things-live.md'
68
+
69
+ if (-not (Test-Path $pluginDir)) {
70
+ Write-Error "plugin/ not found at $pluginDir — run from kushi repo root or pass -Root."
71
+ exit 2
72
+ }
73
+
74
+ $skillDirs = Get-ChildItem $skillsDir -Directory | Sort-Object Name
75
+ $skillNames = $skillDirs.Name
76
+ $instructionFiles = Get-ChildItem $instructionsDir -Filter '*.instructions.md' -EA SilentlyContinue
77
+ $promptFiles = Get-ChildItem $promptsDir -Filter '*.prompt.md' -EA SilentlyContinue
78
+
79
+ # Pre-load text of all plugin md files for cross-reference scans
80
+ $allMd = Get-ChildItem $pluginDir -Recurse -Filter '*.md'
81
+ $mdText = @{}
82
+ foreach ($f in $allMd) { $mdText[$f.FullName] = Get-Content -Raw -Path $f.FullName }
83
+
84
+ # === C1, C2: frontmatter + name match ===
85
+ foreach ($d in $skillDirs) {
86
+ $skillFile = Join-Path $d.FullName 'SKILL.md'
87
+ if (-not (Test-Path $skillFile)) {
88
+ Add-Finding C1 'Frontmatter' 'error' "Skill `"$($d.Name)`" has no SKILL.md" "Create plugin/skills/$($d.Name)/SKILL.md with name/version/description frontmatter." $d.FullName 0
89
+ continue
90
+ }
91
+ $fm = Get-Frontmatter $skillFile
92
+ foreach ($key in @('name','version','description')) {
93
+ if (-not $fm[$key]) {
94
+ Add-Finding C1 'Frontmatter' 'warning' "Skill `"$($d.Name)`" missing frontmatter key '$key'" "Add `"$key`": `"...`" between the leading --- separators in $skillFile." $skillFile 1
95
+ }
96
+ }
97
+ if ($fm['name'] -and ($fm['name'].ToLower() -ne $d.Name.ToLower())) {
98
+ Add-Finding C2 'Frontmatter' 'warning' "Directory `"$($d.Name)/`" declares name `"$($fm['name'])`"" "Rename one of them so they match." $skillFile 1
99
+ }
100
+ }
101
+
102
+ # === C3: every skill referenced in agent ===
103
+ if (Test-Path $agentFile) {
104
+ $agentText = $mdText[$agentFile]
105
+ foreach ($name in $skillNames) {
106
+ $patternBacktick = "``$name``"
107
+ if ($agentText -notmatch [regex]::Escape($name)) {
108
+ Add-Finding C3 'Agent inventory' 'warning' "Skill `"$name`" not mentioned in kushi.agent.md" "Add a row to the routing table: ``| <verb> | $name | <one-line description> |``" $agentFile 0
109
+ }
110
+ }
111
+ } else {
112
+ Add-Finding C3 'Agent inventory' 'error' "Agent file missing: $agentFile" "Create plugin/agents/kushi.agent.md as the orchestrator." $agentFile 0
113
+ }
114
+
115
+ # === C4: prompts route to real skills ===
116
+ foreach ($p in $promptFiles) {
117
+ $text = $mdText[$p.FullName]
118
+ # Find phrases like "delegates to `name`" or "Delegates to `name` skill"
119
+ $matches2 = [regex]::Matches($text, "(?i)delegates? to ``([a-z0-9-]+)``")
120
+ foreach ($m in $matches2) {
121
+ $referenced = $m.Groups[1].Value
122
+ if ($skillNames -notcontains $referenced) {
123
+ Add-Finding C4 'Prompts' 'warning' "Prompt $($p.Name) references skill `"$referenced`" which doesn't exist" "Either create plugin/skills/$referenced/ or update the prompt to point at an existing skill." $p.FullName 0
124
+ }
125
+ }
126
+ }
127
+
128
+ # === C5: instructions referenced somewhere ===
129
+ foreach ($inst in $instructionFiles) {
130
+ $needle = $inst.Name # e.g. workiq-first.instructions.md
131
+ $referenced = $false
132
+ foreach ($key in $mdText.Keys) {
133
+ if ($key -eq $inst.FullName) { continue }
134
+ if ($mdText[$key] -match [regex]::Escape($needle)) { $referenced = $true; break }
135
+ }
136
+ if (-not $referenced) {
137
+ Add-Finding C5 'Instructions' 'warning' "Instruction `"$needle`" is orphan (not referenced from any SKILL.md or agent.md)" "Either reference it in a relevant skill (e.g. add a `## References` bullet) or delete it." $inst.FullName 0
138
+ }
139
+ }
140
+
141
+ # === C6: cross-links resolve ===
142
+ foreach ($f in $allMd) {
143
+ $text = $mdText[$f.FullName]
144
+ # Strip fenced code blocks first (```...```), then inline code spans (`...`).
145
+ # This keeps regex examples from being treated as real links.
146
+ $stripped = [regex]::Replace($text, '(?s)```.*?```', { param($m) ("`n" * ($m.Value -split "`n").Count) })
147
+ $stripped = [regex]::Replace($stripped, '`[^`\n]+`', '')
148
+ $linkRx = [regex]'\]\(([^)]+)\)'
149
+ $i = 0
150
+ foreach ($line in ($stripped -split "`n")) {
151
+ $i++
152
+ foreach ($m in $linkRx.Matches($line)) {
153
+ $tgt = $m.Groups[1].Value.Trim()
154
+ # Skip absolute URLs, anchors, mailto, angle-bracket placeholders, and template variables.
155
+ if ($tgt -match '^(https?:|mailto:|#|<)' -or $tgt -match '^[a-zA-Z]+:') { continue }
156
+ if ($tgt -match '^\{\{.+\}\}$') { continue } # {{URL}} template placeholders
157
+ if ($tgt -match '\.\.\.') { continue } # path/... example syntax
158
+ # Strip query / fragment.
159
+ $tgt = ($tgt -split '[#?]')[0].Trim()
160
+ if (-not $tgt) { continue }
161
+ $resolved = Join-Path (Split-Path $f.FullName) $tgt
162
+ try { $resolved = [System.IO.Path]::GetFullPath($resolved) } catch { continue }
163
+ if (-not (Test-Path $resolved)) {
164
+ Add-Finding C6 'Cross-links' 'warning' "$($f.Name):$i links to `"$tgt`" which doesn't resolve" "Update or remove the link." $f.FullName $i
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ # === C7: verbs table matches prompts ===
171
+ if (Test-Path $readmeFile) {
172
+ $readme = Get-Content -Raw $readmeFile
173
+ foreach ($p in $promptFiles) {
174
+ $verb = $p.BaseName -replace '\.prompt$',''
175
+ if ($readme -notmatch "``$verb``") {
176
+ Add-Finding C7 'Verbs (README)' 'warning' "Verb `"$verb`" has a prompt file but no row in README" "Add to the verbs table in README.md: ``| ``$verb`` | <window> | <description> |``" $readmeFile 0
177
+ }
178
+ }
179
+ } else {
180
+ Add-Finding C7 'Verbs (README)' 'warning' "README.md not found at $readmeFile" "Create README.md and document the verbs surface." $readmeFile 0
181
+ }
182
+
183
+ # === C8: docs/reference/where-things-live.md references all top-level plugin items ===
184
+ if (Test-Path $wtlFile) {
185
+ $wtl = Get-Content -Raw $wtlFile
186
+ foreach ($child in (Get-ChildItem $pluginDir)) {
187
+ if ($wtl -notmatch [regex]::Escape($child.Name)) {
188
+ Add-Finding C8 'Layout (WTL)' 'warning' "plugin/$($child.Name) is missing from docs/reference/where-things-live.md tree" "Add it under '## 1. This repo'." $wtlFile 0
189
+ }
190
+ }
191
+ }
192
+
193
+ # === C9: plugin.json profiles block is well-formed and references real assets ===
194
+ $pluginJsonFile = Join-Path $pluginDir 'plugin.json'
195
+ if (Test-Path $pluginJsonFile) {
196
+ try {
197
+ $pj = Get-Content -Raw $pluginJsonFile | ConvertFrom-Json
198
+ if (-not $pj.profiles) {
199
+ Add-Finding C9 'Profiles' 'error' 'plugin.json has no profiles block' "Add a 'profiles' block with at least core/standard/full." $pluginJsonFile 0
200
+ } else {
201
+ $profileNames = @($pj.profiles.PSObject.Properties.Name)
202
+ $defaultProfile = $pj.default_profile
203
+ if (-not $defaultProfile) {
204
+ Add-Finding C9 'Profiles' 'warning' "plugin.json missing 'default_profile'" "Set 'default_profile' to one of: $($profileNames -join ', ')." $pluginJsonFile 0
205
+ } elseif ($profileNames -notcontains $defaultProfile) {
206
+ Add-Finding C9 'Profiles' 'error' "default_profile '$defaultProfile' is not declared in profiles{}" "Use one of: $($profileNames -join ', ')." $pluginJsonFile 0
207
+ }
208
+ # Resolve each profile chain and verify every referenced skill/prompt/instruction exists
209
+ function Resolve-ProfileChain($name, $seen) {
210
+ if ($seen -contains $name) { return @() }
211
+ $p = $pj.profiles.$name
212
+ if (-not $p) { return @() }
213
+ $chain = @()
214
+ if ($p.extends) { $chain += Resolve-ProfileChain $p.extends ($seen + $name) }
215
+ $chain += $name
216
+ return $chain
217
+ }
218
+ $skillNames = (Get-ChildItem $skillsDir -Directory).Name
219
+ $promptStems = (Get-ChildItem (Join-Path $pluginDir 'prompts') -File -Filter '*.prompt.md' -ErrorAction SilentlyContinue) `
220
+ | ForEach-Object { $_.Name -replace '\.prompt\.md$','' }
221
+ $instructionStems = (Get-ChildItem (Join-Path $pluginDir 'instructions') -File -Filter '*.instructions.md' -ErrorAction SilentlyContinue) `
222
+ | ForEach-Object { $_.Name -replace '\.instructions\.md$','' }
223
+ foreach ($pn in $profileNames) {
224
+ $chain = Resolve-ProfileChain $pn @()
225
+ $skills = New-Object System.Collections.Generic.HashSet[string]
226
+ $prompts = New-Object System.Collections.Generic.HashSet[string]
227
+ $instr = $null
228
+ foreach ($link in $chain) {
229
+ $node = $pj.profiles.$link
230
+ if ($node.skills) { foreach ($s in $node.skills) { $null = $skills.Add($s) } }
231
+ if ($node.prompts) { foreach ($s in $node.prompts) { $null = $prompts.Add($s) } }
232
+ if ($node.instructions) { $instr = $node.instructions }
233
+ }
234
+ foreach ($s in $skills) {
235
+ if ($skillNames -notcontains $s) {
236
+ Add-Finding C9 'Profiles' 'warning' "Profile '$pn' references skill '$s' which doesn't exist under plugin/skills/" "Create plugin/skills/$s/ or remove it from the profile." $pluginJsonFile 0
237
+ }
238
+ }
239
+ foreach ($s in $prompts) {
240
+ if ($promptStems -notcontains $s) {
241
+ Add-Finding C9 'Profiles' 'warning' "Profile '$pn' references prompt '$s.prompt.md' which doesn't exist" "Create plugin/prompts/$s.prompt.md or remove it from the profile." $pluginJsonFile 0
242
+ }
243
+ }
244
+ if ($instr -and $instr -ne '*') {
245
+ foreach ($s in $instr) {
246
+ if ($instructionStems -notcontains $s) {
247
+ Add-Finding C9 'Profiles' 'warning' "Profile '$pn' references instruction '$s.instructions.md' which doesn't exist" "Create plugin/instructions/$s.instructions.md or remove it from the profile." $pluginJsonFile 0
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }
253
+
254
+ # === C10: reference packs referenced by any profile must exist and have a README.md ===
255
+ $refPacksDir = Join-Path $pluginDir 'reference-packs'
256
+ $profileNames = @($pj.profiles.PSObject.Properties.Name)
257
+ foreach ($pn in $profileNames) {
258
+ $packs = $pj.profiles.$pn.reference_packs
259
+ if ($null -eq $packs) { continue }
260
+ foreach ($pack in $packs) {
261
+ $packDir = Join-Path $refPacksDir $pack
262
+ if (-not (Test-Path $packDir)) {
263
+ Add-Finding C10 'Reference packs' 'warning' "Profile '$pn' references reference pack '$pack' which doesn't exist" "Create plugin/reference-packs/$pack/ with a README.md, or remove the pack from the profile." $pluginJsonFile 0
264
+ continue
265
+ }
266
+ $readme = Join-Path $packDir 'README.md'
267
+ if (-not (Test-Path $readme)) {
268
+ Add-Finding C10 'Reference packs' 'warning' "Reference pack '$pack' is missing a README.md" "Create plugin/reference-packs/$pack/README.md documenting pack scope, consumers, read order, and conflict-resolution." $packDir 0
269
+ }
270
+ }
271
+ }
272
+
273
+ # Also: every folder UNDER reference-packs/ should have a README.md, even if not yet wired into a profile.
274
+ if (Test-Path $refPacksDir) {
275
+ $packDirs = Get-ChildItem -Path $refPacksDir -Directory -ErrorAction SilentlyContinue
276
+ foreach ($pd in $packDirs) {
277
+ $readme = Join-Path $pd.FullName 'README.md'
278
+ if (-not (Test-Path $readme)) {
279
+ Add-Finding C10 'Reference packs' 'warning' "Reference pack folder '$($pd.Name)' has no README.md" "Create plugin/reference-packs/$($pd.Name)/README.md documenting pack scope, consumers, read order, and conflict-resolution." $pd.FullName 0
280
+ }
281
+ }
282
+ }
283
+ } catch {
284
+ Add-Finding C9 'Profiles' 'error' "Failed to parse plugin.json: $_" "Validate JSON syntax." $pluginJsonFile 0
285
+ }
286
+ } else {
287
+ Add-Finding C9 'Profiles' 'error' "plugin.json not found at $pluginJsonFile" "Create plugin/plugin.json with a 'profiles' block." $pluginJsonFile 0
288
+ }
289
+
290
+ # === C11: FDE skills must cite at least one reference-pack file in SKILL.md ===
291
+ $fdeSkills = @('fde-intake','fde-report','fde-triage')
292
+ foreach ($name in $fdeSkills) {
293
+ $f = Join-Path $skillsDir "$name\SKILL.md"
294
+ if (Test-Path $f) {
295
+ $text = Get-Content -Raw $f
296
+ if ($text -notmatch 'reference-packs/fde/[a-zA-Z0-9_.-]+\.md') {
297
+ Add-Finding C11 'FDE skills' 'warning' "Skill $name doesn't cite any reference-packs/fde/*.md file" "FDE skills must ground in the FDE reference pack — cite report-doctrine.md / intake-questions.md / core-fde-reference.md as appropriate." $f 0
298
+ }
299
+ }
300
+ }
301
+
302
+ # === C12: pull-* skills must reference thoroughness-detector + evidence templates carry Validation block ===
303
+ foreach ($d in $skillDirs | Where-Object { $_.Name -like 'pull-*' }) {
304
+ $f = Join-Path $d.FullName 'SKILL.md'
305
+ $text = $mdText[$f]
306
+ 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
309
+ }
310
+ }
311
+ $evidenceTemplateDirs = @('templates\weekly','templates\snapshot')
312
+ $nonEvidenceTemplates = @('consolidated.template.md','run-log.template.md')
313
+ foreach ($td in $evidenceTemplateDirs) {
314
+ $dir = Join-Path $pluginDir $td
315
+ if (-not (Test-Path $dir)) { continue }
316
+ Get-ChildItem $dir -Filter '*.template.md' -ErrorAction SilentlyContinue | ForEach-Object {
317
+ if ($nonEvidenceTemplates -contains $_.Name) { return }
318
+ $tx = Get-Content -Raw $_.FullName
319
+ 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
321
+ }
322
+ }
323
+ }
324
+ $pasteTpl = Join-Path $pluginDir 'templates\paste-prompt.md'
325
+ 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
327
+ }
328
+ $detectorFile = Join-Path $pluginDir 'instructions\thoroughness-detector.instructions.md'
329
+ 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
331
+ }
332
+
333
+ # === Deep checks ===
334
+ if ($Deep) {
335
+ # D1: templates referenced from skills exist
336
+ $templatesDir = Join-Path $pluginDir 'templates'
337
+ foreach ($d in $skillDirs) {
338
+ $f = Join-Path $d.FullName 'SKILL.md'
339
+ $text = $mdText[$f]
340
+ if (-not $text) { continue }
341
+ foreach ($m in [regex]::Matches($text, '``templates/([a-zA-Z0-9_./-]+\.template\.[a-z]+)``')) {
342
+ $rel = $m.Groups[1].Value
343
+ $abs = Join-Path $templatesDir $rel
344
+ if (-not (Test-Path $abs)) {
345
+ Add-Finding D1 'Templates' 'warning' "Skill $($d.Name) references templates/$rel which doesn't exist" "Create the template or update the path." $f 0
346
+ }
347
+ }
348
+ }
349
+ # D2: pull-* skills must reference snapshot-vs-stream
350
+ foreach ($d in $skillDirs | Where-Object { $_.Name -like 'pull-*' }) {
351
+ $f = Join-Path $d.FullName 'SKILL.md'
352
+ $text = $mdText[$f]
353
+ if ($text -notmatch 'snapshot-vs-stream\.instructions\.md') {
354
+ Add-Finding D2 'Snapshot/Stream' 'warning' "Skill $($d.Name) doesn't cite snapshot-vs-stream.instructions.md" "Add a reference (e.g. 'per snapshot-vs-stream.instructions.md') in the skill body." $f 0
355
+ }
356
+ }
357
+ # D3: pull-* must put WorkIQ first
358
+ foreach ($d in $skillDirs | Where-Object { $_.Name -like 'pull-*' }) {
359
+ $f = Join-Path $d.FullName 'SKILL.md'
360
+ $text = $mdText[$f]
361
+ if ($text -notmatch '(?ms)1\.\s*\*\*WorkIQ\*\*') {
362
+ Add-Finding D3 'WorkIQ-first' 'warning' "Skill $($d.Name) doesn't list **WorkIQ** as tool #1" "Restructure the Tools section so WorkIQ is item 1." $f 0
363
+ }
364
+ }
365
+ # D4: live install SKILL.md = agent file
366
+ $liveSkill = Join-Path $env:USERPROFILE '.copilot\m-skills\kushi\SKILL.md'
367
+ if (Test-Path $liveSkill) {
368
+ $a = Get-FileHash $liveSkill -Algorithm SHA256
369
+ $b = Get-FileHash $agentFile -Algorithm SHA256
370
+ if ($a.Hash -ne $b.Hash) {
371
+ Add-Finding D4 'Live install sync' 'warning' "Live SKILL.md hash differs from plugin/agents/kushi.agent.md" "Re-run installer: ``node bin/cli.mjs --clawpilot --force``" $liveSkill 0
372
+ }
373
+ }
374
+ # D5: skills-metadata description matches frontmatter
375
+ $metaPath = Join-Path $env:USERPROFILE '.copilot\m-skills\skills-metadata.json'
376
+ if (Test-Path $metaPath) {
377
+ try {
378
+ $meta = Get-Content -Raw $metaPath | ConvertFrom-Json
379
+ $kushiMeta = $meta | Where-Object { $_.name -eq 'kushi' } | Select-Object -First 1
380
+ if ($kushiMeta) {
381
+ $fmAgent = Get-Frontmatter $agentFile
382
+ if ($fmAgent['description'] -and $kushiMeta.description -ne $fmAgent['description']) {
383
+ Add-Finding D5 'Live install sync' 'warning' "skills-metadata.json kushi description drifted from agent frontmatter" "Re-run installer or sync the description manually." $metaPath 0
384
+ }
385
+ }
386
+ } catch {
387
+ Add-Finding D5 'Live install sync' 'warning' "Failed to parse skills-metadata.json: $_" "Validate JSON syntax." $metaPath 0
388
+ }
389
+ }
390
+ # D6: side-by-side rule cited where user config is touched
391
+ $configTouchers = @('bootstrap-project','refresh-project') # heuristic
392
+ foreach ($name in $configTouchers) {
393
+ $f = Join-Path $skillsDir "$name\SKILL.md"
394
+ if (Test-Path $f) {
395
+ $text = $mdText[$f]
396
+ if ($text -notmatch 'side-by-side-config\.instructions\.md') {
397
+ Add-Finding D6 'Instructions' 'warning' "Skill $name doesn't cite side-by-side-config.instructions.md" "Add a reference; this skill writes user-visible config files." $f 0
398
+ }
399
+ }
400
+ }
401
+ # D7: live install kushi-install.json matches plugin.json profile spec
402
+ $liveManifest = Join-Path $env:USERPROFILE '.copilot\m-skills\kushi\kushi-install.json'
403
+ if (Test-Path $liveManifest) {
404
+ try {
405
+ $live = Get-Content -Raw $liveManifest | ConvertFrom-Json
406
+ $pluginJsonFile2 = Join-Path $pluginDir 'plugin.json'
407
+ $pj2 = Get-Content -Raw $pluginJsonFile2 | ConvertFrom-Json
408
+ $liveProfile = $live.profile
409
+ if ($pj2.profiles.$liveProfile) {
410
+ # Re-resolve chain
411
+ function Resolve-ChainD7($name, $seen) {
412
+ if ($seen -contains $name) { return @() }
413
+ $p = $pj2.profiles.$name
414
+ if (-not $p) { return @() }
415
+ $chain = @()
416
+ if ($p.extends) { $chain += Resolve-ChainD7 $p.extends ($seen + $name) }
417
+ $chain += $name
418
+ return $chain
419
+ }
420
+ $chain = Resolve-ChainD7 $liveProfile @()
421
+ $expectedSkills = New-Object System.Collections.Generic.HashSet[string]
422
+ foreach ($link in $chain) {
423
+ $node = $pj2.profiles.$link
424
+ if ($node.skills) { foreach ($s in $node.skills) { $null = $expectedSkills.Add($s) } }
425
+ }
426
+ $missing = @($expectedSkills | Where-Object { $live.skills -notcontains $_ })
427
+ $extra = @($live.skills | Where-Object { -not $expectedSkills.Contains($_) })
428
+ if ($missing.Count -gt 0 -or $extra.Count -gt 0) {
429
+ Add-Finding D7 'Live install manifest' 'warning' "Live kushi-install.json (profile '$liveProfile') drifted from plugin.json — missing: [$($missing -join ', ')]; extra: [$($extra -join ', ')]" "Re-run installer: ``node bin/cli.mjs --clawpilot --profile $liveProfile --force``" $liveManifest 0
430
+ }
431
+
432
+ # Reference packs drift check
433
+ $expectedPacks = New-Object System.Collections.Generic.HashSet[string]
434
+ foreach ($link in $chain) {
435
+ $node = $pj2.profiles.$link
436
+ if ($node.reference_packs) { foreach ($s in $node.reference_packs) { $null = $expectedPacks.Add($s) } }
437
+ }
438
+ $livePacks = @()
439
+ if ($live.PSObject.Properties.Name -contains 'reference_packs') { $livePacks = @($live.reference_packs) }
440
+ $packMissing = @($expectedPacks | Where-Object { $livePacks -notcontains $_ })
441
+ $packExtra = @($livePacks | Where-Object { -not $expectedPacks.Contains($_) })
442
+ if ($packMissing.Count -gt 0 -or $packExtra.Count -gt 0) {
443
+ Add-Finding D7 'Live install manifest' 'warning' "Live kushi-install.json reference_packs (profile '$liveProfile') drifted from plugin.json — missing: [$($packMissing -join ', ')]; extra: [$($packExtra -join ', ')]" "Re-run installer: ``node bin/cli.mjs --clawpilot --profile $liveProfile --force``" $liveManifest 0
444
+ }
445
+ } else {
446
+ Add-Finding D7 'Live install manifest' 'warning' "Live install profile '$liveProfile' not declared in plugin.json" "Re-install with a known profile (core/standard/full)." $liveManifest 0
447
+ }
448
+ } catch {
449
+ Add-Finding D7 'Live install manifest' 'warning' "Failed to parse live kushi-install.json: $_" "Re-run installer to rewrite the manifest." $liveManifest 0
450
+ }
451
+ }
452
+
453
+ # D8: live templates/fde/ matches repo
454
+ $liveFdeTpl = Join-Path $env:USERPROFILE '.copilot\m-skills\kushi\templates\fde'
455
+ $repoFdeTpl = Join-Path $pluginDir 'templates\fde'
456
+ if ((Test-Path $liveFdeTpl) -and (Test-Path $repoFdeTpl)) {
457
+ $liveFiles = Get-ChildItem -Path $liveFdeTpl -File -Filter '*.md' -ErrorAction SilentlyContinue
458
+ $repoFiles = Get-ChildItem -Path $repoFdeTpl -File -Filter '*.md' -ErrorAction SilentlyContinue
459
+ $liveNames = @($liveFiles | ForEach-Object { $_.Name })
460
+ $repoNames = @($repoFiles | ForEach-Object { $_.Name })
461
+ $missing = @($repoNames | Where-Object { $liveNames -notcontains $_ })
462
+ $extra = @($liveNames | Where-Object { $repoNames -notcontains $_ })
463
+ if ($missing.Count -gt 0 -or $extra.Count -gt 0) {
464
+ Add-Finding D8 'Live templates' 'warning' "Live templates/fde/ drifted from repo — missing: [$($missing -join ', ')]; extra: [$($extra -join ', ')]" "Re-run installer to sync FDE templates." $liveFdeTpl 0
465
+ } else {
466
+ foreach ($rf in $repoFiles) {
467
+ $lf = Join-Path $liveFdeTpl $rf.Name
468
+ $rh = (Get-FileHash $rf.FullName -Algorithm SHA256).Hash
469
+ $lh = (Get-FileHash $lf -Algorithm SHA256).Hash
470
+ if ($rh -ne $lh) {
471
+ Add-Finding D8 'Live templates' 'warning' "FDE template $($rf.Name) hash differs between repo and live install" "Re-run installer to sync." $lf 0
472
+ }
473
+ }
474
+ }
475
+ }
476
+ }
477
+
478
+ # === Output ===
479
+ if ($Json) {
480
+ $findings | ConvertTo-Json -Depth 6
481
+ } else {
482
+ $version = (Get-Frontmatter $agentFile).version
483
+ if (-not $version) {
484
+ try { $version = (Get-Content -Raw (Join-Path $Root 'package.json') | ConvertFrom-Json).version } catch {}
485
+ }
486
+ $stamp = Get-Date -Format 'yyyy-MM-dd HH:mm'
487
+ Write-Host ""
488
+ Write-Host "### Self-Check Results — Kushi v$version @ $stamp"
489
+ Write-Host ""
490
+
491
+ $bySurface = $findings | Group-Object surface
492
+ if ($findings.Count -eq 0) {
493
+ Write-Host "✅ All checks passed. Safe to commit."
494
+ } else {
495
+ $coreCount = ($findings | Where-Object { $_.code -notlike 'D*' }).Count
496
+ $deepCount = ($findings | Where-Object { $_.code -like 'D*' }).Count
497
+ Write-Host "| Surface | Count |"
498
+ Write-Host "|---------|------:|"
499
+ foreach ($g in $bySurface | Sort-Object Name) {
500
+ Write-Host ("| {0} | {1} |" -f $g.Name, $g.Count)
501
+ }
502
+ Write-Host ""
503
+ Write-Host "Findings:"
504
+ Write-Host ""
505
+ foreach ($x in $findings) {
506
+ $loc = if ($x.line) { ":$($x.line)" } else { "" }
507
+ Write-Host "⚠️ [$($x.code)] $($x.message)"
508
+ Write-Host " file: $($x.file)$loc"
509
+ Write-Host " fix : $($x.fix)"
510
+ Write-Host ""
511
+ }
512
+ Write-Host "⚠️ Found $($findings.Count) gap(s) — $coreCount core, $deepCount deep. Review and update before committing."
513
+ }
514
+ }
515
+
516
+ if ($StrictExit -and $findings.Count -gt 0) { exit 1 }
517
+ exit 0
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env bash
2
+ # Cross-OS wrapper for run.ps1.
3
+ # pwsh 7+ runs the same script unchanged on macOS / Linux / WSL / Windows.
4
+ #
5
+ # Why a wrapper instead of a native bash port?
6
+ # The check logic relies on regex over markdown + YAML frontmatter parsing
7
+ # + SHA256 hashing. PowerShell 7 has all of this in stdlib and is officially
8
+ # supported on macOS and Linux. A bash port would double the maintenance
9
+ # burden and drift over time. self-check D-checks already verify the .ps1
10
+ # stays consistent with the doctrine in SKILL.md.
11
+ #
12
+ # Install pwsh on macOS: brew install --cask powershell
13
+ # Install pwsh on Ubuntu: https://learn.microsoft.com/powershell/scripting/install/install-ubuntu
14
+ #
15
+ # Usage (matches run.ps1 exactly):
16
+ # ./run.sh # core checks, human output
17
+ # ./run.sh -Deep # core + deep checks
18
+ # ./run.sh -Json # JSON output (empty array = no findings)
19
+ # ./run.sh -Deep -StrictExit # CI-friendly: exit 1 on any finding
20
+ # ./run.sh -Root /path/to/kushi # check a specific kushi root
21
+
22
+ set -euo pipefail
23
+
24
+ if ! command -v pwsh >/dev/null 2>&1; then
25
+ echo "Error: pwsh (PowerShell 7+) is required." >&2
26
+ echo " macOS: brew install --cask powershell" >&2
27
+ echo " Ubuntu: see https://learn.microsoft.com/powershell/scripting/install/install-ubuntu" >&2
28
+ echo " Other: https://github.com/PowerShell/PowerShell/releases" >&2
29
+ exit 127
30
+ fi
31
+
32
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
33
+ exec pwsh -NoProfile -File "${SCRIPT_DIR}/run.ps1" "$@"
@@ -0,0 +1,114 @@
1
+ # FDE Intake — {{customer_name}} · {{engagement_title}} ({{division_or_team}})
2
+
3
+ **MSX Opportunity:** `{{msx_opportunity_id}}`
4
+
5
+ > **Document type:** FDE Intake (first artifact in this engagement)
6
+ > **Last updated:** {{generated_at}}
7
+ > **Generated by:** Kushi `fde-intake` v{{kushi_version}}
8
+
9
+ {{freshness_warning_block}}
10
+
11
+ ---
12
+
13
+ ## Q1 — Who you are currently engaged with or have engaged with
14
+
15
+ {{q1_stakeholders}}
16
+
17
+ > Required: ATU representation, Industry Team representation.
18
+ > Optional but explicitly stated: ISE, ISD, Executive Sponsor, Product Group.
19
+
20
+ ---
21
+
22
+ ## Q2 — Business scenario or technical blocker FDE would solve
23
+
24
+ {{q2_scenario}}
25
+
26
+ **Quantified business impact:** {{q2_impact_dollars}}
27
+
28
+ **Outcome chain (technical intervention → KPI → business outcome):**
29
+
30
+ - {{q2_chain_1}}
31
+ - {{q2_chain_2}}
32
+ - {{q2_chain_3}}
33
+
34
+ ---
35
+
36
+ ## Q3 — Why current offers / solutions are not suitable
37
+
38
+ {{q3_gaps}}
39
+
40
+ ---
41
+
42
+ ## Q4 — What does success look like for the customer
43
+
44
+ ### Outcome hypothesis
45
+
46
+ > *"If we {{q4_intervention}}, then {{q4_kpi}} will improve by {{q4_target}} within {{q4_timeframe}}, as measured by {{q4_method}} against {{q4_baseline}}."*
47
+
48
+ ### Quantified success criteria (≥ 2 KPIs)
49
+
50
+ | KPI | Baseline | Target | Timeframe | Measurement method |
51
+ |---|---|---|---|---|
52
+ | {{kpi1_name}} | {{kpi1_baseline}} | {{kpi1_target}} | {{kpi1_timeframe}} | {{kpi1_method}} |
53
+ | {{kpi2_name}} | {{kpi2_baseline}} | {{kpi2_target}} | {{kpi2_timeframe}} | {{kpi2_method}} |
54
+
55
+ ### Baseline
56
+
57
+ {{q4_baseline_detail}}
58
+
59
+ ### ROI framing
60
+
61
+ > {{q4_roi_statement}}
62
+
63
+ ### Value realization plan
64
+
65
+ {{q4_value_realization}}
66
+
67
+ ### Catcher team
68
+
69
+ {{q4_catcher_team}}
70
+
71
+ ---
72
+
73
+ ## Q5 — Relevant technology workloads
74
+
75
+ {{q5_technologies}}
76
+
77
+ ---
78
+
79
+ ## Q6 — Work completed so far
80
+
81
+ {{q6_completed_work}}
82
+
83
+ ### Commercial posture
84
+
85
+ | Item | Value | Source |
86
+ |---|---|---|
87
+ | Current yearly ACR | {{q6_acr_current}} | {{q6_acr_source}} |
88
+ | ACR growth trajectory | {{q6_acr_growth}} | {{q6_acr_growth_source}} |
89
+ | Expected program ACR (if successful) | {{q6_acr_expected}} | {{q6_acr_expected_source}} |
90
+ | MACC in place | {{q6_macc_in_place}} | {{q6_macc_source}} |
91
+ | MACC pending | {{q6_macc_pending}} | {{q6_macc_pending_source}} |
92
+ | Customer ROI (their own) | {{q6_customer_roi}} | {{q6_customer_roi_source}} |
93
+
94
+ ### Customer readiness
95
+
96
+ {{q6_customer_readiness}}
97
+
98
+ ---
99
+
100
+ ## Validation warnings (open)
101
+
102
+ {{validation_warnings_block}}
103
+
104
+ ---
105
+
106
+ ## Source coverage
107
+
108
+ | Source | Status |
109
+ |---|---|
110
+ {{source_coverage_rows}}
111
+
112
+ ---
113
+
114
+ > _This document was generated by Kushi (`fde-intake`) from `<project>/Evidence/` and the FDE reference pack. Every assertion carries an inline `[source: …]` citation. Edit in place as the engagement evolves — Kushi will preserve manual edits on re-run when the `--merge` flag is supplied._