claude-dev-env 1.93.0 → 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 (220) 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/code_rules_gate.py +127 -13
  7. package/_shared/pr-loop/scripts/fix_hookspath.py +47 -12
  8. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +4 -0
  9. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +146 -69
  10. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -374
  11. package/_shared/pr-loop/state-schema.md +3 -22
  12. package/bin/CLAUDE.md +1 -1
  13. package/bin/install.mjs +2 -7
  14. package/bin/install.test.mjs +10 -4
  15. package/hooks/blocking/CLAUDE.md +2 -0
  16. package/hooks/blocking/pii_commit_command.py +398 -0
  17. package/hooks/blocking/pii_payload_scan.py +133 -0
  18. package/hooks/blocking/pii_prevention_blocker.py +151 -449
  19. package/hooks/blocking/pii_scanner.py +26 -1
  20. package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +2 -2
  21. package/hooks/blocking/test_pii_prevention_blocker.py +299 -0
  22. package/hooks/blocking/test_pii_scanner.py +27 -2
  23. package/hooks/blocking/test_verdict_directory_write_blocker.py +42 -7
  24. package/hooks/hooks_constants/CLAUDE.md +2 -2
  25. package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -2
  26. package/hooks/hooks_constants/local_identity.py +65 -15
  27. package/hooks/hooks_constants/pii_prevention_constants.py +20 -8
  28. package/hooks/hooks_constants/pr_description_enforcer_constants.py +1 -1
  29. package/hooks/hooks_constants/test_local_identity.py +68 -0
  30. package/package.json +1 -1
  31. package/scripts/CLAUDE.md +10 -1
  32. package/scripts/Get-SessionAccount.ps1 +210 -0
  33. package/scripts/tests/CLAUDE.md +15 -1
  34. package/scripts/tests/Get-SessionAccount.Tests.ps1 +350 -0
  35. package/skills/CLAUDE.md +3 -25
  36. package/skills/anthropic-plan/SKILL.md +1 -13
  37. package/skills/autoconverge/CLAUDE.md +5 -2
  38. package/skills/autoconverge/SKILL.md +112 -387
  39. package/skills/autoconverge/reference/CLAUDE.md +4 -1
  40. package/skills/autoconverge/reference/closing-report.md +9 -2
  41. package/skills/autoconverge/reference/convergence.md +26 -22
  42. package/skills/autoconverge/reference/headless-safety.md +44 -0
  43. package/skills/autoconverge/reference/multi-pr.md +88 -0
  44. package/skills/autoconverge/reference/self-closing-loop.md +84 -0
  45. package/skills/autoconverge/reference/stop-conditions.md +21 -13
  46. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +87 -27
  47. package/skills/autoconverge/workflow/converge.contract.test.mjs +40 -10
  48. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +179 -47
  49. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  50. package/skills/autoconverge/workflow/converge.mjs +284 -94
  51. package/skills/bugteam/CLAUDE.md +1 -1
  52. package/skills/bugteam/CONSTRAINTS.md +3 -3
  53. package/skills/bugteam/PROMPTS.md +8 -19
  54. package/skills/bugteam/SKILL.md +13 -14
  55. package/skills/bugteam/reference/CLAUDE.md +1 -1
  56. package/skills/bugteam/reference/audit-and-teammates.md +2 -2
  57. package/skills/bugteam/reference/copilot-gap-analysis.md +2 -0
  58. package/skills/bugteam/reference/github-pr-reviews.md +56 -38
  59. package/skills/bugteam/reference/team-setup.md +11 -11
  60. package/skills/bugteam/scripts/CLAUDE.md +6 -6
  61. package/skills/bugteam/scripts/README.md +20 -8
  62. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +13 -1463
  63. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +23 -358
  64. package/skills/bugteam/scripts/bugteam_preflight.py +13 -315
  65. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +2 -3
  66. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +34 -1053
  67. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +30 -347
  68. package/skills/bugteam/scripts/test_bugteam_preflight.py +30 -305
  69. package/skills/copilot-finding-triage/SKILL.md +80 -28
  70. package/skills/copilot-finding-triage/reference/tier-rubric.md +70 -11
  71. package/skills/copilot-finding-triage/templates/notification.md +8 -3
  72. package/skills/everything-search/SKILL.md +1 -2
  73. package/skills/grokify/SKILL.md +58 -0
  74. package/skills/grokify/templates/handoff-template.md +92 -0
  75. package/skills/orchestrator/SKILL.md +12 -8
  76. package/skills/orchestrator-refresh/SKILL.md +3 -3
  77. package/skills/pr-converge/SKILL.md +132 -233
  78. package/skills/pr-converge/reference/convergence-gates.md +102 -66
  79. package/skills/pr-converge/reference/examples.md +37 -25
  80. package/skills/pr-converge/reference/fix-protocol.md +7 -7
  81. package/skills/pr-converge/reference/multi-pr-orchestration.md +10 -4
  82. package/skills/pr-converge/reference/per-tick.md +64 -41
  83. package/skills/pr-converge/reference/state-schema.md +27 -2
  84. package/skills/pr-converge/scripts/CLAUDE.md +8 -0
  85. package/skills/pr-converge/scripts/README.md +4 -3
  86. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +23 -0
  87. package/skills/pr-converge/scripts/check_convergence.py +189 -645
  88. package/skills/pr-converge/scripts/check_convergence_gates.py +235 -0
  89. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +219 -0
  90. package/skills/pr-converge/scripts/conftest.py +8 -0
  91. package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -0
  92. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -0
  93. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +26 -0
  94. package/skills/pr-converge/scripts/test_check_convergence.py +73 -228
  95. package/skills/pr-converge/scripts/test_check_convergence_contract.py +321 -0
  96. package/skills/pr-converge/scripts/test_check_convergence_gates.py +68 -0
  97. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +75 -0
  98. package/skills/privacy-hygiene/SKILL.md +8 -7
  99. package/skills/reviewer-gates/SKILL.md +26 -8
  100. package/skills/session-log/SKILL.md +11 -12
  101. package/skills/session-tidy/SKILL.md +40 -30
  102. package/skills/skill-builder/CLAUDE.md +13 -10
  103. package/skills/skill-builder/SKILL.md +31 -31
  104. package/skills/skill-builder/references/CLAUDE.md +10 -6
  105. package/skills/skill-builder/references/delegation-map.md +13 -4
  106. package/skills/skill-builder/references/description-field.md +113 -0
  107. package/skills/skill-builder/references/self-audit-checklist.md +21 -4
  108. package/skills/skill-builder/references/skill-modularity.md +113 -0
  109. package/skills/skill-builder/references/skill-types.md +4 -0
  110. package/skills/skill-builder/templates/CLAUDE.md +4 -4
  111. package/skills/skill-builder/templates/gap-analysis.md +25 -1
  112. package/skills/skill-builder/workflows/CLAUDE.md +6 -4
  113. package/skills/skill-builder/workflows/improve-skill.md +17 -16
  114. package/skills/skill-builder/workflows/new-skill.md +34 -10
  115. package/skills/skill-builder/workflows/polish-skill.md +59 -43
  116. package/skills/usage-pause/SKILL.md +10 -14
  117. package/skills/bdd-protocol/CLAUDE.md +0 -26
  118. package/skills/bdd-protocol/SKILL.md +0 -30
  119. package/skills/bdd-protocol/references/CLAUDE.md +0 -10
  120. package/skills/bdd-protocol/references/anti-patterns.md +0 -26
  121. package/skills/bdd-protocol/references/example-mapping.md +0 -23
  122. package/skills/bg-agent/CLAUDE.md +0 -17
  123. package/skills/bg-agent/SKILL.md +0 -69
  124. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +0 -55
  125. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_fix_hookspath_constants.py +0 -26
  126. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_preflight_constants.py +0 -35
  127. package/skills/caveman/CLAUDE.md +0 -15
  128. package/skills/caveman/SKILL.md +0 -38
  129. package/skills/code/CLAUDE.md +0 -17
  130. package/skills/code/SKILL.md +0 -178
  131. package/skills/deep-research/CLAUDE.md +0 -17
  132. package/skills/deep-research/SKILL.md +0 -80
  133. package/skills/findbugs/CLAUDE.md +0 -20
  134. package/skills/findbugs/SKILL.md +0 -241
  135. package/skills/fixbugs/CLAUDE.md +0 -19
  136. package/skills/fixbugs/SKILL.md +0 -142
  137. package/skills/gh-paginate/CLAUDE.md +0 -18
  138. package/skills/gh-paginate/SKILL.md +0 -84
  139. package/skills/gotcha/CLAUDE.md +0 -33
  140. package/skills/gotcha/SKILL.md +0 -73
  141. package/skills/implement/CLAUDE.md +0 -27
  142. package/skills/implement/SKILL.md +0 -66
  143. package/skills/implement/scripts/CLAUDE.md +0 -22
  144. package/skills/implement/scripts/append_note.py +0 -133
  145. package/skills/implement/scripts/implement_scripts_constants/CLAUDE.md +0 -22
  146. package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
  147. package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +0 -12
  148. package/skills/implement/scripts/test_append_note.py +0 -191
  149. package/skills/log-audit/CLAUDE.md +0 -20
  150. package/skills/log-audit/SKILL.md +0 -66
  151. package/skills/log-audit/reference/CLAUDE.md +0 -9
  152. package/skills/log-audit/reference/charter.md +0 -52
  153. package/skills/log-audit/scripts/CLAUDE.md +0 -27
  154. package/skills/log-audit/scripts/cluster_recurrences.py +0 -261
  155. package/skills/log-audit/scripts/collect_log_window.py +0 -199
  156. package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +0 -12
  157. package/skills/log-audit/scripts/log_audit_constants/__init__.py +0 -0
  158. package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +0 -23
  159. package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +0 -24
  160. package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +0 -49
  161. package/skills/log-audit/scripts/mine_copilot_findings.py +0 -302
  162. package/skills/log-audit/scripts/test_cluster_recurrences.py +0 -160
  163. package/skills/log-audit/scripts/test_collect_log_window.py +0 -111
  164. package/skills/log-audit/scripts/test_mine_copilot_findings.py +0 -126
  165. package/skills/logifix/CLAUDE.md +0 -36
  166. package/skills/logifix/SKILL.md +0 -69
  167. package/skills/logifix/scripts/CLAUDE.md +0 -16
  168. package/skills/logifix/scripts/logifix.ps1 +0 -205
  169. package/skills/monitor-open-prs/CLAUDE.md +0 -34
  170. package/skills/monitor-open-prs/SKILL.md +0 -88
  171. package/skills/monitor-open-prs/scripts/CLAUDE.md +0 -17
  172. package/skills/monitor-open-prs/scripts/discover_open_prs.py +0 -69
  173. package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +0 -149
  174. package/skills/monitor-open-prs/test_skill_contract.py +0 -27
  175. package/skills/post-audit-findings/SKILL.md +0 -80
  176. package/skills/pr-consistency-audit/CLAUDE.md +0 -34
  177. package/skills/pr-consistency-audit/SKILL.md +0 -116
  178. package/skills/pr-consistency-audit/reference/CLAUDE.md +0 -16
  179. package/skills/pr-consistency-audit/reference/detection-rules.md +0 -96
  180. package/skills/pr-consistency-audit/reference/illustrations.md +0 -78
  181. package/skills/pr-fix-protocol/SKILL.md +0 -65
  182. package/skills/pr-review-responder/CLAUDE.md +0 -35
  183. package/skills/pr-review-responder/EXAMPLES.md +0 -590
  184. package/skills/pr-review-responder/PRINCIPLES.md +0 -533
  185. package/skills/pr-review-responder/README.md +0 -168
  186. package/skills/pr-review-responder/SKILL.md +0 -203
  187. package/skills/pr-review-responder/TESTING.md +0 -350
  188. package/skills/pr-scope-resolve/SKILL.md +0 -45
  189. package/skills/pre-compact/CLAUDE.md +0 -24
  190. package/skills/pre-compact/SKILL.md +0 -134
  191. package/skills/qbug/CLAUDE.md +0 -40
  192. package/skills/qbug/SKILL.md +0 -387
  193. package/skills/qbug/test_qbug_skill_audit_schema.py +0 -152
  194. package/skills/qbug/test_qbug_skill_post_fix_audit.py +0 -103
  195. package/skills/refine/CLAUDE.md +0 -44
  196. package/skills/refine/SKILL.md +0 -262
  197. package/skills/refine/templates/CLAUDE.md +0 -17
  198. package/skills/refine/templates/implementation-notes-template.html +0 -56
  199. package/skills/refine/templates/plan-template.md +0 -60
  200. package/skills/research-mode/CLAUDE.md +0 -35
  201. package/skills/research-mode/SKILL.md +0 -53
  202. package/skills/structure-prompt/CLAUDE.md +0 -42
  203. package/skills/structure-prompt/SKILL.md +0 -44
  204. package/skills/structure-prompt/reference/CLAUDE.md +0 -28
  205. package/skills/structure-prompt/reference/adversarial-tuning.md +0 -62
  206. package/skills/structure-prompt/reference/block-classification.md +0 -27
  207. package/skills/structure-prompt/reference/canonical-case.md +0 -48
  208. package/skills/structure-prompt/reference/citation-depth.md +0 -70
  209. package/skills/structure-prompt/reference/cleanup.md +0 -33
  210. package/skills/structure-prompt/reference/constraints.md +0 -33
  211. package/skills/structure-prompt/reference/directives.md +0 -37
  212. package/skills/structure-prompt/reference/examples.md +0 -72
  213. package/skills/structure-prompt/reference/instantiation.md +0 -51
  214. package/skills/structure-prompt/reference/output-contract.md +0 -72
  215. package/skills/structure-prompt/reference/per-category.md +0 -23
  216. package/skills/structure-prompt/reference/persona.md +0 -38
  217. package/skills/structure-prompt/reference/research.md +0 -33
  218. package/skills/structure-prompt/reference/structure.md +0 -28
  219. package/skills/verified-build/CLAUDE.md +0 -33
  220. package/skills/verified-build/SKILL.md +0 -33
