claude-dev-env 1.93.1 → 1.94.0

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 (215) hide show
  1. package/_shared/pr-loop/CLAUDE.md +1 -0
  2. package/_shared/pr-loop/audit-contract.md +1 -1
  3. package/_shared/pr-loop/gh-payloads.md +3 -3
  4. package/_shared/pr-loop/post-audit-thread-contract.md +51 -0
  5. package/_shared/pr-loop/scripts/README.md +2 -2
  6. package/_shared/pr-loop/scripts/fix_hookspath.py +47 -12
  7. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -69
  8. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -374
  9. package/_shared/pr-loop/state-schema.md +3 -22
  10. package/bin/CLAUDE.md +1 -1
  11. package/bin/install.mjs +2 -7
  12. package/bin/install.test.mjs +10 -4
  13. package/hooks/blocking/CLAUDE.md +2 -0
  14. package/hooks/blocking/pii_commit_command.py +398 -0
  15. package/hooks/blocking/pii_payload_scan.py +133 -0
  16. package/hooks/blocking/pii_prevention_blocker.py +151 -449
  17. package/hooks/blocking/test_pii_prevention_blocker.py +299 -0
  18. package/hooks/blocking/test_verdict_directory_write_blocker.py +42 -7
  19. package/hooks/hooks_constants/CLAUDE.md +2 -2
  20. package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -2
  21. package/hooks/hooks_constants/local_identity.py +65 -15
  22. package/hooks/hooks_constants/pii_prevention_constants.py +16 -0
  23. package/hooks/hooks_constants/pr_description_enforcer_constants.py +1 -1
  24. package/hooks/hooks_constants/test_local_identity.py +68 -0
  25. package/package.json +1 -1
  26. package/scripts/CLAUDE.md +10 -1
  27. package/scripts/Get-SessionAccount.ps1 +210 -0
  28. package/scripts/tests/CLAUDE.md +15 -1
  29. package/scripts/tests/Get-SessionAccount.Tests.ps1 +350 -0
  30. package/skills/CLAUDE.md +3 -25
  31. package/skills/anthropic-plan/SKILL.md +1 -13
  32. package/skills/autoconverge/CLAUDE.md +5 -2
  33. package/skills/autoconverge/SKILL.md +112 -387
  34. package/skills/autoconverge/reference/CLAUDE.md +4 -1
  35. package/skills/autoconverge/reference/closing-report.md +9 -2
  36. package/skills/autoconverge/reference/convergence.md +26 -22
  37. package/skills/autoconverge/reference/headless-safety.md +44 -0
  38. package/skills/autoconverge/reference/multi-pr.md +88 -0
  39. package/skills/autoconverge/reference/self-closing-loop.md +84 -0
  40. package/skills/autoconverge/reference/stop-conditions.md +21 -13
  41. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +87 -27
  42. package/skills/autoconverge/workflow/converge.contract.test.mjs +40 -10
  43. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +179 -47
  44. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  45. package/skills/autoconverge/workflow/converge.mjs +284 -94
  46. package/skills/bugteam/CLAUDE.md +1 -1
  47. package/skills/bugteam/CONSTRAINTS.md +3 -3
  48. package/skills/bugteam/PROMPTS.md +8 -19
  49. package/skills/bugteam/SKILL.md +13 -14
  50. package/skills/bugteam/reference/CLAUDE.md +1 -1
  51. package/skills/bugteam/reference/audit-and-teammates.md +2 -2
  52. package/skills/bugteam/reference/copilot-gap-analysis.md +2 -0
  53. package/skills/bugteam/reference/github-pr-reviews.md +56 -38
  54. package/skills/bugteam/reference/team-setup.md +11 -11
  55. package/skills/bugteam/scripts/CLAUDE.md +6 -6
  56. package/skills/bugteam/scripts/README.md +20 -8
  57. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +13 -1463
  58. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +23 -358
  59. package/skills/bugteam/scripts/bugteam_preflight.py +13 -315
  60. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +2 -3
  61. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +34 -1053
  62. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +30 -347
  63. package/skills/bugteam/scripts/test_bugteam_preflight.py +30 -305
  64. package/skills/copilot-finding-triage/SKILL.md +80 -28
  65. package/skills/copilot-finding-triage/reference/tier-rubric.md +70 -11
  66. package/skills/copilot-finding-triage/templates/notification.md +8 -3
  67. package/skills/everything-search/SKILL.md +1 -2
  68. package/skills/grokify/SKILL.md +58 -0
  69. package/skills/grokify/templates/handoff-template.md +92 -0
  70. package/skills/orchestrator/SKILL.md +12 -8
  71. package/skills/orchestrator-refresh/SKILL.md +3 -3
  72. package/skills/pr-converge/SKILL.md +132 -233
  73. package/skills/pr-converge/reference/convergence-gates.md +102 -66
  74. package/skills/pr-converge/reference/examples.md +37 -25
  75. package/skills/pr-converge/reference/fix-protocol.md +7 -7
  76. package/skills/pr-converge/reference/multi-pr-orchestration.md +10 -4
  77. package/skills/pr-converge/reference/per-tick.md +64 -41
  78. package/skills/pr-converge/reference/state-schema.md +27 -2
  79. package/skills/pr-converge/scripts/CLAUDE.md +8 -0
  80. package/skills/pr-converge/scripts/README.md +4 -3
  81. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +23 -0
  82. package/skills/pr-converge/scripts/check_convergence.py +189 -645
  83. package/skills/pr-converge/scripts/check_convergence_gates.py +235 -0
  84. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +219 -0
  85. package/skills/pr-converge/scripts/conftest.py +8 -0
  86. package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -0
  87. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -0
  88. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +26 -0
  89. package/skills/pr-converge/scripts/test_check_convergence.py +73 -228
  90. package/skills/pr-converge/scripts/test_check_convergence_contract.py +321 -0
  91. package/skills/pr-converge/scripts/test_check_convergence_gates.py +68 -0
  92. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +75 -0
  93. package/skills/privacy-hygiene/SKILL.md +4 -3
  94. package/skills/reviewer-gates/SKILL.md +26 -8
  95. package/skills/session-log/SKILL.md +11 -12
  96. package/skills/session-tidy/SKILL.md +40 -30
  97. package/skills/skill-builder/CLAUDE.md +13 -10
  98. package/skills/skill-builder/SKILL.md +31 -31
  99. package/skills/skill-builder/references/CLAUDE.md +10 -6
  100. package/skills/skill-builder/references/delegation-map.md +13 -4
  101. package/skills/skill-builder/references/description-field.md +113 -0
  102. package/skills/skill-builder/references/self-audit-checklist.md +21 -4
  103. package/skills/skill-builder/references/skill-modularity.md +113 -0
  104. package/skills/skill-builder/references/skill-types.md +4 -0
  105. package/skills/skill-builder/templates/CLAUDE.md +4 -4
  106. package/skills/skill-builder/templates/gap-analysis.md +25 -1
  107. package/skills/skill-builder/workflows/CLAUDE.md +6 -4
  108. package/skills/skill-builder/workflows/improve-skill.md +17 -16
  109. package/skills/skill-builder/workflows/new-skill.md +34 -10
  110. package/skills/skill-builder/workflows/polish-skill.md +59 -43
  111. package/skills/usage-pause/SKILL.md +10 -14
  112. package/skills/bdd-protocol/CLAUDE.md +0 -26
  113. package/skills/bdd-protocol/SKILL.md +0 -30
  114. package/skills/bdd-protocol/references/CLAUDE.md +0 -10
  115. package/skills/bdd-protocol/references/anti-patterns.md +0 -26
  116. package/skills/bdd-protocol/references/example-mapping.md +0 -23
  117. package/skills/bg-agent/CLAUDE.md +0 -17
  118. package/skills/bg-agent/SKILL.md +0 -69
  119. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +0 -55
  120. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_fix_hookspath_constants.py +0 -26
  121. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_preflight_constants.py +0 -35
  122. package/skills/caveman/CLAUDE.md +0 -15
  123. package/skills/caveman/SKILL.md +0 -38
  124. package/skills/code/CLAUDE.md +0 -17
  125. package/skills/code/SKILL.md +0 -178
  126. package/skills/deep-research/CLAUDE.md +0 -17
  127. package/skills/deep-research/SKILL.md +0 -80
  128. package/skills/findbugs/CLAUDE.md +0 -20
  129. package/skills/findbugs/SKILL.md +0 -241
  130. package/skills/fixbugs/CLAUDE.md +0 -19
  131. package/skills/fixbugs/SKILL.md +0 -142
  132. package/skills/gh-paginate/CLAUDE.md +0 -18
  133. package/skills/gh-paginate/SKILL.md +0 -84
  134. package/skills/gotcha/CLAUDE.md +0 -33
  135. package/skills/gotcha/SKILL.md +0 -73
  136. package/skills/implement/CLAUDE.md +0 -27
  137. package/skills/implement/SKILL.md +0 -66
  138. package/skills/implement/scripts/CLAUDE.md +0 -22
  139. package/skills/implement/scripts/append_note.py +0 -133
  140. package/skills/implement/scripts/implement_scripts_constants/CLAUDE.md +0 -22
  141. package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
  142. package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +0 -12
  143. package/skills/implement/scripts/test_append_note.py +0 -191
  144. package/skills/log-audit/CLAUDE.md +0 -20
  145. package/skills/log-audit/SKILL.md +0 -66
  146. package/skills/log-audit/reference/CLAUDE.md +0 -9
  147. package/skills/log-audit/reference/charter.md +0 -52
  148. package/skills/log-audit/scripts/CLAUDE.md +0 -27
  149. package/skills/log-audit/scripts/cluster_recurrences.py +0 -261
  150. package/skills/log-audit/scripts/collect_log_window.py +0 -199
  151. package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +0 -12
  152. package/skills/log-audit/scripts/log_audit_constants/__init__.py +0 -0
  153. package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +0 -23
  154. package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +0 -24
  155. package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +0 -49
  156. package/skills/log-audit/scripts/mine_copilot_findings.py +0 -302
  157. package/skills/log-audit/scripts/test_cluster_recurrences.py +0 -160
  158. package/skills/log-audit/scripts/test_collect_log_window.py +0 -111
  159. package/skills/log-audit/scripts/test_mine_copilot_findings.py +0 -126
  160. package/skills/logifix/CLAUDE.md +0 -36
  161. package/skills/logifix/SKILL.md +0 -69
  162. package/skills/logifix/scripts/CLAUDE.md +0 -16
  163. package/skills/logifix/scripts/logifix.ps1 +0 -205
  164. package/skills/monitor-open-prs/CLAUDE.md +0 -34
  165. package/skills/monitor-open-prs/SKILL.md +0 -88
  166. package/skills/monitor-open-prs/scripts/CLAUDE.md +0 -17
  167. package/skills/monitor-open-prs/scripts/discover_open_prs.py +0 -69
  168. package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +0 -149
  169. package/skills/monitor-open-prs/test_skill_contract.py +0 -27
  170. package/skills/post-audit-findings/SKILL.md +0 -80
  171. package/skills/pr-consistency-audit/CLAUDE.md +0 -34
  172. package/skills/pr-consistency-audit/SKILL.md +0 -116
  173. package/skills/pr-consistency-audit/reference/CLAUDE.md +0 -16
  174. package/skills/pr-consistency-audit/reference/detection-rules.md +0 -96
  175. package/skills/pr-consistency-audit/reference/illustrations.md +0 -78
  176. package/skills/pr-fix-protocol/SKILL.md +0 -65
  177. package/skills/pr-review-responder/CLAUDE.md +0 -35
  178. package/skills/pr-review-responder/EXAMPLES.md +0 -590
  179. package/skills/pr-review-responder/PRINCIPLES.md +0 -533
  180. package/skills/pr-review-responder/README.md +0 -168
  181. package/skills/pr-review-responder/SKILL.md +0 -203
  182. package/skills/pr-review-responder/TESTING.md +0 -350
  183. package/skills/pr-scope-resolve/SKILL.md +0 -45
  184. package/skills/pre-compact/CLAUDE.md +0 -24
  185. package/skills/pre-compact/SKILL.md +0 -134
  186. package/skills/qbug/CLAUDE.md +0 -40
  187. package/skills/qbug/SKILL.md +0 -387
  188. package/skills/qbug/test_qbug_skill_audit_schema.py +0 -152
  189. package/skills/qbug/test_qbug_skill_post_fix_audit.py +0 -103
  190. package/skills/refine/CLAUDE.md +0 -44
  191. package/skills/refine/SKILL.md +0 -262
  192. package/skills/refine/templates/CLAUDE.md +0 -17
  193. package/skills/refine/templates/implementation-notes-template.html +0 -56
  194. package/skills/refine/templates/plan-template.md +0 -60
  195. package/skills/research-mode/CLAUDE.md +0 -35
  196. package/skills/research-mode/SKILL.md +0 -53
  197. package/skills/structure-prompt/CLAUDE.md +0 -42
  198. package/skills/structure-prompt/SKILL.md +0 -44
  199. package/skills/structure-prompt/reference/CLAUDE.md +0 -28
  200. package/skills/structure-prompt/reference/adversarial-tuning.md +0 -62
  201. package/skills/structure-prompt/reference/block-classification.md +0 -27
  202. package/skills/structure-prompt/reference/canonical-case.md +0 -48
  203. package/skills/structure-prompt/reference/citation-depth.md +0 -70
  204. package/skills/structure-prompt/reference/cleanup.md +0 -33
  205. package/skills/structure-prompt/reference/constraints.md +0 -33
  206. package/skills/structure-prompt/reference/directives.md +0 -37
  207. package/skills/structure-prompt/reference/examples.md +0 -72
  208. package/skills/structure-prompt/reference/instantiation.md +0 -51
  209. package/skills/structure-prompt/reference/output-contract.md +0 -72
  210. package/skills/structure-prompt/reference/per-category.md +0 -23
  211. package/skills/structure-prompt/reference/persona.md +0 -38
  212. package/skills/structure-prompt/reference/research.md +0 -33
  213. package/skills/structure-prompt/reference/structure.md +0 -28
  214. package/skills/verified-build/CLAUDE.md +0 -33
  215. package/skills/verified-build/SKILL.md +0 -33
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env pwsh
2
+ [CmdletBinding()]
3
+ param(
4
+ [switch]$AsJson
5
+ )
6
+
7
+ $noiseEmailPatterns = @(
8
+ '@example\.(com|org|net)$',
9
+ '(@|\.)sentry\.io$',
10
+ '@anthropic\.',
11
+ '^Claude@\d'
12
+ )
13
+
14
+ $isoLatin1CodePage = 28591
15
+
16
+ $profileStorageDirectoryNames = @('sentry', 'IndexedDB', 'Session Storage', 'WebStorage')
17
+
18
+ $emailAddressPattern = '(?<![a-zA-Z0-9._%+-])[a-zA-Z0-9][a-zA-Z0-9._%+-]*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+(?![a-zA-Z0-9-])'
19
+
20
+ $maximumStorageFileBytes = 50MB
21
+
22
+ function Test-NoiseEmail {
23
+ param([string]$Email)
24
+ foreach ($pattern in $noiseEmailPatterns) {
25
+ if ($Email -imatch $pattern) { return $true }
26
+ }
27
+ return $false
28
+ }
29
+
30
+ function Get-EncodingAgnosticTextView {
31
+ param([string]$FilePath)
32
+ try {
33
+ $fileBytes = [System.IO.File]::ReadAllBytes($FilePath)
34
+ if ($fileBytes.Length -eq 0) { return @() }
35
+ $isoLatin1Encoding = [System.Text.Encoding]::GetEncoding($isoLatin1CodePage)
36
+ return @(
37
+ $isoLatin1Encoding.GetString($fileBytes),
38
+ [System.Text.Encoding]::Unicode.GetString($fileBytes)
39
+ )
40
+ }
41
+ catch {
42
+ return @()
43
+ }
44
+ }
45
+
46
+ function Get-EmailCandidatesFromProfile {
47
+ param(
48
+ [string]$ProfileDirectory,
49
+ [string]$ExcludeEmail
50
+ )
51
+ $scanDirectories = $profileStorageDirectoryNames |
52
+ ForEach-Object { Join-Path $ProfileDirectory $_ } |
53
+ Where-Object { Test-Path -LiteralPath $_ }
54
+ $foundEmails = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
55
+
56
+ foreach ($scanDirectory in $scanDirectories) {
57
+ $candidateFiles = Get-ChildItem -LiteralPath $scanDirectory -Recurse -File -ErrorAction SilentlyContinue |
58
+ Where-Object { $_.Length -lt $maximumStorageFileBytes }
59
+ foreach ($candidateFile in $candidateFiles) {
60
+ $decodedTextViews = Get-EncodingAgnosticTextView -FilePath $candidateFile.FullName
61
+ foreach ($decodedText in $decodedTextViews) {
62
+ foreach ($match in [regex]::Matches($decodedText, $emailAddressPattern)) {
63
+ $candidateEmail = $match.Value
64
+ $isKnownCliEmail = $ExcludeEmail -and ($candidateEmail -ieq $ExcludeEmail)
65
+ if (-not (Test-NoiseEmail -Email $candidateEmail) -and -not $isKnownCliEmail) {
66
+ [void]$foundEmails.Add($candidateEmail)
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }
72
+ return $foundEmails
73
+ }
74
+
75
+ function Write-AccountResult {
76
+ param(
77
+ [string]$Email,
78
+ [string]$AccountUuid,
79
+ [string]$Source,
80
+ [string]$CliEmail,
81
+ [string]$CliAccountUuid,
82
+ [switch]$AsJson
83
+ )
84
+ if ($AsJson) {
85
+ [pscustomobject]@{
86
+ account = $Email
87
+ accountUuid = $AccountUuid
88
+ source = $Source
89
+ cliAccount = $CliEmail
90
+ cliAccountUuid = $CliAccountUuid
91
+ } | ConvertTo-Json
92
+ return
93
+ }
94
+ Write-Output "Account: $Email"
95
+ Write-Output "AccountUuid: $AccountUuid"
96
+ Write-Output "Source: $Source"
97
+ if ($CliEmail) {
98
+ Write-Output "CliAccount: $CliEmail"
99
+ Write-Output "CliAccountUuid: $CliAccountUuid"
100
+ }
101
+ }
102
+
103
+ function Get-SessionAccountResult {
104
+ param(
105
+ [int]$ExitCode,
106
+ [string]$Email = $null,
107
+ [string]$AccountUuid = $null,
108
+ [string]$Source = $null,
109
+ [string]$CliEmail = $null,
110
+ [string]$CliAccountUuid = $null,
111
+ [string]$ErrorMessage = $null
112
+ )
113
+ [pscustomobject]@{
114
+ ExitCode = $ExitCode
115
+ Email = $Email
116
+ AccountUuid = $AccountUuid
117
+ Source = $Source
118
+ CliEmail = $CliEmail
119
+ CliAccountUuid = $CliAccountUuid
120
+ ErrorMessage = $ErrorMessage
121
+ }
122
+ }
123
+
124
+ function Resolve-SessionAccount {
125
+ param(
126
+ [string]$CliConfigPath,
127
+ [string]$ProfileDirectory
128
+ )
129
+ if (-not (Test-Path -LiteralPath $CliConfigPath)) {
130
+ return Get-SessionAccountResult -ExitCode 2 -ErrorMessage "CLI config not found: $CliConfigPath"
131
+ }
132
+
133
+ try {
134
+ $cliConfig = Get-Content -LiteralPath $CliConfigPath -Raw -ErrorAction Stop | ConvertFrom-Json -AsHashtable -ErrorAction Stop
135
+ }
136
+ catch {
137
+ return Get-SessionAccountResult -ExitCode 2 -ErrorMessage "Failed to parse CLI config as JSON: $CliConfigPath"
138
+ }
139
+
140
+ if ($cliConfig -isnot [System.Collections.IDictionary]) {
141
+ return Get-SessionAccountResult -ExitCode 2 -ErrorMessage "CLI config top level is not a JSON object: $CliConfigPath"
142
+ }
143
+
144
+ $oauthAccount = $cliConfig['oauthAccount']
145
+ $cliEmail = if ($oauthAccount) { $oauthAccount['emailAddress'] } else { $null }
146
+ $cliAccountUuid = if ($oauthAccount) { $oauthAccount['accountUuid'] } else { $null }
147
+ if (-not $cliEmail -or -not $cliAccountUuid) {
148
+ return Get-SessionAccountResult -ExitCode 2 -ErrorMessage "CLI config is missing oauthAccount.emailAddress or oauthAccount.accountUuid: $CliConfigPath"
149
+ }
150
+
151
+ if (-not $ProfileDirectory -or -not (Test-Path -LiteralPath $ProfileDirectory)) {
152
+ return Get-SessionAccountResult -ExitCode 0 -Email $cliEmail -AccountUuid $cliAccountUuid -Source 'cli-config'
153
+ }
154
+
155
+ $profileConfigPath = Join-Path $ProfileDirectory 'config.json'
156
+ if (-not (Test-Path -LiteralPath $profileConfigPath)) {
157
+ return Get-SessionAccountResult -ExitCode 2 -ErrorMessage "Desktop profile config not found: $profileConfigPath"
158
+ }
159
+
160
+ try {
161
+ $profileConfig = Get-Content -LiteralPath $profileConfigPath -Raw -ErrorAction Stop | ConvertFrom-Json -AsHashtable -ErrorAction Stop
162
+ }
163
+ catch {
164
+ return Get-SessionAccountResult -ExitCode 2 -ErrorMessage "Failed to parse desktop profile config as JSON: $profileConfigPath"
165
+ }
166
+
167
+ if ($profileConfig -isnot [System.Collections.IDictionary]) {
168
+ return Get-SessionAccountResult -ExitCode 2 -ErrorMessage "Desktop profile config top level is not a JSON object: $profileConfigPath"
169
+ }
170
+
171
+ $profileAccountUuid = $profileConfig['lastKnownAccountUuid']
172
+ if (-not $profileAccountUuid) {
173
+ return Get-SessionAccountResult -ExitCode 2 -ErrorMessage "Desktop profile config is missing lastKnownAccountUuid: $profileConfigPath"
174
+ }
175
+
176
+ if ($profileAccountUuid -eq $cliAccountUuid) {
177
+ return Get-SessionAccountResult -ExitCode 0 -Email $cliEmail -AccountUuid $cliAccountUuid -Source 'desktop-profile (matches cli-config)'
178
+ }
179
+
180
+ $candidateEmails = @(Get-EmailCandidatesFromProfile -ProfileDirectory $ProfileDirectory -ExcludeEmail $cliEmail)
181
+ if ($candidateEmails.Count -eq 1) {
182
+ return Get-SessionAccountResult -ExitCode 0 -Email $candidateEmails[0] -AccountUuid $profileAccountUuid -Source 'desktop-profile (differs from cli-config)' -CliEmail $cliEmail -CliAccountUuid $cliAccountUuid
183
+ }
184
+
185
+ if ($candidateEmails.Count -eq 0) {
186
+ return Get-SessionAccountResult -ExitCode 1 -ErrorMessage "Desktop profile account ($profileAccountUuid) differs from CLI config, but no candidate email was recovered from profile storage: $ProfileDirectory"
187
+ }
188
+
189
+ return Get-SessionAccountResult -ExitCode 1 -ErrorMessage "Desktop profile account ($profileAccountUuid) differs from CLI config, and multiple candidate emails were found in profile storage: $($candidateEmails -join ', ')"
190
+ }
191
+
192
+ function Invoke-GetSessionAccount {
193
+ param(
194
+ [switch]$AsJson
195
+ )
196
+ $cliConfigPath = Join-Path $HOME '.claude.json'
197
+ $sessionAccount = Resolve-SessionAccount -CliConfigPath $cliConfigPath -ProfileDirectory $env:CLAUDE_USER_DATA_DIR
198
+ if ($sessionAccount.ErrorMessage) {
199
+ Write-Error $sessionAccount.ErrorMessage -ErrorAction Continue
200
+ exit $sessionAccount.ExitCode
201
+ }
202
+ Write-AccountResult -Email $sessionAccount.Email -AccountUuid $sessionAccount.AccountUuid -Source $sessionAccount.Source -CliEmail $sessionAccount.CliEmail -CliAccountUuid $sessionAccount.CliAccountUuid -AsJson:$AsJson
203
+ exit $sessionAccount.ExitCode
204
+ }
205
+
206
+ if ($MyInvocation.InvocationName -ne '.') {
207
+ Set-StrictMode -Version Latest
208
+ $ErrorActionPreference = 'Stop'
209
+ Invoke-GetSessionAccount -AsJson:$AsJson
210
+ }
@@ -1,6 +1,6 @@
1
1
  # scripts/tests
2
2
 
3
- pytest suite for the Python scripts in `scripts/`.
3
+ pytest suite for the Python scripts and Pester suite for the PowerShell scripts in `scripts/`.
4
4
 
5
5
  ## Test files
6
6
 
@@ -11,8 +11,22 @@ pytest suite for the Python scripts in `scripts/`.
11
11
  | `test_sweep_empty_dirs.py` | `sweep_empty_dirs.py` — age check, one-shot mode, and continuous-watch behavior |
12
12
  | `test_sync_to_cursor.py` | `sync_to_cursor/` package — mapping, hashing, manifest, and path resolution |
13
13
 
14
+ ## PowerShell test files
15
+
16
+ | File | Covers |
17
+ |---|---|
18
+ | `Get-SessionAccount.Tests.ps1` | `Get-SessionAccount.ps1` — noise filtering, profile-storage email recovery, CLI-vs-desktop account resolution, and output formatting |
19
+
14
20
  ## Running
15
21
 
22
+ Python scripts (pytest):
23
+
16
24
  ```bash
17
25
  python -m pytest packages/claude-dev-env/scripts/tests/
18
26
  ```
27
+
28
+ PowerShell scripts (Pester 5+, `*.Tests.ps1`):
29
+
30
+ ```powershell
31
+ Invoke-Pester -Path packages/claude-dev-env/scripts/tests/
32
+ ```
@@ -0,0 +1,350 @@
1
+ Set-StrictMode -Version Latest
2
+
3
+ BeforeAll {
4
+ $scriptUnderTest = Join-Path (Split-Path -Parent $PSScriptRoot) 'Get-SessionAccount.ps1'
5
+ . $scriptUnderTest
6
+
7
+ function New-Utf16StorageFile {
8
+ param(
9
+ [string]$ProfileDirectory,
10
+ [string]$StoreName,
11
+ [string]$FileName,
12
+ [string]$EmailText
13
+ )
14
+ $storeDirectory = Join-Path $ProfileDirectory $StoreName
15
+ New-Item -ItemType Directory -Path $storeDirectory -Force | Out-Null
16
+ $emailBytes = [System.Text.Encoding]::Unicode.GetBytes("`0`0$EmailText`0`0")
17
+ $filePath = Join-Path $storeDirectory $FileName
18
+ [System.IO.File]::WriteAllBytes($filePath, $emailBytes)
19
+ }
20
+
21
+ function New-Utf8StorageFile {
22
+ param(
23
+ [string]$ProfileDirectory,
24
+ [string]$StoreName,
25
+ [string]$FileName,
26
+ [string]$JsonText
27
+ )
28
+ $storeDirectory = Join-Path $ProfileDirectory $StoreName
29
+ New-Item -ItemType Directory -Path $storeDirectory -Force | Out-Null
30
+ $filePath = Join-Path $storeDirectory $FileName
31
+ [System.IO.File]::WriteAllText($filePath, $JsonText, [System.Text.Encoding]::UTF8)
32
+ }
33
+
34
+ function New-CliConfigFile {
35
+ param(
36
+ [string]$Directory,
37
+ [string]$Email,
38
+ [string]$AccountUuid
39
+ )
40
+ New-Item -ItemType Directory -Path $Directory -Force | Out-Null
41
+ $cliConfigPath = Join-Path $Directory '.claude.json'
42
+ $cliConfigJson = [pscustomobject]@{
43
+ oauthAccount = [pscustomobject]@{
44
+ emailAddress = $Email
45
+ accountUuid = $AccountUuid
46
+ }
47
+ } | ConvertTo-Json
48
+ [System.IO.File]::WriteAllText($cliConfigPath, $cliConfigJson, [System.Text.Encoding]::UTF8)
49
+ return $cliConfigPath
50
+ }
51
+
52
+ function New-ProfileConfigFile {
53
+ param(
54
+ [string]$ProfileDirectory,
55
+ [string]$LastKnownAccountUuid
56
+ )
57
+ New-Item -ItemType Directory -Path $ProfileDirectory -Force | Out-Null
58
+ $profileConfigPath = Join-Path $ProfileDirectory 'config.json'
59
+ $profileConfigJson = [pscustomobject]@{
60
+ lastKnownAccountUuid = $LastKnownAccountUuid
61
+ } | ConvertTo-Json
62
+ [System.IO.File]::WriteAllText($profileConfigPath, $profileConfigJson, [System.Text.Encoding]::UTF8)
63
+ }
64
+ }
65
+
66
+ Describe 'Test-NoiseEmail' {
67
+ It 'rejects the placeholder example domain' {
68
+ (Test-NoiseEmail -Email 'noreply@example.com') | Should -Be $true
69
+ }
70
+
71
+ It 'keeps a legitimate local part that contains the substring example' {
72
+ (Test-NoiseEmail -Email 'sam.exampleson@company.com') | Should -Be $false
73
+ }
74
+
75
+ It 'keeps a legitimate domain that merely contains the substring example' {
76
+ (Test-NoiseEmail -Email 'person@example-labs.io') | Should -Be $false
77
+ }
78
+
79
+ It 'still rejects sentry and anthropic noise addresses' {
80
+ (Test-NoiseEmail -Email 'someone@sentry.io') | Should -Be $true
81
+ (Test-NoiseEmail -Email 'billing@anthropic.com') | Should -Be $true
82
+ }
83
+
84
+ It 'rejects a sentry ingest subdomain address' {
85
+ (Test-NoiseEmail -Email 'abc@o123.ingest.sentry.io') | Should -Be $true
86
+ }
87
+
88
+ It 'keeps an address whose local part merely contains sentry.io' {
89
+ (Test-NoiseEmail -Email 'sentry.io.reports@realcompany.com') | Should -Be $false
90
+ }
91
+
92
+ It 'keeps an address whose domain merely starts with anthropic' {
93
+ (Test-NoiseEmail -Email 'user@anthropic-partner.com') | Should -Be $false
94
+ }
95
+ }
96
+
97
+ Describe 'Get-EmailCandidatesFromProfile' {
98
+ It 'recovers an email persisted as UTF-16 in Session Storage' {
99
+ $profileDirectory = Join-Path $TestDrive 'utf16-profile'
100
+ New-Utf16StorageFile -ProfileDirectory $profileDirectory -StoreName 'Session Storage' -FileName '000003.ldb' -EmailText 'user@host.com'
101
+
102
+ $candidateEmails = @(Get-EmailCandidatesFromProfile -ProfileDirectory $profileDirectory)
103
+
104
+ ($candidateEmails -contains 'user@host.com') | Should -Be $true
105
+ }
106
+
107
+ It 'recovers an email stored as UTF-8 JSON in the sentry store' {
108
+ $profileDirectory = Join-Path $TestDrive 'utf8-profile'
109
+ New-Utf8StorageFile -ProfileDirectory $profileDirectory -StoreName 'sentry' -FileName 'events.json' -JsonText '{"user":{"email":"person@company.com"}}'
110
+
111
+ $candidateEmails = @(Get-EmailCandidatesFromProfile -ProfileDirectory $profileDirectory)
112
+
113
+ ($candidateEmails -contains 'person@company.com') | Should -Be $true
114
+ }
115
+
116
+ It 'keeps a single UTF-16 email whose local part contains the substring example' {
117
+ $profileDirectory = Join-Path $TestDrive 'example-profile'
118
+ New-Utf16StorageFile -ProfileDirectory $profileDirectory -StoreName 'Session Storage' -FileName '000004.ldb' -EmailText 'sam.exampleson@company.com'
119
+
120
+ $candidateEmails = @(Get-EmailCandidatesFromProfile -ProfileDirectory $profileDirectory)
121
+
122
+ $candidateEmails.Count | Should -Be 1
123
+ $candidateEmails[0] | Should -Be 'sam.exampleson@company.com'
124
+ }
125
+
126
+ It 'counts the same email recovered with differing casing across stores as one candidate' {
127
+ $profileDirectory = Join-Path $TestDrive 'mixed-case-profile'
128
+ New-Utf16StorageFile -ProfileDirectory $profileDirectory -StoreName 'Session Storage' -FileName '000012.ldb' -EmailText 'Desktop@Company.com'
129
+ New-Utf16StorageFile -ProfileDirectory $profileDirectory -StoreName 'IndexedDB' -FileName '000013.ldb' -EmailText 'desktop@company.com'
130
+
131
+ $candidateEmails = @(Get-EmailCandidatesFromProfile -ProfileDirectory $profileDirectory)
132
+
133
+ $candidateEmails.Count | Should -Be 1
134
+ }
135
+
136
+ It 'refuses to recover an email whose local part is glued to an adjacent boundary byte' {
137
+ $profileDirectory = Join-Path $TestDrive 'prefix-polluted-profile'
138
+ New-Utf16StorageFile -ProfileDirectory $profileDirectory -StoreName 'Session Storage' -FileName '000010.ldb' -EmailText '.desktop@company.com'
139
+
140
+ $candidateEmails = @(Get-EmailCandidatesFromProfile -ProfileDirectory $profileDirectory)
141
+
142
+ ($candidateEmails -contains '.desktop@company.com') | Should -Be $false
143
+ $candidateEmails.Count | Should -Be 0
144
+ }
145
+ }
146
+
147
+ Describe 'Resolve-SessionAccount' {
148
+ It 'returns exit code 2 when the CLI config file is absent' {
149
+ $cliConfigPath = Join-Path $TestDrive 'absent-cli/.claude.json'
150
+
151
+ $sessionAccount = Resolve-SessionAccount -CliConfigPath $cliConfigPath -ProfileDirectory ''
152
+
153
+ $sessionAccount.ExitCode | Should -Be 2
154
+ $sessionAccount.ErrorMessage | Should -Match 'CLI config not found'
155
+ }
156
+
157
+ It 'returns exit code 2 when the CLI config is not valid JSON' {
158
+ $configDirectory = Join-Path $TestDrive 'unparseable-cli'
159
+ New-Item -ItemType Directory -Path $configDirectory -Force | Out-Null
160
+ $cliConfigPath = Join-Path $configDirectory '.claude.json'
161
+ [System.IO.File]::WriteAllText($cliConfigPath, 'this is not json {', [System.Text.Encoding]::UTF8)
162
+
163
+ $sessionAccount = Resolve-SessionAccount -CliConfigPath $cliConfigPath -ProfileDirectory ''
164
+
165
+ $sessionAccount.ExitCode | Should -Be 2
166
+ $sessionAccount.ErrorMessage | Should -Match 'Failed to parse CLI config'
167
+ }
168
+
169
+ It 'returns exit code 2 when the CLI config top level is a JSON string' {
170
+ $configDirectory = Join-Path $TestDrive 'scalar-cli'
171
+ New-Item -ItemType Directory -Path $configDirectory -Force | Out-Null
172
+ $cliConfigPath = Join-Path $configDirectory '.claude.json'
173
+ [System.IO.File]::WriteAllText($cliConfigPath, '"just a string"', [System.Text.Encoding]::UTF8)
174
+
175
+ $sessionAccount = Resolve-SessionAccount -CliConfigPath $cliConfigPath -ProfileDirectory ''
176
+
177
+ $sessionAccount.ExitCode | Should -Be 2
178
+ $sessionAccount.ErrorMessage | Should -Match 'not a JSON object'
179
+ }
180
+
181
+ It 'returns exit code 2 when the CLI config top level is a JSON array' {
182
+ $configDirectory = Join-Path $TestDrive 'array-cli'
183
+ New-Item -ItemType Directory -Path $configDirectory -Force | Out-Null
184
+ $cliConfigPath = Join-Path $configDirectory '.claude.json'
185
+ [System.IO.File]::WriteAllText($cliConfigPath, '[1, 2, 3]', [System.Text.Encoding]::UTF8)
186
+
187
+ $sessionAccount = Resolve-SessionAccount -CliConfigPath $cliConfigPath -ProfileDirectory ''
188
+
189
+ $sessionAccount.ExitCode | Should -Be 2
190
+ $sessionAccount.ErrorMessage | Should -Match 'not a JSON object'
191
+ }
192
+
193
+ It 'returns exit code 2 when the desktop profile config top level is not a JSON object' {
194
+ $cliConfigPath = New-CliConfigFile -Directory (Join-Path $TestDrive 'scalar-profile-cli') -Email 'cli@company.com' -AccountUuid 'uuid-cli'
195
+ $profileDirectory = Join-Path $TestDrive 'scalar-profile'
196
+ New-Item -ItemType Directory -Path $profileDirectory -Force | Out-Null
197
+ $profileConfigPath = Join-Path $profileDirectory 'config.json'
198
+ [System.IO.File]::WriteAllText($profileConfigPath, '42', [System.Text.Encoding]::UTF8)
199
+
200
+ $sessionAccount = Resolve-SessionAccount -CliConfigPath $cliConfigPath -ProfileDirectory $profileDirectory
201
+
202
+ $sessionAccount.ExitCode | Should -Be 2
203
+ $sessionAccount.ErrorMessage | Should -Match 'not a JSON object'
204
+ }
205
+
206
+ It 'returns exit code 2 when the CLI config lacks oauthAccount fields' {
207
+ $configDirectory = Join-Path $TestDrive 'incomplete-cli'
208
+ New-Item -ItemType Directory -Path $configDirectory -Force | Out-Null
209
+ $cliConfigPath = Join-Path $configDirectory '.claude.json'
210
+ [System.IO.File]::WriteAllText($cliConfigPath, '{"oauthAccount":{"emailAddress":"","accountUuid":""}}', [System.Text.Encoding]::UTF8)
211
+
212
+ $sessionAccount = Resolve-SessionAccount -CliConfigPath $cliConfigPath -ProfileDirectory ''
213
+
214
+ $sessionAccount.ExitCode | Should -Be 2
215
+ $sessionAccount.ErrorMessage | Should -Match 'missing oauthAccount'
216
+ }
217
+
218
+ It 'reports the cli-config account with exit code 0 when no desktop profile is set' {
219
+ $cliConfigPath = New-CliConfigFile -Directory (Join-Path $TestDrive 'cli-only') -Email 'cli@company.com' -AccountUuid 'uuid-cli'
220
+
221
+ $sessionAccount = Resolve-SessionAccount -CliConfigPath $cliConfigPath -ProfileDirectory ''
222
+
223
+ $sessionAccount.ExitCode | Should -Be 0
224
+ $sessionAccount.Source | Should -Be 'cli-config'
225
+ $sessionAccount.Email | Should -Be 'cli@company.com'
226
+ }
227
+
228
+ It 'returns exit code 2 when the desktop profile config is absent' {
229
+ $cliConfigPath = New-CliConfigFile -Directory (Join-Path $TestDrive 'missing-profile-cli') -Email 'cli@company.com' -AccountUuid 'uuid-cli'
230
+ $profileDirectory = Join-Path $TestDrive 'profile-without-config'
231
+ New-Item -ItemType Directory -Path $profileDirectory -Force | Out-Null
232
+
233
+ $sessionAccount = Resolve-SessionAccount -CliConfigPath $cliConfigPath -ProfileDirectory $profileDirectory
234
+
235
+ $sessionAccount.ExitCode | Should -Be 2
236
+ $sessionAccount.ErrorMessage | Should -Match 'Desktop profile config not found'
237
+ }
238
+
239
+ It 'reports the matching desktop-profile account with exit code 0' {
240
+ $cliConfigPath = New-CliConfigFile -Directory (Join-Path $TestDrive 'match-cli') -Email 'cli@company.com' -AccountUuid 'uuid-shared'
241
+ $profileDirectory = Join-Path $TestDrive 'match-profile'
242
+ New-ProfileConfigFile -ProfileDirectory $profileDirectory -LastKnownAccountUuid 'uuid-shared'
243
+
244
+ $sessionAccount = Resolve-SessionAccount -CliConfigPath $cliConfigPath -ProfileDirectory $profileDirectory
245
+
246
+ $sessionAccount.ExitCode | Should -Be 0
247
+ $sessionAccount.Source | Should -Be 'desktop-profile (matches cli-config)'
248
+ $sessionAccount.Email | Should -Be 'cli@company.com'
249
+ }
250
+
251
+ It 'recovers the desktop account email with exit code 0 when exactly one candidate differs from the cli account' {
252
+ $cliConfigPath = New-CliConfigFile -Directory (Join-Path $TestDrive 'differ-cli') -Email 'cli@company.com' -AccountUuid 'uuid-cli'
253
+ $profileDirectory = Join-Path $TestDrive 'differ-profile'
254
+ New-ProfileConfigFile -ProfileDirectory $profileDirectory -LastKnownAccountUuid 'uuid-desktop'
255
+ New-Utf16StorageFile -ProfileDirectory $profileDirectory -StoreName 'Session Storage' -FileName '000005.ldb' -EmailText 'desktop@company.com'
256
+
257
+ $sessionAccount = Resolve-SessionAccount -CliConfigPath $cliConfigPath -ProfileDirectory $profileDirectory
258
+
259
+ $sessionAccount.ExitCode | Should -Be 0
260
+ $sessionAccount.Email | Should -Be 'desktop@company.com'
261
+ $sessionAccount.AccountUuid | Should -Be 'uuid-desktop'
262
+ $sessionAccount.Source | Should -Be 'desktop-profile (differs from cli-config)'
263
+ $sessionAccount.CliEmail | Should -Be 'cli@company.com'
264
+ }
265
+
266
+ It 'recovers the desktop email even when the known cli email also lingers in profile storage' {
267
+ $cliConfigPath = New-CliConfigFile -Directory (Join-Path $TestDrive 'lingering-cli') -Email 'cli@company.com' -AccountUuid 'uuid-cli'
268
+ $profileDirectory = Join-Path $TestDrive 'lingering-profile'
269
+ New-ProfileConfigFile -ProfileDirectory $profileDirectory -LastKnownAccountUuid 'uuid-desktop'
270
+ New-Utf16StorageFile -ProfileDirectory $profileDirectory -StoreName 'Session Storage' -FileName '000008.ldb' -EmailText 'desktop@company.com'
271
+ New-Utf16StorageFile -ProfileDirectory $profileDirectory -StoreName 'IndexedDB' -FileName '000009.ldb' -EmailText 'cli@company.com'
272
+
273
+ $sessionAccount = Resolve-SessionAccount -CliConfigPath $cliConfigPath -ProfileDirectory $profileDirectory
274
+
275
+ $sessionAccount.ExitCode | Should -Be 0
276
+ $sessionAccount.Email | Should -Be 'desktop@company.com'
277
+ $sessionAccount.AccountUuid | Should -Be 'uuid-desktop'
278
+ $sessionAccount.CliEmail | Should -Be 'cli@company.com'
279
+ }
280
+
281
+ It 'fails safe with exit code 1 rather than reporting a prefix-polluted email as the account' {
282
+ $cliConfigPath = New-CliConfigFile -Directory (Join-Path $TestDrive 'polluted-cli') -Email 'cli@company.com' -AccountUuid 'uuid-cli'
283
+ $profileDirectory = Join-Path $TestDrive 'polluted-resolve-profile'
284
+ New-ProfileConfigFile -ProfileDirectory $profileDirectory -LastKnownAccountUuid 'uuid-desktop'
285
+ New-Utf16StorageFile -ProfileDirectory $profileDirectory -StoreName 'Session Storage' -FileName '000011.ldb' -EmailText '.desktop@company.com'
286
+
287
+ $sessionAccount = Resolve-SessionAccount -CliConfigPath $cliConfigPath -ProfileDirectory $profileDirectory
288
+
289
+ $sessionAccount.ExitCode | Should -Be 1
290
+ $sessionAccount.Email | Should -BeNullOrEmpty
291
+ $sessionAccount.ErrorMessage | Should -Match 'no candidate email was recovered'
292
+ }
293
+
294
+ It 'returns exit code 1 when the desktop account differs but no candidate email is recovered' {
295
+ $cliConfigPath = New-CliConfigFile -Directory (Join-Path $TestDrive 'none-cli') -Email 'cli@company.com' -AccountUuid 'uuid-cli'
296
+ $profileDirectory = Join-Path $TestDrive 'none-profile'
297
+ New-ProfileConfigFile -ProfileDirectory $profileDirectory -LastKnownAccountUuid 'uuid-desktop'
298
+
299
+ $sessionAccount = Resolve-SessionAccount -CliConfigPath $cliConfigPath -ProfileDirectory $profileDirectory
300
+
301
+ $sessionAccount.ExitCode | Should -Be 1
302
+ $sessionAccount.ErrorMessage | Should -Match 'no candidate email was recovered'
303
+ }
304
+
305
+ It 'returns exit code 1 when the desktop account differs and multiple candidate emails are found' {
306
+ $cliConfigPath = New-CliConfigFile -Directory (Join-Path $TestDrive 'multi-cli') -Email 'cli@company.com' -AccountUuid 'uuid-cli'
307
+ $profileDirectory = Join-Path $TestDrive 'multi-profile'
308
+ New-ProfileConfigFile -ProfileDirectory $profileDirectory -LastKnownAccountUuid 'uuid-desktop'
309
+ New-Utf16StorageFile -ProfileDirectory $profileDirectory -StoreName 'Session Storage' -FileName '000006.ldb' -EmailText 'first@company.com'
310
+ New-Utf16StorageFile -ProfileDirectory $profileDirectory -StoreName 'IndexedDB' -FileName '000007.ldb' -EmailText 'second@company.com'
311
+
312
+ $sessionAccount = Resolve-SessionAccount -CliConfigPath $cliConfigPath -ProfileDirectory $profileDirectory
313
+
314
+ $sessionAccount.ExitCode | Should -Be 1
315
+ $sessionAccount.ErrorMessage | Should -Match 'multiple candidate emails'
316
+ }
317
+ }
318
+
319
+ Describe 'Write-AccountResult' {
320
+ It 'writes the account fields as text without cli lines when no CliEmail is given' {
321
+ $textLines = Write-AccountResult -Email 'a@company.com' -AccountUuid 'uuid-a' -Source 'cli-config'
322
+ $joinedText = $textLines -join "`n"
323
+
324
+ $joinedText | Should -Match 'Account: a@company.com'
325
+ $joinedText | Should -Match 'AccountUuid: uuid-a'
326
+ $joinedText | Should -Match 'Source: cli-config'
327
+ $joinedText | Should -Not -Match 'CliAccount:'
328
+ }
329
+
330
+ It 'writes the cli account lines as text when a CliEmail is given' {
331
+ $textLines = Write-AccountResult -Email 'd@company.com' -AccountUuid 'uuid-d' -Source 'desktop-profile (differs from cli-config)' -CliEmail 'c@company.com' -CliAccountUuid 'uuid-c'
332
+ $joinedText = $textLines -join "`n"
333
+
334
+ $joinedText | Should -Match 'Account: d@company.com'
335
+ $joinedText | Should -Match 'CliAccount: c@company.com'
336
+ $joinedText | Should -Match 'CliAccountUuid: uuid-c'
337
+ }
338
+
339
+ It 'emits a JSON object carrying the account contract fields when AsJson is set' {
340
+ $AsJson = $true
341
+ $jsonOutput = Write-AccountResult -Email 'd@company.com' -AccountUuid 'uuid-d' -Source 'desktop-profile (differs from cli-config)' -CliEmail 'c@company.com' -CliAccountUuid 'uuid-c' -AsJson:$AsJson
342
+ $parsedResult = $jsonOutput | ConvertFrom-Json
343
+
344
+ $parsedResult.account | Should -Be 'd@company.com'
345
+ $parsedResult.accountUuid | Should -Be 'uuid-d'
346
+ $parsedResult.source | Should -Be 'desktop-profile (differs from cli-config)'
347
+ $parsedResult.cliAccount | Should -Be 'c@company.com'
348
+ $parsedResult.cliAccountUuid | Should -Be 'uuid-c'
349
+ }
350
+ }
package/skills/CLAUDE.md CHANGED
@@ -17,16 +17,13 @@ Skills install to `~/.claude/skills/<skill-name>/` via `packages/claude-dev-env/
17
17
 
18
18
  ## Shared support code
19
19
 
20
- `_shared/` — support code used by more than one skill. It holds `pr-loop/`, which provides prompt templates and Python helper scripts shared across the PR-loop skills, and `advisor/`, which provides the shared warm-advisor protocol used by `team-advisor` and `orchestrator`.
20
+ `_shared/` — support code used by more than one skill. It holds `pr-loop/`, which provides prompt templates and Python helper scripts shared across the PR-loop skills, and `advisor/`, which provides the shared warm-advisor protocol used by `team-advisor`, `orchestrator`, and `orchestrator-refresh`.
21
21
 
22
22
  ## Skill groups
23
23
 
24
24
  **Planning and implementation**
25
25
  - `anthropic-plan` — creates a source-grounded plan packet before any code changes
26
- - `implement` — structured implementation from an existing plan packet
27
- - `bdd-protocol` — BDD depth: Example Mapping, scenario quality, outside-in layout
28
- - `verified-build` — build + test loop that gates on a verifier verdict
29
- - `orchestrator` — turns the session into the advisor-orchestrator: it spawns executor subagents to do the code edits and test runs; hard decisions go to a shared advisor (Claude warm `session-advisor` via SendMessage; Grok self-as-advisor on the orchestrating session)
26
+ - `orchestrator` — turns the session into the orchestrator: it spawns executor subagents to do the code edits and test runs; hard decisions go to a shared advisor (Claude warm `session-advisor` via SendMessage; Grok self-as-advisor on the orchestrating session)
30
27
  - `orchestrator-refresh` — sub-skill fired by the `/orchestrator` loop to re-assert the host-matched shared-advisor discipline mid-run (Claude SendMessage; Grok self-as-advisor, no Agent spawn)
31
28
  - `team-advisor` — binds one advisor at the strongest reachable tier (Claude warm agent; Grok self-as-advisor) and consults it for a second opinion before a big decision, at completion, when stuck, or when reconsidering the approach
32
29
 
@@ -34,44 +31,25 @@ Skills install to `~/.claude/skills/<skill-name>/` via `packages/claude-dev-env/
34
31
  - `autoconverge` — autonomous single-run workflow that drives a PR to ready
35
32
  - `pr-converge` — paced convergence loop across `ScheduleWakeup` ticks
36
33
  - `bugteam` — open-loop audit-fix until convergence
37
- - `pr-review-responder` — fetches all reviewer comments and replies systematically
38
- - `pr-consistency-audit` — cross-file consistency check on a PR diff
39
34
  - `copilot-review` — requests and polls a GitHub Copilot review
40
- - `findbugs` / `fixbugs` find bugs then fix them in separate passes
35
+ - `copilot-finding-triage` tiers each Copilot gate finding, verifies each code concern with an executed check, then routes it (auto-fix a confirmed defect, resolve a refuted one, page the user only for an inconclusive one)
41
36
  - `reviewer-gates` — availability gates for external reviewers (opt-out parse, Copilot quota, Bugbot trigger/detect)
42
- - `pr-scope-resolve` — one resolution ladder for a PR-loop skill's audit/fix target
43
- - `pr-fix-protocol` — fix, reply, and resolve reviewer findings; the unresolved-thread sweep
44
- - `post-audit-findings` — publishes an audit pass as one GitHub PR review
45
37
  - `pr-loop-lifecycle` — opens and closes a PR-loop run (grant, teardown, PR description, revoke, report)
46
38
  - `pr-loop-cloud-transport` — six-step transport workflow that lets any PR-loop skill run in a session whose `gh` CLI is absent or cannot act on the PR (MCP schema load, origin/HEAD fix, identity rules, the gh-to-MCP substitution matrix, the Copilot status rule, and the post self-check)
47
- - `code` — strict-mode code generation session
48
39
 
49
40
  **Research and discovery**
50
- - `deep-research` — multi-source research with citation
51
- - `research-mode` — activates anti-hallucination discipline for a session
52
41
  - `recall` — retrieves facts from memory files
53
42
  - `remember` — saves a decision, gotcha, or architectural choice to the Obsidian vault
54
43
  - `everything-search` — file-system search via the Everything `es.exe` CLI
55
- - `caveman` — trims noise from a draft artifact
56
44
 
57
45
  **Session and workflow management**
58
46
  - `session-log` — logs a session report to the Obsidian vault
59
47
  - `session-tidy` — tidies the session folder
60
- - `bg-agent` — launches a background agent
61
48
  - `task-build` — gathers open tasks
62
49
  - `privacy-hygiene` — full-repo personal-data and secret sweep plus remediation guide
63
50
  - `update` — updates the dev-env package
64
- - `gh-paginate` — safe `gh api` pagination patterns
65
51
  - `fresh-branch` — creates a clean branch off main
66
52
  - `rebase` — rebases onto main
67
- - `gotcha` — records a hard-won lesson to memory
68
- - `logifix` — restores the Logitech Gaming Software (LCore) tray icon when it disappears on Windows
69
- - `refine` — refinement pass on an artifact
70
- - `structure-prompt` — structures a freeform prompt
71
- - `monitor-open-prs` — polls open PRs for status
72
- - `pre-compact` — compact-safe session handoff
73
- - `qbug` — required baseline PR audit; one clean-coder subagent loops audit → fix → commit → push until clean or stuck
74
53
  - `usage-pause` — waits out the 5-hour usage window in ScheduleWakeup stages that keep agent contexts warm; probes the OAuth usage endpoint or takes a manual reset override
75
54
  - `skill-builder` — complete skill-building lifecycle
76
55
  - `auditing-claude-config` — audits a Claude Code setup for context-budget waste and produces a migration table with savings
77
- - `log-audit` — background agent that audits this repo's own logs for recurring errors and timing regressions and files grouped fixes