@caddis/cli 0.4.13 → 0.4.15

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 (28) hide show
  1. package/bundles/antigravity-plugin/caddis_statusline.py +30 -3
  2. package/bundles/antigravity-plugin/plugin.json +1 -1
  3. package/bundles/antigravity-plugin/repo_freshness.py +146 -0
  4. package/bundles/antigravity-plugin/scripts/caddis-run.ps1 +11 -0
  5. package/bundles/antigravity-plugin/scripts/caddis-run.sh +9 -0
  6. package/bundles/antigravity-plugin/scripts/caddis_gate.py +24 -3
  7. package/bundles/antigravity-plugin/scripts/caddis_governor.py +542 -0
  8. package/bundles/antigravity-plugin/scripts/caddis_host_guard.py +689 -0
  9. package/bundles/antigravity-plugin/scripts/caddis_lanes.py +1632 -0
  10. package/bundles/antigravity-plugin/scripts/caddis_statusline.py +30 -3
  11. package/bundles/antigravity-plugin/scripts/install-host-guard.ps1 +393 -0
  12. package/bundles/antigravity-plugin/skills/_registry.md +2 -0
  13. package/bundles/antigravity-plugin/skills/confluence-hla/SKILL.md +375 -0
  14. package/bundles/antigravity-plugin/skills/confluence-hla/scripts/convert_hla_to_confluence.py +1164 -0
  15. package/bundles/antigravity-plugin/skills/confluence-hla/scripts/discover_db_lineage.py +970 -0
  16. package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +30 -4
  17. package/bundles/antigravity-plugin/skills/implement/SKILL.md +48 -1
  18. package/bundles/antigravity-plugin/skills/pr-merge/SKILL.md +155 -0
  19. package/bundles/antigravity-plugin/warm_start_agy.py +38 -4
  20. package/bundles/codex/.codex/skills/docs/confluence-hla/SKILL.md +375 -0
  21. package/bundles/codex/.codex/skills/docs/confluence-hla/scripts/convert_hla_to_confluence.py +1164 -0
  22. package/bundles/codex/.codex/skills/docs/confluence-hla/scripts/discover_db_lineage.py +970 -0
  23. package/bundles/manifest.json +2 -2
  24. package/dist/claude-deepseek.js +50 -10
  25. package/dist/claude-glm.js +50 -10
  26. package/dist/claude-oss.js +50 -10
  27. package/dist/cli.js +173 -63
  28. package/package.json +1 -1
@@ -196,7 +196,7 @@ def git_facts(cwd: str) -> dict:
196
196
  try:
197
197
  proc = subprocess.run(
198
198
  ["git", "-C", cwd, "--no-optional-locks", "status", "--porcelain=v2", "--branch"],
199
- stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, timeout=2.0,
199
+ stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, timeout=1.0,
200
200
  )
201
201
  text = proc.stdout.decode("utf-8", "replace")
202
202
  except Exception:
@@ -582,6 +582,7 @@ def render_agy(data: dict, cfg: dict) -> str:
582
582
  used = as_int(cw.get("total_input_tokens"))
583
583
  max_tokens = as_int(cw.get("context_window_size")) or 1_048_576
584
584
  pct = float(as_int(cw.get("used_percentage")))
585
+ record_context(cwd, pct)
585
586
  parts.append(context_segment(c, cfg, used, max_tokens, pct))
586
587
  loud = cached_segment(c, cfg, cached)
587
588
  if loud:
@@ -821,6 +822,10 @@ def main(argv=None) -> int:
821
822
  sys.stdout.reconfigure(encoding="utf-8", errors="replace")
822
823
  except Exception:
823
824
  pass
825
+ try:
826
+ sys.stdin.reconfigure(encoding="utf-8", errors="replace")
827
+ except Exception:
828
+ pass
824
829
 
825
830
  if args.install:
826
831
  hosts = ["claude", "agy"] if args.host == "both" else [args.host]
@@ -854,9 +859,31 @@ def main(argv=None) -> int:
854
859
  # A status line must never break the host. Degrade to the directory name.
