kushi-agents 5.4.3 → 5.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +10 -0
  2. package/package.json +2 -2
  3. package/plugin/agents/kushi.agent.md +1 -0
  4. package/plugin/instructions/bootstrap-status-format.instructions.md +14 -13
  5. package/plugin/instructions/multi-user-shared-files.instructions.md +117 -87
  6. package/plugin/plugin.json +7 -4
  7. package/plugin/prompts/bootstrap.prompt.md +2 -1
  8. package/plugin/prompts/consolidate.prompt.md +4 -1
  9. package/plugin/prompts/migrate-files.prompt.md +29 -0
  10. package/plugin/skills/aggregate-project/SKILL.md +3 -3
  11. package/plugin/skills/bootstrap-project/SKILL.md +6 -6
  12. package/plugin/skills/consolidate-evidence/SKILL.md +94 -1
  13. package/plugin/skills/doctor/doctor.ps1 +101 -11
  14. package/plugin/skills/migrate-per-user-files/SKILL.md +47 -0
  15. package/plugin/skills/migrate-per-user-files/evals/evals.json +41 -0
  16. package/plugin/skills/migrate-per-user-files/migrate.ps1 +136 -0
  17. package/plugin/skills/migrate-per-user-files/references/migration-strategy.md +23 -0
  18. package/plugin/skills/pull-ado/SKILL.md +1 -1
  19. package/plugin/skills/pull-crm/SKILL.md +1 -1
  20. package/plugin/skills/pull-email/SKILL.md +1 -1
  21. package/plugin/skills/pull-loop/SKILL.md +1 -1
  22. package/plugin/skills/pull-meetings/SKILL.md +1 -1
  23. package/plugin/skills/pull-misc/SKILL.md +3 -3
  24. package/plugin/skills/pull-onenote/SKILL.md +1 -1
  25. package/plugin/skills/pull-sharepoint/SKILL.md +1 -1
  26. package/plugin/skills/pull-teams/SKILL.md +1 -1
  27. package/plugin/skills/refresh-project/SKILL.md +5 -5
  28. package/plugin/skills/self-check/SKILL.md +4 -2
  29. package/plugin/skills/self-check/run.ps1 +235 -1
  30. package/src/hooks-dispatcher.test.mjs +2 -2
  31. package/src/layout-portable.test.mjs +89 -0
  32. package/src/per-user-files.test.mjs +137 -0
  33. package/src/profile-coverage.test.mjs +84 -0
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: "consolidate-evidence"
3
- version: "3.0.0"
3
+ version: "3.1.0"
4
4
  description: "USE WHEN multiple contributors have pulled the same project AND the orchestrator (refresh-project / aggregate-project) needs a merged view at Evidence/_Consolidated/ for downstream skills (build-state, link-entities, ask-project). DO NOT USE manually — it's an internal pass. Capability: merges per-contributor Evidence/<alias>/<source>/ into Evidence/_Consolidated/ for a window using the 3-step reader fallback (_index → weekly → legacy). Latest-fact-wins; cites originating alias."
5
5
  ---
6
6
 
@@ -59,6 +59,98 @@ Snapshots are mostly per-source-of-truth (one canonical entity), so consolidatio
59
59
 
60
60
  Append a `consolidation_runs:` entry: `{ window, contributors, files_written }`.
61
61
 