@@ -121,6 +121,7 @@ DEFAULT_READABILITY_THRESHOLDS: ReadabilityThresholds = ReadabilityThresholds(
121
121
  __all__ = [
122
122
  "ALL_HEAVY_OPENING_HEADERS",
123
123
  "ALL_HEAVY_TESTING_HEADERS",
124
+ "ALL_PR_NUMBER_BEARING_GH_PR_SUBCOMMANDS",
124
125
  "ALL_READABILITY_CLI_FLAG_TOKENS",
125
126
  "ATOMIC_WRITE_TEMP_SUFFIX",
126
127
  "BLOCKQUOTE_LINE_PATTERN",
@@ -135,7 +136,6 @@ __all__ = [
135
136
  "FLESCH_SYLLABLES_PER_WORD_COEFFICIENT",
136
137
  "FLESCH_WORDS_PER_SENTENCE_COEFFICIENT",
137
138
  "GH_PR_COMMAND_MIN_TOKEN_COUNT",
138
- "ALL_PR_NUMBER_BEARING_GH_PR_SUBCOMMANDS",
139
139
  "HEADING_LINE_PATTERN",
140
140
  "HEAVY_MIN_BODY_CHARS_FOR_CLASSIFICATION",
141
141
  "HEAVY_SHAPE",
@@ -66,6 +66,74 @@ class TestNasValuesPlaceholderDefault:
66
66
  assert local_identity.nas_ssh_port() == 22
67
67
 
68
68
 
69
+ class TestPiiExemptRepositorySlugs:
70
+ def should_read_slugs_from_the_environment_lowercased(self) -> None:
71
+ with patch.dict(
72
+ os.environ,
73
+ {"CLAUDE_PII_EXEMPT_REPOS": "ExemptOwner/exempt-repo, Other/Repo"},
74
+ clear=False,
75
+ ):
76
+ slugs = local_identity.pii_exempt_repository_slugs()
77
+ assert slugs == frozenset({"exemptowner/exempt-repo", "other/repo"})
78
+
79
+ def should_read_slugs_from_the_local_identity_file(self, tmp_path: Path) -> None:
80
+ claude_home = tmp_path / ".claude"
81
+ claude_home.mkdir()
82
+ (claude_home / "local-identity.json").write_text(
83
+ json.dumps({"pii_exempt_repositories": ["FileOwner/file-repo"]}),
84
+ encoding="utf-8",
85
+ )
86
+ with patch.dict(os.environ, {}, clear=False):
87
+ os.environ.pop("CLAUDE_PII_EXEMPT_REPOS", None)
88
+ with patch.object(Path, "home", return_value=tmp_path):
89
+ slugs = local_identity.pii_exempt_repository_slugs()
90
+ assert slugs == frozenset({"fileowner/file-repo"})
91
+
92
+ def should_return_an_empty_set_when_no_env_and_no_file(
93
+ self, tmp_path: Path
94
+ ) -> None:
95
+ with patch.dict(os.environ, {}, clear=False):
96
+ os.environ.pop("CLAUDE_PII_EXEMPT_REPOS", None)
97
+ with patch.object(Path, "home", return_value=tmp_path):
98
+ assert local_identity.pii_exempt_repository_slugs() == frozenset()
99
+
100
+ def should_prefer_environment_slugs_over_local_identity_file(
101
+ self, tmp_path: Path
102
+ ) -> None:
103
+ claude_home = tmp_path / ".claude"
104
+ claude_home.mkdir()
105
+ (claude_home / "local-identity.json").write_text(
106
+ json.dumps({"pii_exempt_repositories": ["FileOwner/file-repo"]}),
107
+ encoding="utf-8",
108
+ )
109
+ with patch.dict(
110
+ os.environ,
111
+ {"CLAUDE_PII_EXEMPT_REPOS": "EnvOwner/env-repo"},
112
+ clear=False,
113
+ ):
114
+ with patch.object(Path, "home", return_value=tmp_path):
115
+ slugs = local_identity.pii_exempt_repository_slugs()
116
+ assert slugs == frozenset({"envowner/env-repo"})
117
+
118
+ def should_fall_through_to_json_when_environment_is_whitespace_only(
119
+ self, tmp_path: Path
120
+ ) -> None:
121
+ claude_home = tmp_path / ".claude"
122
+ claude_home.mkdir()
123
+ (claude_home / "local-identity.json").write_text(
124
+ json.dumps({"pii_exempt_repositories": ["FileOwner/file-repo"]}),
125
+ encoding="utf-8",
126
+ )
127
+ with patch.dict(
128
+ os.environ,
129
+ {"CLAUDE_PII_EXEMPT_REPOS": " , , "},
130
+ clear=False,
131
+ ):
132
+ with patch.object(Path, "home", return_value=tmp_path):
133
+ slugs = local_identity.pii_exempt_repository_slugs()
134
+ assert slugs == frozenset({"fileowner/file-repo"})
135
+
136
+
69
137
  class TestDenyMessagesQuoteTheResolvedHost:
70
138
  def should_include_the_resolved_host_and_port_in_the_bare_binary_message(
71
139
  self,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.93.0",
3
+ "version": "1.94.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/CLAUDE.md CHANGED
@@ -21,6 +21,7 @@ Utility scripts installed into `~/.claude/scripts/` by `bin/install.mjs`. Each s
21
21
  | `Install-SweepEmptyDirs.ps1` | Registers `sweep_empty_dirs.py` as a scheduled task on Windows |
22
22
  | `check.ps1` | Runs the full code-quality check suite |
23
23
  | `Show-Asset.ps1` | Opens files on screen, sizing each image window to the image's pixel dimensions (scaled to fit the screen); non-image files open in their default application |
24
+ | `Get-SessionAccount.ps1` | Reports which Claude account the current session is actually logged into by comparing `~/.claude.json`'s CLI login against a `CLAUDE_USER_DATA_DIR` desktop profile's `lastKnownAccountUuid`, recovering the desktop account's email from profile storage when the two accounts differ |
24
25
 
25
26
  ## Subdirectories
26
27
 
@@ -28,10 +29,18 @@ Utility scripts installed into `~/.claude/scripts/` by `bin/install.mjs`. Each s
28
29
  |---|---|
29
30
  | `dev_env_scripts_constants/` | Named constants (`timing.py`) for scripts in this directory |
30
31
  | `sync_to_cursor/` | Package that builds Cursor `.mdc` files from Claude rules and docs |
31
- | `tests/` | pytest suite for the Python scripts in this directory |
32
+ | `tests/` | pytest suite for the Python scripts and Pester (`*.Tests.ps1`) suite for the PowerShell scripts in this directory |
32
33
 
33
34
  ## Running tests
34
35
 
36
+ Python scripts (pytest):
37
+
35
38
  ```bash
36
39
  python -m pytest packages/claude-dev-env/scripts/tests/
37
40
  ```
41
+
42
+ PowerShell scripts (Pester 5+, `*.Tests.ps1`):
43
+
44
+ ```powershell
45
+ Invoke-Pester -Path packages/claude-dev-env/scripts/tests/
46
+ ```
@@ -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
+ ```