855
860
  line = os.path.basename(os.getcwd())
856
861
 
857
- print(line)
862
+ try:
863
+ print(line)
864
+ sys.stdout.flush()
865
+ except (BrokenPipeError, OSError):
866
+ # Parent process cancelled or closed stdout pipe (e.g. agy runner on user prompt/state switch).
867
+ # Redirecting to devnull prevents Windows Python from throwing unhandled TextIOWrapper errors on exit.
868
+ try:
869
+ devnull = open(os.devnull, "w")
870
+ os.dup2(devnull.fileno(), sys.stdout.fileno())
871
+ except Exception:
872
+ pass
873
+ return 0
874
+ except Exception:
875
+ return 0
876
+
858
877
  return 0
859
878
 
860
879
 
861
880
  if __name__ == "__main__":
862
- sys.exit(main())
881
+ try:
882
+ code = main()
883
+ except BaseException:
884
+ code = 0
885
+ try:
886
+ sys.stdout.flush()
887
+ except Exception:
888
+ pass
889
+ sys.exit(code)
@@ -0,0 +1,393 @@
1
+ [CmdletBinding()]
2
+ param(
3
+ [switch]$Apply,
4
+ [string]$NssmSha256,
5
+ [ValidateSet('report', 'enforce')]
6
+ [string]$Mode = 'report'
7
+ )
8
+
9
+ Set-StrictMode -Version Latest
10
+ $ErrorActionPreference = 'Stop'
11
+
12
+ $sourceNssm = 'C:\Tools\nssm\nssm.exe'
13
+ $serviceName = 'caddis-host-guard'
14
+ $programRoot = Join-Path $env:ProgramFiles 'caddis'
15
+ $nssmDir = Join-Path $programRoot 'nssm'
16
+ $nssm = Join-Path $nssmDir 'nssm.exe'
17
+ $pythonDir = Join-Path $env:ProgramFiles 'caddis\python'
18
+ $privatePython = Join-Path $pythonDir 'python.exe'
19
+ $serviceDir = Join-Path $env:ProgramData 'caddis'
20
+ $guardScript = Join-Path $serviceDir 'caddis_host_guard.py'
21
+ $governorScript = Join-Path $serviceDir 'caddis_governor.py'
22
+ $budget = Join-Path $env:ProgramData 'caddis\host-budget.toml'
23
+ $stdoutLog = Join-Path $serviceDir 'guard.out.log'
24
+ $stderrLog = Join-Path $serviceDir 'guard.err.log'
25
+ $sourceGuard = Join-Path $PSScriptRoot 'caddis_host_guard.py'
26
+ $sourceGovernor = Join-Path $PSScriptRoot 'caddis_governor.py'
27
+
28
+ if ($Apply -and $NssmSha256 -notmatch '\A[0-9a-fA-F]{64}\z') {
29
+ [Console]::Error.WriteLine('-Apply requires -NssmSha256 with the trusted NSSM SHA256 (64 hexadecimal characters).')
30
+ exit 3
31
+ }
32
+
33
+ if (-not (Test-Path -LiteralPath $sourceNssm -PathType Leaf)) {
34
+ [Console]::Error.WriteLine("NSSM was not found at $sourceNssm.")
35
+ exit 3
36
+ }
37
+
38
+ if (-not $Apply) {
39
+ Write-Output '# Dry run. Run again with -Apply -NssmSha256 <trusted SHA256> from an elevated shell.'
40
+ Write-Output "New-Item -ItemType Directory -Force -Path `"$programRoot`""
41
+ Write-Output "New-Item -ItemType Directory -Force -Path `"$serviceDir`""
42
+ Write-Output "Get-ChildItem -LiteralPath `"$programRoot`" -Force -Recurse # refuse reparse points"
43
+ Write-Output "Get-ChildItem -LiteralPath `"$serviceDir`" -Force -Recurse # refuse reparse points"
44
+ Write-Output "icacls `"$programRoot`" /setowner `"*S-1-5-32-544`" /T /C"
45
+ Write-Output "icacls `"$programRoot`" /reset /T /C"
46
+ Write-Output "icacls `"$programRoot`" /inheritance:r /grant:r `"*S-1-5-32-544:(OI)(CI)F`" `"*S-1-5-18:(OI)(CI)F`" `"*S-1-5-32-545:(OI)(CI)RX`""
47
+ Write-Output "icacls `"$serviceDir`" /setowner `"*S-1-5-32-544`" /T /C"
48
+ Write-Output "icacls `"$serviceDir`" /reset /T /C"
49
+ Write-Output "icacls `"$serviceDir`" /inheritance:r /grant:r `"*S-1-5-32-544:(OI)(CI)F`" `"*S-1-5-18:(OI)(CI)F`" `"*S-1-5-32-545:(OI)(CI)RX`""
50
+ Write-Output "New-Item -ItemType Directory -Force -Path `"$nssmDir`""
51
+ Write-Output "Lock-CaddisTree -Path `"$nssmDir`""
52
+ Write-Output "Copy-Item -LiteralPath `"$sourceNssm`" -Destination `"$nssm`" -Force"
53
+ Write-Output "Lock-CaddisTree -Path `"$nssmDir`""
54
+ Write-Output "Get-TrustedFileHash -Path `"$nssm`" # must equal -NssmSha256; refuse with exit 3 otherwise"
55
+ Write-Output '$sourcePython = (Get-Command python -CommandType Application -ErrorAction Stop).Source # require a valid PSF signature'
56
+ Write-Output '$basePrefix = Split-Path -Parent $sourcePython # refuse venvs, python*._pth and python*.zip'
57
+ Write-Output ('robocopy "$basePrefix" "{0}" /MIR /XD Lib\site-packages /XF python*._pth python*.zip' -f $pythonDir)
58
+ Write-Output ('& "{0}" -I -S -c ''import sys; print(sys.base_prefix)'' # must resolve to the private copy' -f $privatePython)
59
+ Write-Output "Copy-Item -LiteralPath `"$sourceGuard`" -Destination `"$guardScript`" -Force"
60
+ Write-Output "Copy-Item -LiteralPath `"$sourceGovernor`" -Destination `"$governorScript`" -Force"
61
+ Write-Output "Write default host budget to `"$budget`" with guard mode `"$Mode`" if it is missing"
62
+ Write-Output "icacls `"$programRoot`" /setowner `"*S-1-5-32-544`" /T /C"
63
+ Write-Output "icacls `"$serviceDir`" /setowner `"*S-1-5-32-544`" /T /C"
64
+ Write-Output "& `"$nssm`" install $serviceName `"$privatePython`" -I -S `"$guardScript`" run --budget `"$budget`""
65
+ Write-Output "Update the existing $serviceName service PathName to the quoted protected path `"$nssm`" using Win32_Service.Change."
66
+ Write-Output "& `"$nssm`" set $serviceName AppStdout `"$stdoutLog`""
67
+ Write-Output "& `"$nssm`" set $serviceName AppStderr `"$stderrLog`""
68
+ Write-Output "& `"$nssm`" set $serviceName ObjectName LocalSystem"
69
+ Write-Output "& `"$nssm`" set $serviceName Start SERVICE_AUTO_START"
70
+ Write-Output "& `"$nssm`" start $serviceName"
71
+ exit 0
72
+ }
73
+
74
+ $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
75
+ [Security.Principal.WindowsBuiltInRole]::Administrator
76
+ )
77
+ if (-not $isAdmin) {
78
+ [Console]::Error.WriteLine('install-host-guard.ps1 -Apply requires an elevated shell.')
79
+ exit 3
80
+ }
81
+
82
+ foreach ($source in @($sourceGuard, $sourceGovernor)) {
83
+ if (-not (Test-Path -LiteralPath $source -PathType Leaf)) {
84
+ throw "Required service file not found: $source"
85
+ }
86
+ }
87
+
88
+ $serviceAlreadyInstalled = $null -ne (Get-Service -Name $serviceName -ErrorAction SilentlyContinue)
89
+ $budgetAlreadyExists = Test-Path -LiteralPath $budget -PathType Leaf
90
+ if ($budgetAlreadyExists -and -not $serviceAlreadyInstalled) {
91
+ [Console]::Error.WriteLine(
92
+ "Refusing the pre-existing $budget because no installed service proves that it is admin-owned. " +
93
+ 'Inspect it, remove it, and run the installer again.'
94
+ )
95
+ exit 3
96
+ }
97
+
98
+ function Lock-CaddisTree {
99
+ param([Parameter(Mandatory)][string]$Path)
100
+
101
+ $rootItem = Get-Item -LiteralPath $Path -Force
102
+ $reparsePoint = @($rootItem) + @(Get-ChildItem -LiteralPath $Path -Force -Recurse)
103
+ $reparsePoint = $reparsePoint | Where-Object {
104
+ ($_.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0
105
+ } | Select-Object -First 1
106
+ if ($null -ne $reparsePoint) {
107
+ throw "Refusing reparse point in service tree: $($reparsePoint.FullName)"
108
+ }
109
+
110
+ # Remove inherited and explicit grants from any pre-existing tree before the LocalSystem
111
+ # service can read it. New files then inherit only the three grants below.
112
+ & icacls.exe $Path '/setowner' '*S-1-5-32-544' '/T' '/C'
113
+ if ($LASTEXITCODE -ne 0) {
114
+ throw "icacls owner change failed for $Path with exit code $LASTEXITCODE"
115
+ }
116
+ & icacls.exe $Path '/reset' '/T' '/C'
117
+ if ($LASTEXITCODE -ne 0) {
118
+ throw "icacls reset failed for $Path with exit code $LASTEXITCODE"
119
+ }
120
+ & icacls.exe $Path '/inheritance:r' '/grant:r' '*S-1-5-32-544:(OI)(CI)F' '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-545:(OI)(CI)RX'
121
+ if ($LASTEXITCODE -ne 0) {
122
+ throw "icacls lock failed for $Path with exit code $LASTEXITCODE"
123
+ }
124
+ }
125
+
126
+ function Set-CaddisTreeOwner {
127
+ param([Parameter(Mandatory)][string]$Path)
128
+
129
+ & icacls.exe $Path '/setowner' '*S-1-5-32-544' '/T' '/C'
130
+ if ($LASTEXITCODE -ne 0) {
131
+ throw "icacls owner change failed for $Path with exit code $LASTEXITCODE"
132
+ }
133
+ }
134
+
135
+ function Assert-NoPythonPathOverride {
136
+ param([Parameter(Mandatory)][string]$Path)
137
+
138
+ if (Test-Path -LiteralPath $Path -PathType Container) {
139
+ foreach ($pattern in @('python*._pth', 'python*.zip')) {
140
+ $override = Get-ChildItem -LiteralPath $Path -Filter $pattern -File -Force |
141
+ Select-Object -First 1
142
+ if ($null -ne $override) {
143
+ [Console]::Error.WriteLine("Refusing Python path override file: $($override.FullName)")
144
+ exit 3
145
+ }
146
+ }
147
+ }
148
+ }
149
+
150
+ function Assert-AdminOwned {
151
+ param(
152
+ [Parameter(Mandatory)][string]$Path,
153
+ [switch]$RequireReadOnlyFile
154
+ )
155
+
156
+ $acl = Get-Acl -LiteralPath $Path
157
+ $ownerSid = ([Security.Principal.NTAccount]$acl.Owner).Translate(
158
+ [Security.Principal.SecurityIdentifier]
159
+ ).Value
160
+ $trustedSids = @('S-1-5-18', 'S-1-5-32-544')
161
+ if ($ownerSid -notin $trustedSids) {
162
+ [Console]::Error.WriteLine("Refusing non-admin-owned path: $Path (owner $ownerSid)")
163
+ exit 3
164
+ }
165
+
166
+ if ($RequireReadOnlyFile) {
167
+ $writeRights = [Security.AccessControl.FileSystemRights]::Write -bor
168
+ [Security.AccessControl.FileSystemRights]::Delete -bor
169
+ [Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor
170
+ [Security.AccessControl.FileSystemRights]::ChangePermissions -bor
171
+ [Security.AccessControl.FileSystemRights]::TakeOwnership
172
+ foreach ($rule in $acl.Access) {
173
+ $ruleSid = $rule.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value
174
+ if ($rule.AccessControlType -eq [Security.AccessControl.AccessControlType]::Allow -and
175
+ $ruleSid -notin $trustedSids -and ($rule.FileSystemRights -band $writeRights)) {
176
+ [Console]::Error.WriteLine("Refusing user-writable path: $Path ($ruleSid)")
177
+ exit 3
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ function Get-TrustedFileHash {
184
+ param([Parameter(Mandatory)][string]$Path)
185
+
186
+ $stream = [IO.File]::Open(
187
+ $Path,
188
+ [IO.FileMode]::Open,
189
+ [IO.FileAccess]::Read,
190
+ [IO.FileShare]::Read
191
+ )
192
+ try {
193
+ # The open handle denies writes and deletes, so this ACL check and hash describe
194
+ # the same file even when the parent directory permits users to add files.
195
+ Assert-AdminOwned -Path $Path -RequireReadOnlyFile
196
+ $sha256 = [Security.Cryptography.SHA256]::Create()
197
+ try {
198
+ return [BitConverter]::ToString($sha256.ComputeHash($stream)).Replace('-', '')
199
+ } finally {
200
+ $sha256.Dispose()
201
+ }
202
+ } finally {
203
+ $stream.Dispose()
204
+ }
205
+ }
206
+
207
+ function Get-TrustedPythonFiles {
208
+ param([Parameter(Mandatory)][string]$BasePrefix)
209
+
210
+ $trustedFiles = [Collections.Generic.Dictionary[string, string]]::new(
211
+ [StringComparer]::OrdinalIgnoreCase
212
+ )
213
+ $sitePackages = [IO.Path]::GetFullPath((Join-Path $BasePrefix 'Lib\site-packages')).TrimEnd('\') + '\'
214
+ foreach ($item in Get-ChildItem -LiteralPath $BasePrefix -Force -Recurse -ErrorAction Stop) {
215
+ $fullPath = [IO.Path]::GetFullPath($item.FullName)
216
+ if ($fullPath.StartsWith($sitePackages, [StringComparison]::OrdinalIgnoreCase)) {
217
+ continue
218
+ }
219
+
220
+ if ($item.PSIsContainer) {
221
+ Assert-AdminOwned -Path $fullPath
222
+ continue
223
+ }
224
+
225
+ $relativePath = [IO.Path]::GetRelativePath($BasePrefix, $fullPath)
226
+ if ($item.Extension -in @('.dll', '.pyd') -and
227
+ -not $relativePath.StartsWith('tcl\', [StringComparison]::OrdinalIgnoreCase)) {
228
+ $signature = Get-AuthenticodeSignature -LiteralPath $fullPath
229
+ if ($signature.Status -ne 'Valid' -or
230
+ $signature.SignerCertificate.Subject -notmatch
231
+ 'O=(?:Python Software Foundation|Microsoft Corporation)(?:,|$)') {
232
+ [Console]::Error.WriteLine("Refusing an unsigned or unexpected Python binary: $fullPath")
233
+ exit 3
234
+ }
235
+ }
236
+ $trustedFiles[$relativePath] = Get-TrustedFileHash -Path $fullPath
237
+ }
238
+ return ,$trustedFiles
239
+ }
240
+
241
+ $sourcePython = (Get-Command python -CommandType Application -ErrorAction Stop).Source
242
+ $sourcePythonDir = Split-Path -Parent $sourcePython
243
+ Assert-NoPythonPathOverride -Path $sourcePythonDir
244
+ $sourcePythonItem = Get-Item -LiteralPath $sourcePython -Force
245
+ if (($sourcePythonItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
246
+ [Console]::Error.WriteLine("Refusing a reparse-point Python executable: $sourcePython")
247
+ exit 3
248
+ }
249
+ Assert-AdminOwned -Path $sourcePython -RequireReadOnlyFile
250
+ $sourceSignature = Get-AuthenticodeSignature -LiteralPath $sourcePython
251
+ if ($sourceSignature.Status -ne 'Valid' -or
252
+ $sourceSignature.SignerCertificate.Subject -notmatch 'O=Python Software Foundation(?:,|$)') {
253
+ [Console]::Error.WriteLine("Refusing an untrusted Python executable: $sourcePython")
254
+ exit 3
255
+ }
256
+
257
+ $basePrefix = $sourcePythonDir
258
+ if (-not (Test-Path -LiteralPath (Join-Path $basePrefix 'Lib') -PathType Container) -or
259
+ (Test-Path -LiteralPath (Join-Path (Split-Path -Parent $basePrefix) 'pyvenv.cfg') -PathType Leaf)) {
260
+ [Console]::Error.WriteLine('The Python on PATH must be a base installation, not a virtual environment.')
261
+ exit 3
262
+ }
263
+ $sourceReparsePoint = Get-ChildItem -LiteralPath $basePrefix -Force -Recurse | Where-Object {
264
+ ($_.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0
265
+ } | Select-Object -First 1
266
+ if ($null -ne $sourceReparsePoint) {
267
+ [Console]::Error.WriteLine("Refusing reparse point in the source Python tree: $($sourceReparsePoint.FullName)")
268
+ exit 3
269
+ }
270
+ Assert-NoPythonPathOverride -Path $basePrefix
271
+ $trustedSourceFiles = Get-TrustedPythonFiles -BasePrefix $basePrefix
272
+
273
+ New-Item -ItemType Directory -Force -Path $programRoot | Out-Null
274
+ New-Item -ItemType Directory -Force -Path $serviceDir | Out-Null
275
+ Lock-CaddisTree -Path $programRoot
276
+ Lock-CaddisTree -Path $serviceDir
277
+
278
+ New-Item -ItemType Directory -Force -Path $nssmDir | Out-Null
279
+ Lock-CaddisTree -Path $nssmDir
280
+ Copy-Item -LiteralPath $sourceNssm -Destination $nssm -Force
281
+ Lock-CaddisTree -Path $nssmDir
282
+ Assert-AdminOwned -Path $nssmDir -RequireReadOnlyFile
283
+ if ((Get-TrustedFileHash -Path $nssm) -ine $NssmSha256) {
284
+ [Console]::Error.WriteLine('The protected NSSM copy does not match -NssmSha256.')
285
+ exit 3
286
+ }
287
+
288
+ & robocopy.exe $basePrefix $pythonDir /MIR /XD 'Lib\site-packages' /XF 'python*._pth' 'python*.zip'
289
+ if ($LASTEXITCODE -ge 8) {
290
+ throw "robocopy failed with exit code $LASTEXITCODE"
291
+ }
292
+ if (-not (Test-Path -LiteralPath $privatePython -PathType Leaf)) {
293
+ throw "The private Python copy does not contain $privatePython"
294
+ }
295
+ Assert-NoPythonPathOverride -Path $pythonDir
296
+ Set-CaddisTreeOwner -Path $programRoot
297
+ $trustedPrivateFiles = Get-TrustedPythonFiles -BasePrefix $pythonDir
298
+ if ($trustedPrivateFiles.Count -ne $trustedSourceFiles.Count) {
299
+ throw 'The private Python copy does not contain the validated source file set.'
300
+ }
301
+ foreach ($entry in $trustedSourceFiles.GetEnumerator()) {
302
+ $privateHash = $null
303
+ if (-not $trustedPrivateFiles.TryGetValue($entry.Key, [ref]$privateHash) -or
304
+ $privateHash -ne $entry.Value) {
305
+ throw "The private Python copy does not match the validated source: $($entry.Key)"
306
+ }
307
+ }
308
+ $privateSignature = Get-AuthenticodeSignature -LiteralPath $privatePython
309
+ if ($privateSignature.Status -ne 'Valid' -or
310
+ $privateSignature.SignerCertificate.Subject -notmatch 'O=Python Software Foundation(?:,|$)') {
311
+ throw "The private Python copy failed signature validation: $privatePython"
312
+ }
313
+ $copiedBasePrefix = (& $privatePython -I -S -c 'import sys; print(sys.base_prefix)').Trim()
314
+ if ($LASTEXITCODE -ne 0 -or $copiedBasePrefix -ine $pythonDir) {
315
+ throw "The private Python resolved sys.base_prefix to $copiedBasePrefix instead of $pythonDir"
316
+ }
317
+
318
+ Copy-Item -LiteralPath $sourceGuard -Destination $guardScript -Force
319
+ Copy-Item -LiteralPath $sourceGovernor -Destination $governorScript -Force
320
+
321
+ if (-not (Test-Path -LiteralPath $budget -PathType Leaf)) {
322
+ $budgetText = @"
323
+ [session]
324
+ cpu_percent = 50
325
+ memory_mb = 24576
326
+ max_processes = 256
327
+
328
+ [lane]
329
+ cpu_percent = 25
330
+ memory_mb = 8192
331
+ max_processes = 64
332
+
333
+ [guard]
334
+ mode = "$Mode"
335
+ host_cpu_percent = 85
336
+ sustain_seconds = 60
337
+ sample_seconds = 10
338
+ min_free_memory_mb = 4096
339
+ root_images = ["claude.exe", "codex.exe", "agy.exe"]
340
+ "@
341
+ [System.IO.File]::WriteAllText($budget, $budgetText, [System.Text.UTF8Encoding]::new($false))
342
+ }
343
+
344
+ $modeReader = 'import pathlib, sys, tomllib; p = pathlib.Path(sys.argv[1]); data = tomllib.loads(p.read_text(encoding="utf-8")); print(data.get("guard", {}).get("mode", "enforce"))'
345
+ $configuredMode = (& $privatePython -I -S -c $modeReader $budget).Trim()
346
+ if ($LASTEXITCODE -ne 0 -or -not $configuredMode) {
347
+ throw "Could not read the guard mode from $budget"
348
+ }
349
+ if ($configuredMode -ne $Mode) {
350
+ [Console]::Error.WriteLine(
351
+ "$budget is in $configuredMode mode, not requested $Mode mode. Change it from an elevated shell and run again."
352
+ )
353
+ exit 3
354
+ }
355
+
356
+ Set-CaddisTreeOwner -Path $programRoot
357
+ Set-CaddisTreeOwner -Path $serviceDir
358
+
359
+ if ($serviceAlreadyInstalled) {
360
+ # Updating Application alone leaves the old wrapper in the service ImagePath.
361
+ $service = Get-CimInstance -ClassName Win32_Service -Filter "Name='$serviceName'"
362
+ $change = Invoke-CimMethod -InputObject $service -MethodName Change -Arguments @{
363
+ PathName = '"{0}"' -f $nssm
364
+ }
365
+ if ($change.ReturnValue -ne 0) {
366
+ throw "Service binary update failed with code $($change.ReturnValue)"
367
+ }
368
+ $appParameters = '-I -S "{0}" run --budget "{1}"' -f $guardScript, $budget
369
+ & $nssm set $serviceName Application $privatePython
370
+ if ($LASTEXITCODE -ne 0) { throw "nssm Application failed with exit code $LASTEXITCODE" }
371
+ & $nssm set $serviceName AppParameters $appParameters
372
+ if ($LASTEXITCODE -ne 0) { throw "nssm AppParameters failed with exit code $LASTEXITCODE" }
373
+ } else {
374
+ & $nssm install $serviceName $privatePython '-I' '-S' $guardScript 'run' '--budget' $budget
375
+ if ($LASTEXITCODE -ne 0) { throw "nssm install failed with exit code $LASTEXITCODE" }
376
+ }
377
+ & $nssm set $serviceName AppStdout $stdoutLog
378
+ if ($LASTEXITCODE -ne 0) { throw "nssm AppStdout failed with exit code $LASTEXITCODE" }
379
+ & $nssm set $serviceName AppStderr $stderrLog
380
+ if ($LASTEXITCODE -ne 0) { throw "nssm AppStderr failed with exit code $LASTEXITCODE" }
381
+ & $nssm set $serviceName ObjectName LocalSystem
382
+ if ($LASTEXITCODE -ne 0) { throw "nssm ObjectName failed with exit code $LASTEXITCODE" }
383
+ & $nssm set $serviceName Start SERVICE_AUTO_START
384
+ if ($LASTEXITCODE -ne 0) { throw "nssm Start failed with exit code $LASTEXITCODE" }
385
+ if ($serviceAlreadyInstalled) {
386
+ & $nssm restart $serviceName
387
+ if ($LASTEXITCODE -ne 0) { throw "nssm restart failed with exit code $LASTEXITCODE" }
388
+ } else {
389
+ & $nssm start $serviceName
390
+ if ($LASTEXITCODE -ne 0) { throw "nssm start failed with exit code $LASTEXITCODE" }
391
+ }
392
+
393
+ Write-Output "installed in $Mode mode; switch with: set mode in $budget then & `"$nssm`" restart $serviceName"
@@ -48,6 +48,7 @@
48
48
  | Skill | Path | When to Use |
49
49
  |-------|------|-------------|
50
50
  | Code Documentation | `code-documentation/` | Writing effective code documentation - API docs, README files, inline comments, and technical guides. Use for documenting codebases, APIs, or writing developer guides. |
51
+ | Confluence Hla | `confluence-hla/` | Author enterprise High-Level Architecture (HLA) documents in Simplified Technical English and generate Confluence Storage Format XHTML with self-contained PlantUML diagrams, wrapped tables, and zero AI fingerprints. Use when creating high-level architecture designs, architecture documentation for Confluence, or converting markdown architecture to Confluence storage format. |
51
52
  | Explainer Doc | `explainer-doc/` | Write a document that hands off work to a team who did NOT do the investigation — an external vendor, another team, a contractor — and who must be able to act on it alone. Use when the user says "write a spec for the X team", "document this for someone outside the team", "explainer doc", "implementation spec for a vendor", "hand this off to Y", or is about to write a design doc whose primary reader did not live through the debugging/design process. NOT for internal specs where the reader already has full context (use technical-writing or writing-plans instead) — this is specifically for the "stranger must follow it alone" case. |
52
53
  | Tech Explainer | `tech-explainer/` | >- |
53
54
  | Technical Writing | `technical-writing/` | Technical documentation best practices for READMEs, API docs, architecture docs, runbooks, and developer guides. Use when writing or reviewing documentation, creating onboarding guides, or establishing documentation standards. |
@@ -83,6 +84,7 @@
83
84
  | Mermaid Db | `mermaid-db/` | Turn a SQL artifact (proc, view, query, .sql file, or table name) into a Mermaid diagram that explains it — git-diffable, saved as .md |
84
85
  | Migrate Dir | `migrate-dir/` | Rename this repo's legacy `.claudster/` artifact dir to `.caddis/` — dry-run first, `git mv` to keep history, merges a straggler dir, rewrites live refs. Opt-in, never automatic. |
85
86
  | Park | `park/` | File a future-work item in the one backlog — .caddis/parking-lot/ — or list what is open |
87
+ | Pr Merge | `pr-merge/` | End-to-end pull request and merge lane — push feature branch, open or update PR, monitor CI to green, merge to default branch, and clean up the feature branch. |
86
88
  | Prd | `prd/` | Capture requirements through structured discovery and write a PRD |
87
89
  | Resume | `resume/` | Pop the most recently parked workstream off the stack and resume it at its exact resume point |
88
90
  | Review | `review/` | Review someone else's OPEN pull request end to end — what changed, what CI says, what is risky — then approve or request changes. Never approves on its own. |