62
+ ### Step 5 — Consolidate per-user authored files (v5.4.4+)
63
+
64
+ Per `multi-user-shared-files.instructions.md` § "Per-user authored files", three artifacts are written per-user under `Evidence/<alias>/<file>`. This step emits the cross-contributor rollup under `_Consolidated/<file>` for each. Deterministic merge — NO LLM. Idempotent: same input set produces byte-identical output.
65
+
66
+ This step **always runs** (even with a single contributor — a single-alias rollup is just a verbatim copy with an attribution header).
67
+
68
+ For each of the three files (`bootstrap-status.md`, `FOLLOW-UPS.md`, `OPEN-QUESTIONS-DRAFT.md`):
69
+
70
+ 1. Walk `<project>/Evidence/<alias>/<file>` for every alias listed in `Evidence/contributors.yml` (skip aliases that don't have the file).
71
+ 2. Apply the merge rules below.
72
+ 3. Write the result atomically to `<project>/_Consolidated/<file>` (write to `<file>.tmp`, then rename).
73
+
74
+ #### `_Consolidated/bootstrap-status.md`
75
+
76
+ Shape:
77
+
78
+ ```markdown
79
+ # Bootstrap Status (consolidated, <N> contributors)
80
+
81
+ > Auto-generated by `consolidate-evidence` Step 5 at <ISO-ts>. Source of truth lives under each `Evidence/<alias>/bootstrap-status.md`. Do not edit by hand.
82
+
83
+ ## Contributors who have bootstrapped this project
84
+
85
+ | Alias | Display Name | Last Run | Mode | Outcome |
86
+ |---|---|---|---|---|
87
+ | <alias> | ... | ... | ... | ... |
88
+
89
+ ## Latest discovery sweep results (most recent resolved per source, across all contributors)
90
+
91
+ | Source | Status | Discovered by | Discovered at |
92
+ |---|---|---|---|
93
+ | crm | resolved | ushak | 2026-05-27 09:42 EDT |
94
+ | ado | resolved | stand | 2026-05-25 11:01 EDT |
95
+ ...
96
+
97
+ ## Per-contributor bootstrap-status snapshots
98
+
99
+ ### ushak — `Evidence/ushak/bootstrap-status.md`
100
+
101
+ <verbatim copy of that file's body, demoted one heading level>
102
+
103
+ ### stand — `Evidence/stand/bootstrap-status.md`
104
+
105
+ <verbatim copy>
106
+ ```
107
+
108
+ - The "Contributors" table is harvested from each per-user file's `## Run summary` row.
109
+ - The "Latest discovery sweep results" table is harvested from each per-user file's `## Context Artifact Status` table; per source, keep the row whose Status is `resolved` (or `populated`) with the most recent timestamp.
110
+ - Per-contributor snapshot bodies are copied verbatim with heading levels demoted (`#` → `##`, etc.) for unambiguous Markdown nesting.
111
+
112
+ #### `_Consolidated/FOLLOW-UPS.md`
113
+
114
+ Shape:
115
+
116
+ ```markdown
117
+ # Follow-ups (consolidated, <N> contributors)
118
+
119
+ > Auto-generated by `consolidate-evidence` Step 5 at <ISO-ts>. Source of truth lives under each `Evidence/<alias>/FOLLOW-UPS.md`. Do not edit by hand.
120
+
121
+ ## Open follow-ups
122
+
123
+ ### <source> · <YYYY-MM-DD> · <alias> (also reported by: <other-alias>, ...)
124
+
125
+ <verbatim 5-field block from the originating alias's `Evidence/<alias>/FOLLOW-UPS.md`>
126
+
127
+ ## Resolved follow-ups
128
+
129
+ <dedup-merged blocks tagged with originating alias>
130
+ ```
131
+
132
+ - Dedup key per row: `(source, normalized first line of "Next-time-do")`. When two aliases report the same gap, keep the earliest-dated block and append "also reported by: <alias>" to the heading.
133
+ - Resolution status is per-alias — a row stays in "Open" if ANY contributor still has it Open.
134
+
135
+ #### `_Consolidated/OPEN-QUESTIONS-DRAFT.md`
136
+
137
+ Shape:
138
+
139
+ ```markdown
140
+ # Open questions (consolidated, <N> contributors)
141
+
142
+ > Auto-generated by `consolidate-evidence` Step 5 at <ISO-ts>. Source of truth lives under each `Evidence/<alias>/OPEN-QUESTIONS-DRAFT.md`. Do not edit by hand.
143
+
144
+ | # | Question | Source | Earliest asked | Asked by | Status |
145
+ |---|---|---|---|---|---|
146
+ | 1 | ... | ... | 2026-05-20 | ushak, stand | open |
147
+ | 2 | ... | ... | 2026-05-22 | stand | open |
148
+ ```
149
+
150
+ - Dedup key: lowercase + whitespace-collapsed question text.
151
+ - Earliest-asked = MIN(per-alias asked-on date).
152
+ - Asked-by column = sorted union of every alias that asked it.
153
+
62
154
  ## Triggers
63
155
 
64
156
  - "consolidate `<X>` last `<N>` days"
@@ -71,6 +163,7 @@ When this skill exposes a reusable defect (auth pattern, doctrine gap, layout mi
71
163
 
72
164
  ## Changelog
73
165
 
166
+ - **v3.1.0 (kushi v5.4.4, 2026-05-27)**: Step 5 added — consolidate per-user authored files (`bootstrap-status.md`, `FOLLOW-UPS.md`, `OPEN-QUESTIONS-DRAFT.md`) into `_Consolidated/<file>`. Deterministic merge, no LLM. Always runs (even single-contributor). Cross-references per-user truth at `Evidence/<alias>/<file>`.
74
167
  - **v3.0.0 (kushi v4.9.0, 2026-05-26)**: 3-step reader fallback chain (`_index/entities.yml` → `weekly/*.md` → legacy `snapshot/` + `stream/`). New citation form `weekly/<YYYY-MM-DD>_<source>-csc.md#<anchor>`. Legacy citations suffixed `(legacy pre-v4.9.0 layout)`. Output marked with `Source-layout:` footer.
75
168
 
76
169
  ## Validation loop
@@ -27,12 +27,51 @@ pwsh plugin/skills/doctor/doctor.ps1 -Json
27
27
  param(
28
28
  [string]$Repo = (Resolve-Path (Join-Path $PSScriptRoot "..\..\..")).Path,
29
29
  [switch]$Json,
30
- [switch]$Strict
30
+ [switch]$Strict,
31
+ [ValidateSet('auto','source','install')] [string]$LayoutMode = 'auto'
31
32
  )
32
33
 
33
34
  $ErrorActionPreference = 'Continue'
34
35
  $sections = New-Object System.Collections.Generic.List[object]
35
36
 
37
+ # === v5.4.5: layout-portable doctor ===
38
+ function Resolve-KushiLayout([string]$RootPath) {
39
+ if (Test-Path (Join-Path $RootPath 'plugin')) {
40
+ return @{ Mode='source'; Plugin=(Join-Path $RootPath 'plugin'); Repo=$RootPath; HasPluginJson=$true }
41
+ }
42
+ if (Test-Path (Join-Path $RootPath 'skills')) {
43
+ $pj = Join-Path $RootPath 'kushi-install.json'
44
+ return @{ Mode='install'; Plugin=$RootPath; Repo=$RootPath; HasPluginJson=(Test-Path $pj) }
45
+ }
46
+ throw "Not a kushi layout: $RootPath (need either plugin/ or skills/)."
47
+ }
48
+
49
+ function Get-KushiInstallHost([string]$InstallRoot) {
50
+ $norm = ($InstallRoot -replace '\\','/').ToLower()
51
+ if ($norm -match '/\.copilot/') { return 'clawpilot' }
52
+ if ($norm -match '/\.vscode/') { return 'vscode' }
53
+ return 'unknown'
54
+ }
55
+
56
+ $Layout = Resolve-KushiLayout $Repo
57
+ if ($LayoutMode -ne 'auto' -and $Layout.Mode -ne $LayoutMode) {
58
+ if ($LayoutMode -eq 'source') {
59
+ if (-not (Test-Path (Join-Path $Repo 'plugin'))) { Write-Error "LayoutMode=source requested but no plugin/ under $Repo."; exit 2 }
60
+ $Layout = @{ Mode='source'; Plugin=(Join-Path $Repo 'plugin'); Repo=$Repo; HasPluginJson=$true }
61
+ } else {
62
+ if (-not (Test-Path (Join-Path $Repo 'skills'))) { Write-Error "LayoutMode=install requested but no skills/ under $Repo."; exit 2 }
63
+ $Layout = @{ Mode='install'; Plugin=$Repo; Repo=$Repo; HasPluginJson=(Test-Path (Join-Path $Repo 'kushi-install.json')) }
64
+ }
65
+ }
66
+
67
+ if (-not $Json) {
68
+ if ($Layout.Mode -eq 'install') {
69
+ Write-Host ("[layout=install (host={0})]" -f (Get-KushiInstallHost $Layout.Repo))
70
+ } else {
71
+ Write-Host "[layout=source]"
72
+ }
73
+ }
74
+
36
75
  function Add-Section {
37
76
  param(
38
77
  [string]$Name,
@@ -90,9 +129,15 @@ if ($envProblems.Count -eq 0) {
90
129
 
91
130
  # ── Section 2: self-check -Deep ─────────────────────────────────────────────
92
131
  Write-Banner "2. self-check -Deep" "Cyan"
93
- $selfCheckPath = Join-Path $Repo 'plugin\skills\self-check\run.ps1'
132
+ if ($Layout.Mode -eq 'source') {
133
+ $selfCheckPath = Join-Path $Layout.Plugin 'skills\self-check\run.ps1'
134
+ } else {
135
+ $selfCheckPath = Join-Path $Layout.Plugin 'skills\self-check\run.ps1'
136
+ }
94
137
  if (Test-Path $selfCheckPath) {
95
- $scJson = & pwsh -NoProfile -File $selfCheckPath -Deep -Json -Root $Repo 2>$null
138
+ $scArgs = @('-NoProfile','-File',$selfCheckPath,'-Deep','-Json','-Root',$Layout.Repo)
139
+ if ($Layout.Mode -eq 'install') { $scArgs += @('-LayoutMode','install') }
140
+ $scJson = & pwsh @scArgs 2>$null
96
141
  $scExit = $LASTEXITCODE
97
142
  $scFindings = @()
98
143
  try { $scFindings = $scJson | ConvertFrom-Json } catch {}
@@ -106,15 +151,24 @@ if (Test-Path $selfCheckPath) {
106
151
  Write-Host " ⚠️ $scCount finding(s)" -ForegroundColor Yellow
107
152
  foreach ($f in $scFindings) { Write-Host " [$($f.code)] $($f.message)" -ForegroundColor DarkYellow }
108
153
  }
109
- Add-Section 'self-check' 'yellow' "$scCount finding(s)" 'pwsh plugin/skills/self-check/run.ps1 -Deep # then fix each [code]'
154
+ Add-Section 'self-check' 'yellow' "$scCount finding(s)" 'pwsh skills/self-check/run.ps1 -Deep # then fix each [code]'
110
155
  }
111
156
  } else {
112
157
  if (-not $Json) { Write-Host " ❌ run.ps1 missing" -ForegroundColor Red }
113
- Add-Section 'self-check' 'red' 'run.ps1 not found' "Restore plugin/skills/self-check/run.ps1 from git."
158
+ if ($Layout.Mode -eq 'install') {
159
+ $hostFlag = switch (Get-KushiInstallHost $Layout.Repo) { 'clawpilot' { '--clawpilot' } 'vscode' { '--vscode' } default { '--all-hosts' } }
160
+ Add-Section 'self-check' 'red' 'run.ps1 not found in install' "Re-install: ``npx kushi-agents@latest $hostFlag --force``"
161
+ } else {
162
+ Add-Section 'self-check' 'red' 'run.ps1 not found' "Restore plugin/skills/self-check/run.ps1 from git."
163
+ }
114
164
  }
115
165
 
116
166
  # ── Section 3: canary evals ─────────────────────────────────────────────────
117
167
  Write-Banner "3. canary evals" "Cyan"
168
+ if ($Layout.Mode -eq 'install') {
169
+ if (-not $Json) { Write-Host " ℹ️ skipped in install mode (no eval baseline shipped)" -ForegroundColor Gray }
170
+ Add-Section 'canary-evals' 'green' 'skipped (install mode)' ''
171
+ } else {
118
172
  Push-Location $Repo
119
173
  $canaryOut = & npm run --silent eval:canary 2>&1
120
174
  $canaryExit = $LASTEXITCODE
@@ -141,31 +195,66 @@ if ($canaryExit -eq 0) {
141
195
  Add-Section 'canary-evals' 'red' "FAIL exit=$canaryExit" 'npm run eval:canary # investigate failing case'
142
196
  }
143
197
  }
198
+ }
144
199
 
145
200
  # ── Section 4: skill-checker -All ───────────────────────────────────────────
146
201
  Write-Banner "4. skill-checker -All" "Cyan"
147
- $checkerPath = Join-Path $Repo 'plugin\skills\skill-checker\check-skill.ps1'
202
+ $checkerPath = Join-Path $Layout.Plugin 'skills\skill-checker\check-skill.ps1'
148
203
  if (Test-Path $checkerPath) {
149
- $ckOut = & pwsh -NoProfile -File $checkerPath -All -Root $Repo 2>&1
204
+ $ckOut = & pwsh -NoProfile -File $checkerPath -All -Root $Layout.Repo 2>&1
150
205
  $ckExit = $LASTEXITCODE
151
206
  if ($ckExit -eq 0) {
152
207
  if (-not $Json) { Write-Host " ✅ all skills compliant" -ForegroundColor Green }
153
208
  Add-Section 'skill-checker' 'green' 'all skills pass blueprint' ''
154
209
  } else {
155
210
  if (-not $Json) { Write-Host " ⚠️ blueprint violations (exit=$ckExit)" -ForegroundColor Yellow }
156
- Add-Section 'skill-checker' 'yellow' "exit=$ckExit" 'pwsh plugin/skills/skill-checker/check-skill.ps1 -All # review output'
211
+ Add-Section 'skill-checker' 'yellow' "exit=$ckExit" 'pwsh skills/skill-checker/check-skill.ps1 -All # review output'
157
212
  }
158
213
  } else {
159
- Add-Section 'skill-checker' 'red' 'check-skill.ps1 not found' "Restore plugin/skills/skill-checker/check-skill.ps1"
214
+ if ($Layout.Mode -eq 'install') {
215
+ if (-not $Json) { Write-Host " ⚠️ skill-checker not installed in this profile (full only)" -ForegroundColor Yellow }
216
+ Add-Section 'skill-checker' 'yellow' 'not installed (full profile only)' "Re-install with full profile: ``npx kushi-agents@latest --all-hosts --profile full --force``"
217
+ } else {
218
+ Add-Section 'skill-checker' 'red' 'check-skill.ps1 not found' "Restore plugin/skills/skill-checker/check-skill.ps1"
219
+ }
160
220
  }
161
221
 
162
222
  # ── Section 5: Live-install drift ───────────────────────────────────────────
163
223
  Write-Banner "5. live-install drift" "Cyan"
224
+ if ($Layout.Mode -eq 'install') {
225
+ # Install mode: compare installed version (from kushi-install.json) to npm latest.
226
+ $installPjPath = Join-Path $Layout.Repo 'kushi-install.json'
227
+ $installedVer = $null
228
+ if (Test-Path $installPjPath) {
229
+ try { $installedVer = (Get-Content -Raw $installPjPath | ConvertFrom-Json).version } catch {}
230
+ }
231
+ $npmOk = $false
232
+ try { $null = & npm --version 2>$null; if ($LASTEXITCODE -eq 0) { $npmOk = $true } } catch {}
233
+ if (-not $npmOk) {
234
+ if (-not $Json) { Write-Host " ℹ️ npm not on PATH (skipped)" -ForegroundColor Gray }
235
+ Add-Section 'live-install' 'green' 'npm not on PATH (skipped)' ''
236
+ } elseif (-not $installedVer) {
237
+ if (-not $Json) { Write-Host " ⚠️ no kushi-install.json version field" -ForegroundColor Yellow }
238
+ $hostFlag = switch (Get-KushiInstallHost $Layout.Repo) { 'clawpilot' { '--clawpilot' } 'vscode' { '--vscode' } default { '--all-hosts' } }
239
+ Add-Section 'live-install' 'yellow' 'install manifest missing version' "Re-install: ``npx kushi-agents@latest $hostFlag --force``"
240
+ } else {
241
+ $latest = $null
242
+ try { $latest = (& npm view kushi-agents version 2>$null).Trim() } catch {}
243
+ if ($latest -and $latest -ne $installedVer) {
244
+ if (-not $Json) { Write-Host " ⚠️ installed=v$installedVer latest=v$latest" -ForegroundColor Yellow }
245
+ $hostFlag = switch (Get-KushiInstallHost $Layout.Repo) { 'clawpilot' { '--clawpilot' } 'vscode' { '--vscode' } default { '--all-hosts' } }
246
+ Add-Section 'live-install' 'yellow' "installed=v$installedVer latest=v$latest" "Re-install: ``npx kushi-agents@latest $hostFlag --force``"
247
+ } else {
248
+ if (-not $Json) { Write-Host " ✅ install matches npm latest" -ForegroundColor Green }
249
+ Add-Section 'live-install' 'green' "installed=v$installedVer (matches npm latest)" ''
250
+ }
251
+ }
252
+ } else {
164
253
  $liveSkill = Join-Path $userHome '.copilot\m-skills\kushi\SKILL.md'
165
- $agentFile = Join-Path $Repo 'plugin\agents\kushi.agent.md'
254
+ $agentFile = Join-Path $Layout.Plugin 'agents\kushi.agent.md'
166
255
  $metaPath = Join-Path $userHome '.copilot\m-skills\skills-metadata.json'
167
256
  $repoVersion = $null
168
- try { $repoVersion = (Get-Content -Raw (Join-Path $Repo 'package.json') | ConvertFrom-Json).version } catch {}
257
+ try { $repoVersion = (Get-Content -Raw (Join-Path $Layout.Repo 'package.json') | ConvertFrom-Json).version } catch {}
169
258
  $liveVersion = $null
170
259
  if (Test-Path $metaPath) {
171
260
  try {
@@ -204,6 +293,7 @@ if (-not (Test-Path $liveSkill)) {
204
293
  }
205
294
  }
206
295
  }
296
+ }
207
297
 
208
298
  # ── Section 6: Global wiki shape ────────────────────────────────────────────
209
299
  Write-Banner "6. global wiki shape" "Cyan"
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: "migrate-per-user-files"
3
+ version: "1.0.0"
4
+ description: "USE WHEN a project was bootstrapped under kushi ≤ v5.4.3 and the three root files (`bootstrap-status.md`, `FOLLOW-UPS.md`, `OPEN-QUESTIONS-DRAFT.md`) need to be relocated to per-user `Evidence/<alias>/` paths AND the user is ready to commit the move. DO NOT USE for routine refreshes (refresh-project already writes to new paths in v5.4.4+) or for shared root files (integrations.yml, kushi.yaml, .settings.yml) which stay at the project root."
5
+ profile: "standard"
6
+ verb: "migrate-files"
7
+ ---
8
+
9
+ # migrate-per-user-files
10
+
11
+ USE WHEN: A project repo was bootstrapped/refreshed under kushi ≤ v5.4.3 and the three previously-shared root files (`bootstrap-status.md`, `FOLLOW-UPS.md`, `OPEN-QUESTIONS-DRAFT.md`) exist at `<project>/`. v5.4.4 promotes those three files to per-user truth at `<project>/Evidence/<alias>/<file>`, with auto-consolidated rollups at `<project>/_Consolidated/<file>` emitted by `consolidate-evidence`. This skill performs the one-time relocation safely.
12
+
13
+ DO NOT USE FOR: routine refreshes (`refresh-project` already writes to the new paths in v5.4.4+) or for the shared files that stay at the root (`integrations.yml`, `kushi.yaml`, `.settings.yml`).
14
+
15
+ ## Steps
16
+
17
+ 1. **Resolve alias** — `Get-KushiConfig -Name 'project-evidence'` returns the current user's alias. Refuse to run if no alias is configured.
18
+ 2. **Discover sources** — For each of the three basenames, check whether `<project>/<file>` exists at the root.
19
+ 3. **Check destination** — For each found source, check whether `<project>/Evidence/<alias>/<file>` already exists.
20
+ - **No destination** → planned action: `move` (root → per-user).
21
+ - **Destination exists with byte-identical content** → planned action: `delete-root-duplicate` (per-user is authoritative, root is stale copy).
22
+ - **Destination exists with different content** → planned action: `needs-merge`. **Defensive: never overwrite.** Emit a `[needs-merge]` row, exit 1 in `--apply` mode (in dry-run, still report and exit 0 so the user sees the full plan).
23
+ 4. **Print the plan** — One row per file: `<basename> | <action> | <reason>`. With `--apply`, also print the destination path that was written.
24
+ 5. **Execute or no-op**:
25
+ - Without `--apply`: dry-run only. Print plan, exit 0.
26
+ - With `--apply`: for each `move` or `delete-root-duplicate`, perform the filesystem operation. Re-running with `--apply` after a successful run is a no-op (nothing to migrate).
27
+ 6. **Log to run-log** — Append a `migrations:` entry to `Evidence/run-log.yml` with timestamp, alias, files moved, files needing merge.
28
+
29
+ ## Arguments
30
+
31
+ - `-ProjectRoot <path>` (required) — Absolute path to the project folder.
32
+ - `-Apply` (switch) — Perform the migration. Without this flag, the script is dry-run only.
33
+ - `-StrictExit` (switch) — Exit 1 on any `needs-merge` row, even in dry-run mode.
34
+
35
+ ## Validation loop
36
+
37
+ After `--apply`, the script self-verifies by re-listing the three root files. If any of them still exists (other than because of a `needs-merge` row), exit 2 with a `[verify]` error.
38
+
39
+ ## Idempotency contract
40
+
41
+ Running with `--apply` on an already-migrated repo MUST be a no-op (exit 0, plan-table prints `nothing-to-do` for all three rows).
42
+
43
+ ## See also
44
+
45
+ - `plugin/instructions/multi-user-shared-files.instructions.md` — the v5.4.4 doctrine.
46
+ - `plugin/skills/consolidate-evidence/SKILL.md` Step 5 — the auto-rollup that consumes per-user files.
47
+ - `references/migration-strategy.md` — why defensive, why content-hash compare, why dry-run by default.
@@ -0,0 +1,41 @@
1
+ {
2
+ "skill": "migrate-per-user-files",
3
+ "version": "1.0.0",
4
+ "description": "Migration script + SKILL.md ship together and document the three target basenames.",
5
+ "cases": [
6
+ {
7
+ "id": "skill-md-exists",
8
+ "name": "SKILL.md and migrate.ps1 are present",
9
+ "input": "verify migrate-per-user-files SKILL.md and migrate.ps1 exist",
10
+ "canary": true,
11
+ "grader_type": "script",
12
+ "expected_assertions": [
13
+ { "type": "file-exists", "path": "plugin/skills/migrate-per-user-files/SKILL.md" },
14
+ { "type": "file-exists", "path": "plugin/skills/migrate-per-user-files/migrate.ps1" }
15
+ ]
16
+ },
17
+ {
18
+ "id": "skill-documents-three-basenames",
19
+ "name": "SKILL.md documents all three target basenames",
20
+ "input": "verify SKILL.md mentions bootstrap-status.md, FOLLOW-UPS.md, and OPEN-QUESTIONS-DRAFT.md",
21
+ "canary": false,
22
+ "grader_type": "script",
23
+ "expected_assertions": [
24
+ { "type": "file-contains", "path": "plugin/skills/migrate-per-user-files/SKILL.md", "needle": "bootstrap-status.md" },
25
+ { "type": "file-contains", "path": "plugin/skills/migrate-per-user-files/SKILL.md", "needle": "FOLLOW-UPS.md" },
26
+ { "type": "file-contains", "path": "plugin/skills/migrate-per-user-files/SKILL.md", "needle": "OPEN-QUESTIONS-DRAFT.md" }
27
+ ]
28
+ },
29
+ {
30
+ "id": "strategy-reference-ships",
31
+ "name": "references/migration-strategy.md documents the defensive contract",
32
+ "input": "verify migration-strategy.md exists and mentions content-hash",
33
+ "canary": false,
34
+ "grader_type": "script",
35
+ "expected_assertions": [
36
+ { "type": "file-exists", "path": "plugin/skills/migrate-per-user-files/references/migration-strategy.md" },
37
+ { "type": "file-contains", "path": "plugin/skills/migrate-per-user-files/references/migration-strategy.md", "needle": "content-hash" }
38
+ ]
39
+ }
40
+ ]
41
+ }
@@ -0,0 +1,136 @@
1
+ #requires -Version 7.0
2
+ <#
3
+ .SYNOPSIS
4
+ Migrate root-level bootstrap-status.md / FOLLOW-UPS.md / OPEN-QUESTIONS-DRAFT.md
5
+ to per-user Evidence/<alias>/ paths (kushi v5.4.4+ doctrine).
6
+
7
+ .PARAMETER ProjectRoot
8
+ Absolute path to the project folder.
9
+
10
+ .PARAMETER Apply
11
+ Perform the migration. Without this switch, the script is dry-run only.
12
+
13
+ .PARAMETER StrictExit
14
+ Exit 1 on any needs-merge row, even in dry-run mode.
15
+ #>
16
+ [CmdletBinding()]
17
+ param(
18
+ [Parameter(Mandatory)] [string] $ProjectRoot,
19
+ [switch] $Apply,
20
+ [switch] $StrictExit
21
+ )
22
+
23
+ $ErrorActionPreference = 'Stop'
24
+ $basenames = @('bootstrap-status.md','FOLLOW-UPS.md','OPEN-QUESTIONS-DRAFT.md')
25
+
26
+ if (-not (Test-Path -LiteralPath $ProjectRoot -PathType Container)) {
27
+ Write-Error "[migrate-per-user-files] ProjectRoot not found: $ProjectRoot"
28
+ exit 2
29
+ }
30
+
31
+ # --- Resolve alias --------------------------------------------------------
32
+ # Read directly from the project's user config — keeps this script standalone
33
+ # and avoids dot-sourcing helpers that have mandatory parameters.
34
+ $alias = $null
35
+ $cfgPath = Join-Path $ProjectRoot '.kushi/config/user/project-evidence.yml'
36
+ if (Test-Path -LiteralPath $cfgPath) {
37
+ $aliasLine = (Get-Content -LiteralPath $cfgPath) | Where-Object { $_ -match '^\s*alias\s*:\s*(\S+)' } | Select-Object -First 1
38
+ if ($aliasLine -and $aliasLine -match '^\s*alias\s*:\s*(\S+)') { $alias = $Matches[1].Trim('"').Trim("'") }
39
+ }
40
+ if (-not $alias) {
41
+ Write-Error "[migrate-per-user-files] No alias configured at $cfgPath. Run setup or set the project-evidence alias before migrating."
42
+ exit 2
43
+ }
44
+
45
+ $perUserDir = Join-Path $ProjectRoot "Evidence\$alias"
46
+ $plan = @()
47
+ $needsMergeCount = 0
48
+ $actedCount = 0
49
+
50
+ foreach ($name in $basenames) {
51
+ $src = Join-Path $ProjectRoot $name
52
+ $dst = Join-Path $perUserDir $name
53
+ $hasSrc = Test-Path -LiteralPath $src -PathType Leaf
54
+ $hasDst = Test-Path -LiteralPath $dst -PathType Leaf
55
+
56
+ if (-not $hasSrc -and -not $hasDst) {
57
+ $plan += [pscustomobject]@{ file=$name; action='nothing-to-do'; reason='neither root nor per-user copy present' }
58
+ continue
59
+ }
60
+ if (-not $hasSrc -and $hasDst) {
61
+ $plan += [pscustomobject]@{ file=$name; action='nothing-to-do'; reason='already migrated' }
62
+ continue
63
+ }
64
+ if ($hasSrc -and -not $hasDst) {
65
+ if ($Apply) {
66
+ if (-not (Test-Path -LiteralPath $perUserDir -PathType Container)) {
67
+ New-Item -ItemType Directory -Path $perUserDir -Force | Out-Null
68
+ }
69
+ Move-Item -LiteralPath $src -Destination $dst
70
+ $actedCount++
71
+ $plan += [pscustomobject]@{ file=$name; action='moved'; reason="-> Evidence/$alias/$name" }
72
+ } else {
73
+ $plan += [pscustomobject]@{ file=$name; action='move'; reason="would move root -> Evidence/$alias/$name" }
74
+ }
75
+ continue
76
+ }
77
+ # Both exist — compare content hashes.
78
+ $srcHash = (Get-FileHash -LiteralPath $src -Algorithm SHA256).Hash
79
+ $dstHash = (Get-FileHash -LiteralPath $dst -Algorithm SHA256).Hash
80
+ if ($srcHash -eq $dstHash) {
81
+ if ($Apply) {
82
+ Remove-Item -LiteralPath $src -Force
83
+ $actedCount++
84
+ $plan += [pscustomobject]@{ file=$name; action='deleted-root-duplicate'; reason='per-user copy is byte-identical' }
85
+ } else {
86
+ $plan += [pscustomobject]@{ file=$name; action='delete-root-duplicate'; reason='would delete byte-identical root copy' }
87
+ }
88
+ } else {
89
+ $needsMergeCount++
90
+ $plan += [pscustomobject]@{ file=$name; action='needs-merge'; reason='root and per-user copies differ — manual merge required, will not overwrite' }
91
+ }
92
+ }
93
+
94
+ Write-Host ""
95
+ Write-Host "[migrate-per-user-files] alias = $alias" -ForegroundColor Cyan
96
+ Write-Host "[migrate-per-user-files] project = $ProjectRoot" -ForegroundColor Cyan
97
+ Write-Host "[migrate-per-user-files] mode = $(if ($Apply) {'APPLY'} else {'DRY-RUN'})" -ForegroundColor Cyan
98
+ Write-Host ""
99
+ $plan | Format-Table -AutoSize | Out-String | Write-Host
100
+
101
+ # --- Run-log append (apply mode only) ------------------------------------
102
+ if ($Apply -and $actedCount -gt 0) {
103
+ $evDir = Join-Path $ProjectRoot 'Evidence'
104
+ if (-not (Test-Path -LiteralPath $evDir -PathType Container)) {
105
+ New-Item -ItemType Directory -Path $evDir -Force | Out-Null
106
+ }
107
+ $log = Join-Path $evDir 'run-log.yml'
108
+ $ts = (Get-Date).ToString('s') + 'Z'
109
+ $moved = ($plan | Where-Object { $_.action -in @('moved','deleted-root-duplicate') } | ForEach-Object { $_.file }) -join ', '
110
+ $merge = ($plan | Where-Object { $_.action -eq 'needs-merge' } | ForEach-Object { $_.file }) -join ', '
111
+ $entry = @(
112
+ "migrations:",
113
+ " - timestamp: $ts",
114
+ " alias: $alias",
115
+ " skill: migrate-per-user-files",
116
+ " files_migrated: [$moved]",
117
+ " files_needs_merge: [$merge]"
118
+ ) -join "`r`n"
119
+ Add-Content -LiteralPath $log -Value $entry
120
+ }
121
+
122
+ # --- Verify (apply mode only) --------------------------------------------
123
+ if ($Apply) {
124
+ foreach ($name in $basenames) {
125
+ $src = Join-Path $ProjectRoot $name
126
+ $stillThere = Test-Path -LiteralPath $src -PathType Leaf
127
+ $row = $plan | Where-Object { $_.file -eq $name } | Select-Object -First 1
128
+ if ($stillThere -and $row.action -notin @('needs-merge','nothing-to-do')) {
129
+ Write-Error "[verify] $name still present at root after migration"
130
+ exit 2
131
+ }
132
+ }
133
+ }
134
+
135
+ if ($needsMergeCount -gt 0 -and ($Apply -or $StrictExit)) { exit 1 }
136
+ exit 0
@@ -0,0 +1,23 @@
1
+ # Migration strategy — per-user file relocation
2
+
3
+ ## Why defensive (refuse to overwrite)
4
+
5
+ The three migrated files are authored artifacts that may have hand edits, accumulated follow-ups, or carefully-curated open-question wording. A naive `Move-Item -Force` could silently destroy hours of work if a per-user copy already exists. We instead detect the collision and refuse — the user must decide which copy wins (or hand-merge them).
6
+
7
+ ## Why content-hash compare
8
+
9
+ If both copies exist with byte-identical content, the per-user copy is authoritative (v5.4.4+) and the root copy is a stale duplicate left behind by some workflow. Deleting it is safe and quiet — no manual merge needed.
10
+
11
+ If the content differs by even one byte, we cannot know which copy is newer, so we emit `[needs-merge]` and stop.
12
+
13
+ ## Why dry-run by default
14
+
15
+ Mass filesystem operations should always print a plan first. The `--apply` flag is the explicit "yes, do it" gate. This matches the pattern used by `apply-ado-update` (preview profile) and the v5.4.3 lessons learned from the profile-allowlist catch-up.
16
+
17
+ ## Why one-time (not part of `refresh-project`)
18
+
19
+ v5.4.4 `refresh-project` already writes to the new per-user paths. The migration is a one-time data move for repos initialized under ≤ v5.4.3. Folding it into refresh would mean checking-and-migrating on every refresh — wasteful and potentially destructive if the user has not yet decided how to handle a `needs-merge` situation.
20
+
21
+ ## Why log to run-log.yml
22
+
23
+ Every project-touching kushi action logs to `Evidence/run-log.yml`. The migration is a project-touching action and follows the same convention so a future `doctor` or `project-status` run can see when the migration happened and which files were affected.
@@ -311,7 +311,7 @@ After successful pass:
311
311
  ## References (v4.4.7)
312
312
 
313
313
  - Name → ID resolution follows ..\..\instructions\fuzzy-disambiguation.instructions.md (universal fuzzy contract).
314
- - After this pull completes, the per-source verification gate runs: ..\..\instructions\per-source-verification-gate.instructions.md (retry once, then write FOLLOW-UPS.md).
314
+ - After this pull completes, the per-source verification gate runs: ..\..\instructions\per-source-verification-gate.instructions.md (retry once, then write Evidence/<alias>/FOLLOW-UPS.md per v5.4.4+ per-user files doctrine; consolidated at _Consolidated/FOLLOW-UPS.md).
315
315
 
316
316
 
317
317
  ## Issue Recovery
@@ -202,7 +202,7 @@ After successful pass:
202
202
  ## References (v4.4.7)
203
203
 
204
204
  - Name → ID resolution follows ..\..\instructions\fuzzy-disambiguation.instructions.md (universal fuzzy contract).
205
- - After this pull completes, the per-source verification gate runs: ..\..\instructions\per-source-verification-gate.instructions.md (retry once, then write FOLLOW-UPS.md).
205
+ - After this pull completes, the per-source verification gate runs: ..\..\instructions\per-source-verification-gate.instructions.md (retry once, then write Evidence/<alias>/FOLLOW-UPS.md per v5.4.4+ per-user files doctrine; consolidated at _Consolidated/FOLLOW-UPS.md).
206
206
 
207
207
 
208
208
  ## Issue Recovery
@@ -193,7 +193,7 @@ An entity that cannot meet the threshold is flagged `low_signal: true` in `_inde
193
193
  ## References (v4.4.7)
194
194
 
195
195
  - Name → ID resolution follows ..\..\instructions\fuzzy-disambiguation.instructions.md (universal fuzzy contract).
196
- - After this pull completes, the per-source verification gate runs: ..\..\instructions\per-source-verification-gate.instructions.md (retry once, then write FOLLOW-UPS.md).
196
+ - After this pull completes, the per-source verification gate runs: ..\..\instructions\per-source-verification-gate.instructions.md (retry once, then write Evidence/<alias>/FOLLOW-UPS.md per v5.4.4+ per-user files doctrine; consolidated at _Consolidated/FOLLOW-UPS.md).
197
197
 
198
198
 
199
199
  ## Issue Recovery
@@ -124,7 +124,7 @@ Per `loop-bootstrap-discovery.instructions.md`:
124
124
  | Pre-flight | Failure action |
125
125
  |---|---|
126
126
  | **A. Workspaces registered** for the resolved project | Refuse to run. Instruct user to run `@Kushi setup --reconfigure`. |
127
- | **B. Pages registered** OR `loop_pages_status: to-be-enumerated` | Run page enumeration via WorkIQ first; on enumerate-fail, write FOLLOW-UPS.md per the gate. |
127
+ | **B. Pages registered** OR `loop_pages_status: to-be-enumerated` | Run page enumeration via WorkIQ first; on enumerate-fail, write Evidence/<alias>/FOLLOW-UPS.md per the gate (v5.4.4+; rollup at _Consolidated/FOLLOW-UPS.md). |
128
128
  | **C. Playwright profile exists** at `~/.kushi/playwright-profile/m365/` or `.../onenote/` | Refuse; instruct: `node plugin/skills/pull-onenote/runner.mjs --bootstrap`. |
129
129
  | **D. URLs are canonical** (matches Loop URL grammar; not synthesized) | Refuse + log to learnings/loop.md per `issue-recovery.instructions.md`. |
130
130
 
@@ -171,7 +171,7 @@ After the pass:
171
171
  ## References (v4.4.7)
172
172
 
173
173
  - Name → ID resolution follows ..\..\instructions\fuzzy-disambiguation.instructions.md (universal fuzzy contract).
174
- - After this pull completes, the per-source verification gate runs: ..\..\instructions\per-source-verification-gate.instructions.md (retry once, then write FOLLOW-UPS.md).
174
+ - After this pull completes, the per-source verification gate runs: ..\..\instructions\per-source-verification-gate.instructions.md (retry once, then write Evidence/<alias>/FOLLOW-UPS.md per v5.4.4+ per-user files doctrine; consolidated at _Consolidated/FOLLOW-UPS.md).
175
175
 
176
176
 
177
177
  ## Issue Recovery
@@ -98,7 +98,7 @@ For each project, the bootstrap step:
98
98
  1. **Locates `<project>/external-links.txt`.** If absent, write a starter template (same format as ABN AMRO has today) and mark `boundaries.misc.externalLinksPath` in `integrations.yml`.
99
99
  2. **Parses the file.** Skip comments, skip blank lines, skip placeholder URLs (`<PASTE_*_URL>`, `<TODO*>`).
100
100
  3. **Initializes `misc_links[]`** in `m365-mutable.json#knownSections.<projectKey>` with one entry per non-placeholder link, all `last_status: not-yet-attempted`.
101
- 4. **Records** `boundaries.misc.linkCount` and `placeholderCount` to `bootstrap-status.md`.
101
+ 4. **Records** `boundaries.misc.linkCount` and `placeholderCount` to `Evidence/<alias>/bootstrap-status.md` (per-user, v5.4.4+; consolidated rollup at `_Consolidated/bootstrap-status.md`).
102
102
 
103
103
  ## Step A — enumerate (every refresh)
104
104
 
@@ -117,7 +117,7 @@ The runner branches per `type`:
117
117
 
118
118
  Skip fetch. Write registry entry with `captured_via: delegated`, `delegated_to: pull-loop`. The actual workspace/page capture happens in `pull-loop` (see `plugin/skills/pull-loop/SKILL.md`), which uses the canonical Loop boundary in `<project>/integrations.yml#boundaries.loop.workspace_ids[]` rather than free-text `external-links.txt` URLs.
119
119
 
120
- If the Loop URL pasted into `external-links.txt` references a workspace NOT registered in `boundaries.loop.workspace_ids[]`, `pull-misc` records `last_status: unregistered-loop-workspace` and notes the URL in `<project>/OPEN-QUESTIONS-DRAFT.md` so the user can decide whether to register it via `@Kushi setup --reconfigure`.
120
+ If the Loop URL pasted into `external-links.txt` references a workspace NOT registered in `boundaries.loop.workspace_ids[]`, `pull-misc` records `last_status: unregistered-loop-workspace` and notes the URL in `<project>/Evidence/<alias>/OPEN-QUESTIONS-DRAFT.md` (per-user, v5.4.4+; consolidated rollup at `_Consolidated/OPEN-QUESTIONS-DRAFT.md`) so the user can decide whether to register it via `@Kushi setup --reconfigure`.
121
121
 
122
122
  ### B.2 `web` / `confluence` / `learn` / `docs` / `github` / unknown — HTTP
123
123
 
@@ -263,7 +263,7 @@ Example: `[source: misc/loop/ABN-Core-Team-Sync-2026-03-27 · 2026-05-14]`
263
263
  ## References (v4.4.7)
264
264
 
265
265
  - Name → ID resolution follows ..\..\instructions\fuzzy-disambiguation.instructions.md (universal fuzzy contract).
266
- - After this pull completes, the per-source verification gate runs: ..\..\instructions\per-source-verification-gate.instructions.md (retry once, then write FOLLOW-UPS.md).
266
+ - After this pull completes, the per-source verification gate runs: ..\..\instructions\per-source-verification-gate.instructions.md (retry once, then write Evidence/<alias>/FOLLOW-UPS.md per v5.4.4+ per-user files doctrine; consolidated at _Consolidated/FOLLOW-UPS.md).
267
267
 
268
268
 
269
269
  ## Issue Recovery
@@ -171,7 +171,7 @@ Every refresh writes `Evidence/<alias>/refresh-reports/<YYYY-MM-DD>-<HHMM>-oneno
171
171
  ## References (v4.4.7)
172
172
 
173
173
  - Name → ID resolution follows ..\..\instructions\fuzzy-disambiguation.instructions.md (universal fuzzy contract).
174
- - After this pull completes, the per-source verification gate runs: ..\..\instructions\per-source-verification-gate.instructions.md (retry once, then write FOLLOW-UPS.md).
174
+ - After this pull completes, the per-source verification gate runs: ..\..\instructions\per-source-verification-gate.instructions.md (retry once, then write Evidence/<alias>/FOLLOW-UPS.md per v5.4.4+ per-user files doctrine; consolidated at _Consolidated/FOLLOW-UPS.md).
175
175
 
176
176
 
177
177
  ## Issue Recovery
@@ -183,7 +183,7 @@ An entity that cannot meet the threshold is flagged `low_signal: true` in `_inde
183
183
  ## References (v4.4.7)
184
184
 
185
185
  - Name → ID resolution follows ..\..\instructions\fuzzy-disambiguation.instructions.md (universal fuzzy contract).
186
- - After this pull completes, the per-source verification gate runs: ..\..\instructions\per-source-verification-gate.instructions.md (retry once, then write FOLLOW-UPS.md).
186
+ - After this pull completes, the per-source verification gate runs: ..\..\instructions\per-source-verification-gate.instructions.md (retry once, then write Evidence/<alias>/FOLLOW-UPS.md per v5.4.4+ per-user files doctrine; consolidated at _Consolidated/FOLLOW-UPS.md).
187
187
 
188
188
 
189
189
  ## Issue Recovery