@agentshouse/kit 0.1.0-alpha.4 → 0.1.0-alpha.41

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 (150) hide show
  1. package/README.md +41 -1671
  2. package/bin/connect-linux.sh +501 -53
  3. package/bin/connect-windows.ps1 +440 -0
  4. package/bin/kit.js +200 -159
  5. package/bootstrap.json +28 -0
  6. package/docs/configuration.md +104 -0
  7. package/docs/google.md +33 -0
  8. package/docs/linux.md +77 -0
  9. package/docs/macos.md +26 -0
  10. package/docs/telegram.md +29 -0
  11. package/docs/windows.md +37 -0
  12. package/package.json +7 -2
  13. package/src/adapter-mcp.js +92 -12
  14. package/src/agents.js +63 -8
  15. package/src/binding.js +52 -0
  16. package/src/blocks.js +2 -26
  17. package/src/bridge.js +28 -14
  18. package/src/bytes.js +19 -8
  19. package/src/configuration.js +14 -211
  20. package/src/connector-login.js +3 -84
  21. package/src/contract.js +44 -12
  22. package/src/conversation-originals.js +1 -3
  23. package/src/delivery.js +4 -7
  24. package/src/directory.js +6 -26
  25. package/src/draft.js +4 -82
  26. package/src/git-main.js +138 -0
  27. package/src/git.js +62 -0
  28. package/src/google/calendar.js +39 -45
  29. package/src/google/drive.js +61 -186
  30. package/src/google/entries.js +349 -0
  31. package/src/google/gateway.js +3 -6
  32. package/src/google/gmail.js +33 -23
  33. package/src/google/oauth.js +106 -66
  34. package/src/google/source.js +21 -23
  35. package/src/host.js +6 -2
  36. package/src/house-main.js +65 -8
  37. package/src/house.js +446 -127
  38. package/src/how-we-work.md +25 -0
  39. package/src/inbound.js +2 -18
  40. package/src/kit-work-stream.js +120 -0
  41. package/src/launcher.js +46 -10
  42. package/src/local-files.js +3 -11
  43. package/src/login.js +27 -14
  44. package/src/mcp.js +18 -0
  45. package/src/mount.js +9 -18
  46. package/src/original.js +4 -4
  47. package/src/pacing.js +41 -0
  48. package/src/plan.js +37 -45
  49. package/src/repository.js +3 -61
  50. package/src/session-bin/git +2 -0
  51. package/src/setup.js +14 -15
  52. package/src/skill-set/ask-matt/PHASE-BOUNDARIES.md +55 -0
  53. package/src/skill-set/ask-matt/SKILL.md +91 -0
  54. package/src/skill-set/ask-matt/agents/openai.yaml +4 -0
  55. package/src/skill-set/code-review/LICENSE +21 -0
  56. package/src/skill-set/code-review/SKILL.md +89 -0
  57. package/src/skill-set/code-review/agents/openai.yaml +3 -0
  58. package/src/skill-set/codebase-design/DEEPENING.md +37 -0
  59. package/src/skill-set/codebase-design/DESIGN-IT-TWICE.md +44 -0
  60. package/src/skill-set/codebase-design/LICENSE +21 -0
  61. package/src/skill-set/codebase-design/SKILL.md +116 -0
  62. package/src/skill-set/codebase-design/agents/openai.yaml +3 -0
  63. package/src/skill-set/diagnosing-bugs/LICENSE +21 -0
  64. package/src/skill-set/diagnosing-bugs/SKILL.md +140 -0
  65. package/src/skill-set/diagnosing-bugs/agents/openai.yaml +3 -0
  66. package/src/skill-set/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
  67. package/src/skill-set/domain-modeling/ADR-FORMAT.md +47 -0
  68. package/src/skill-set/domain-modeling/CONTEXT-FORMAT.md +60 -0
  69. package/src/skill-set/domain-modeling/LICENSE +21 -0
  70. package/src/skill-set/domain-modeling/SKILL.md +76 -0
  71. package/src/skill-set/domain-modeling/agents/openai.yaml +3 -0
  72. package/src/skill-set/grill-me/LICENSE +21 -0
  73. package/src/skill-set/grill-me/SKILL.md +8 -0
  74. package/src/skill-set/grill-me/agents/openai.yaml +4 -0
  75. package/src/skill-set/grill-with-docs/LICENSE +21 -0
  76. package/src/skill-set/grill-with-docs/SKILL.md +8 -0
  77. package/src/skill-set/grill-with-docs/agents/openai.yaml +4 -0
  78. package/src/skill-set/grilling/LICENSE +21 -0
  79. package/src/{starter-set → skill-set}/grilling/SKILL.md +1 -1
  80. package/src/skill-set/handoff/LICENSE +21 -0
  81. package/src/skill-set/handoff/SKILL.md +17 -0
  82. package/src/skill-set/handoff/agents/openai.yaml +4 -0
  83. package/src/skill-set/implement/LICENSE +21 -0
  84. package/src/skill-set/implement/SKILL.md +16 -0
  85. package/src/skill-set/implement/agents/openai.yaml +4 -0
  86. package/src/skill-set/improve-codebase-architecture/HTML-REPORT.md +123 -0
  87. package/src/skill-set/improve-codebase-architecture/LICENSE +21 -0
  88. package/src/skill-set/improve-codebase-architecture/SKILL.md +72 -0
  89. package/src/skill-set/improve-codebase-architecture/agents/openai.yaml +4 -0
  90. package/src/skill-set/prototype/LICENSE +21 -0
  91. package/src/skill-set/prototype/LOGIC.md +67 -0
  92. package/src/skill-set/prototype/SKILL.md +28 -0
  93. package/src/skill-set/prototype/UI.md +112 -0
  94. package/src/skill-set/prototype/agents/openai.yaml +3 -0
  95. package/src/skill-set/research/LICENSE +21 -0
  96. package/src/skill-set/research/SKILL.md +14 -0
  97. package/src/skill-set/research/agents/openai.yaml +3 -0
  98. package/src/skill-set/resolving-merge-conflicts/LICENSE +21 -0
  99. package/src/skill-set/resolving-merge-conflicts/SKILL.md +16 -0
  100. package/src/skill-set/resolving-merge-conflicts/agents/openai.yaml +3 -0
  101. package/src/skill-set/tdd/LICENSE +21 -0
  102. package/src/skill-set/tdd/SKILL.md +40 -0
  103. package/src/skill-set/tdd/agents/openai.yaml +3 -0
  104. package/src/skill-set/tdd/mocking.md +59 -0
  105. package/src/skill-set/tdd/tests.md +77 -0
  106. package/src/skill-set/teach/GLOSSARY-FORMAT.md +35 -0
  107. package/src/skill-set/teach/LEARNING-RECORD-FORMAT.md +46 -0
  108. package/src/skill-set/teach/LICENSE +21 -0
  109. package/src/skill-set/teach/MISSION-FORMAT.md +31 -0
  110. package/src/skill-set/teach/RESOURCES-FORMAT.md +32 -0
  111. package/src/skill-set/teach/SKILL.md +141 -0
  112. package/src/skill-set/teach/agents/openai.yaml +4 -0
  113. package/src/skill-set/to-questionnaire/LICENSE +21 -0
  114. package/src/skill-set/to-questionnaire/SKILL.md +55 -0
  115. package/src/skill-set/to-questionnaire/agents/openai.yaml +4 -0
  116. package/src/skill-set/to-spec/LICENSE +21 -0
  117. package/src/skill-set/to-spec/SKILL.md +76 -0
  118. package/src/skill-set/to-spec/agents/openai.yaml +4 -0
  119. package/src/skill-set/to-tickets/LICENSE +21 -0
  120. package/src/skill-set/to-tickets/SKILL.md +106 -0
  121. package/src/skill-set/to-tickets/agents/openai.yaml +4 -0
  122. package/src/skill-set/wait-what/LICENSE +21 -0
  123. package/src/skill-set/wait-what/SKILL.md +8 -0
  124. package/src/skill-set/wait-what/agents/openai.yaml +4 -0
  125. package/src/skill-set/wayfinder/LICENSE +21 -0
  126. package/src/skill-set/wayfinder/SKILL.md +129 -0
  127. package/src/skill-set/wayfinder/agents/openai.yaml +4 -0
  128. package/src/skill-set/writing-for-agents/LICENSE +21 -0
  129. package/src/skill-set/writing-for-agents/SKILL-MECHANICS.md +22 -0
  130. package/src/skill-set/writing-for-agents/SKILL.md +83 -0
  131. package/src/skill-set/writing-for-agents/agents/openai.yaml +3 -0
  132. package/src/skill-set.js +85 -0
  133. package/src/source.js +93 -54
  134. package/src/state.js +19 -1
  135. package/src/sync-library.js +7 -11
  136. package/src/sync-state.js +12 -3
  137. package/src/sync.js +142 -43
  138. package/src/tasks.js +92 -0
  139. package/src/telegram-view.js +217 -0
  140. package/src/telegram.js +204 -69
  141. package/src/turn-frames.js +69 -0
  142. package/src/worker.js +738 -189
  143. package/src/working-copy.js +1274 -0
  144. package/src/workspace.js +299 -88
  145. package/src/google/declared.js +0 -146
  146. package/src/google/id.js +0 -1
  147. package/src/instance/google.js +0 -1
  148. package/src/starter-set.js +0 -17
  149. /package/src/{starter-set/grilling → skill-set/ask-matt}/LICENSE +0 -0
  150. /package/src/{starter-set → skill-set}/grilling/agents/openai.yaml +0 -0
@@ -0,0 +1,440 @@
1
+ $ErrorActionPreference = 'Stop'
2
+ $ProgressPreference = 'SilentlyContinue'
3
+
4
+ $Image = 'ghcr.io/agentshouse/kit@sha256:__IMAGE_DIGEST__'
5
+ $Name = 'house-kit'
6
+ $KitHome = 'house-kit-home'
7
+ $KitHomeMount = "type=volume,src=$KitHome,dst=/kit-home"
8
+ $BootstrapArguments = @($args | ForEach-Object { [string]$_ })
9
+ $MinimumBuild = 22631
10
+ $MinimumWsl = [version]'2.1.5'
11
+ $DesktopInstaller = 'https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe'
12
+ $DesktopWaitSeconds = 300
13
+
14
+ function Stop-Bootstrap([string] $Cause) {
15
+ throw "kit_bootstrap_refused: $Cause"
16
+ }
17
+
18
+ function Exit-Bootstrap([int] $Status) {
19
+ throw "kit_bootstrap_exit: $Status"
20
+ }
21
+
22
+ function Invoke-Native([string] $Command, [string[]] $NativeArguments) {
23
+ $ErrorActionPreference = 'Continue'
24
+ $output = @(& $Command @NativeArguments 2>&1 | ForEach-Object { "$_" })
25
+ [pscustomobject]@{ Status = $LASTEXITCODE; Output = $output }
26
+ }
27
+
28
+ function ConvertTo-NativeArgument([string] $Value) {
29
+ if ((Get-Variable -Name PSNativeCommandArgumentPassing -ValueOnly -ErrorAction SilentlyContinue) -in @('Windows', 'Standard')) { return $Value }
30
+ if ($Value -eq '') { return '""' }
31
+ $escaped = $Value -replace '(\\*)"', '$1$1\"'
32
+ if ($PSVersionTable.PSEdition -eq 'Desktop' -and $Value -match '\s') { $escaped = $escaped -replace '(\\+)$', '$1$1' }
33
+ $escaped
34
+ }
35
+
36
+ function Invoke-Docker {
37
+ & docker @args
38
+ if ($LASTEXITCODE -ne 0) { Exit-Bootstrap $LASTEXITCODE }
39
+ }
40
+
41
+ function Read-Docker {
42
+ $read = [pscustomobject]@{ Answers = $false; Platform = ''; System = ''; Kernel = ''; Cause = 'the docker command is not installed' }
43
+ if (-not (Get-Command docker -CommandType Application -ErrorAction SilentlyContinue)) { return $read }
44
+ $result = Invoke-Native docker @('info', '--format', '{{.OSType}}/{{.Architecture}}|{{.OperatingSystem}}|{{.KernelVersion}}')
45
+ $line = @($result.Output | Where-Object { $_ -match '^[a-z]+/[a-z0-9_]+\|' })
46
+ if ($result.Status -ne 0 -or $line.Count -eq 0) {
47
+ $read.Cause = @($result.Output | Where-Object { $_ })[-1]
48
+ return $read
49
+ }
50
+ $fields = $line[0].Split('|')
51
+ $read.Answers = $true
52
+ $read.Platform = $fields[0]
53
+ $read.System = $fields[1]
54
+ $read.Kernel = $fields[2]
55
+ $read
56
+ }
57
+
58
+ function Invoke-Elevated([string] $FilePath, [string[]] $ElevatedArguments, [string] $Failure, [int[]] $Accepted = @(0)) {
59
+ try {
60
+ $process = Start-Process -FilePath $FilePath -ArgumentList $ElevatedArguments -Verb RunAs -Wait -PassThru
61
+ } catch {
62
+ Stop-Bootstrap "$Failure; Windows did not grant administrator rights: $($_.Exception.Message); rerun this bootstrap and approve the prompt"
63
+ }
64
+ if ($Accepted -notcontains $process.ExitCode) {
65
+ Stop-Bootstrap "$Failure (exit $($process.ExitCode)); rerun this bootstrap"
66
+ }
67
+ }
68
+
69
+ function Get-WslVersion {
70
+ if (-not (Get-Command wsl.exe -CommandType Application -ErrorAction SilentlyContinue)) { return $null }
71
+ $result = Invoke-Native wsl.exe @('--version')
72
+ if ($result.Status -ne 0) { return $null }
73
+ if ((($result.Output -join "`n") -replace "`0", '') -match '(\d+)\.(\d+)\.(\d+)') {
74
+ return [version]"$($Matches[1]).$($Matches[2]).$($Matches[3])"
75
+ }
76
+ $null
77
+ }
78
+
79
+ function Initialize-Wsl {
80
+ $version = Get-WslVersion
81
+ if ($null -eq $version) {
82
+ Write-Host 'Installing WSL through wsl --install --no-distribution; Windows asks you to approve administrator rights.'
83
+ Invoke-Elevated 'wsl.exe' @('--install', '--no-distribution') 'WSL could not be installed' @(0, 3010)
84
+ Stop-Bootstrap 'WSL is installed and needs a Windows restart; restart Windows, then rerun this bootstrap'
85
+ }
86
+ if ($version -lt $MinimumWsl) {
87
+ Write-Host "Updating WSL $version through wsl --update; Windows asks you to approve administrator rights."
88
+ Invoke-Elevated 'wsl.exe' @('--update') 'WSL could not be updated'
89
+ }
90
+ }
91
+
92
+ function Find-DockerDesktop {
93
+ foreach ($location in @((Join-Path $env:ProgramFiles 'Docker\Docker'), (Join-Path $env:LOCALAPPDATA 'Programs\DockerDesktop'))) {
94
+ if (Test-Path -LiteralPath (Join-Path $location 'Docker Desktop.exe') -PathType Leaf) { return $location }
95
+ }
96
+ $null
97
+ }
98
+
99
+ function Add-DockerPath([string] $Location) {
100
+ if (-not (Get-Command docker -CommandType Application -ErrorAction SilentlyContinue)) {
101
+ $env:PATH = "$env:PATH;$(Join-Path $Location 'resources\bin')"
102
+ }
103
+ }
104
+
105
+ function Install-DockerDesktop {
106
+ $download = Join-Path ([IO.Path]::GetTempPath()) ([IO.Path]::GetRandomFileName())
107
+ [void][IO.Directory]::CreateDirectory($download)
108
+ try {
109
+ $installer = Join-Path $download 'Docker Desktop Installer.exe'
110
+ Write-Host 'Downloading Docker Desktop for Windows from docker.com.'
111
+ try {
112
+ Invoke-WebRequest -UseBasicParsing -Uri $DesktopInstaller -OutFile $installer
113
+ } catch {
114
+ Stop-Bootstrap "Docker Desktop could not be downloaded: $($_.Exception.Message)"
115
+ }
116
+ Write-Host 'Installing Docker Desktop for this Windows account.'
117
+ try {
118
+ $process = Start-Process -FilePath $installer -ArgumentList @('install', '--user', '--quiet') -Wait -PassThru
119
+ } catch {
120
+ Stop-Bootstrap "the Docker Desktop installer could not be started: $($_.Exception.Message)"
121
+ }
122
+ if ($process.ExitCode -ne 0) { Stop-Bootstrap "the Docker Desktop installer did not finish (exit $($process.ExitCode))" }
123
+ } finally {
124
+ Remove-Item -LiteralPath $download -Recurse -Force -ErrorAction SilentlyContinue
125
+ }
126
+ }
127
+
128
+ function Initialize-DockerDesktop {
129
+ $location = Find-DockerDesktop
130
+ if ($location) { Add-DockerPath $location }
131
+ $docker = Read-Docker
132
+ if ($docker.Answers) { return $docker }
133
+ if ($Forwarding) {
134
+ if (-not $location) { Stop-Bootstrap 'House Kit is not installed; run this bootstrap without arguments first' }
135
+ Stop-Bootstrap "Docker Desktop does not answer: $($docker.Cause); start Docker Desktop, then rerun this command"
136
+ }
137
+ Initialize-Wsl
138
+ if (-not $location) {
139
+ Install-DockerDesktop
140
+ $location = Find-DockerDesktop
141
+ if (-not $location) { Stop-Bootstrap 'Docker Desktop is not where its installer puts it; rerun this bootstrap' }
142
+ Add-DockerPath $location
143
+ }
144
+ Write-Host 'Starting Docker Desktop; accept its terms or finish its setup if its window asks.'
145
+ try {
146
+ Start-Process -FilePath (Join-Path $location 'Docker Desktop.exe')
147
+ } catch {
148
+ Stop-Bootstrap "Docker Desktop could not be started: $($_.Exception.Message)"
149
+ }
150
+ $waited = 0
151
+ while (-not ($docker = Read-Docker).Answers) {
152
+ if ($waited -ge $DesktopWaitSeconds) {
153
+ Stop-Bootstrap "Docker Desktop is not ready: $($docker.Cause); finish what its window asks, then rerun this bootstrap"
154
+ }
155
+ Start-Sleep -Seconds 2
156
+ $waited += 2
157
+ }
158
+ $docker
159
+ }
160
+
161
+ function Protect-Directory([string] $Path) {
162
+ if ($Path.Contains(',')) { Stop-Bootstrap "$Path contains a comma, which this bootstrap cannot pass to a Docker mount from Windows PowerShell" }
163
+ $item = Get-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue
164
+ if ($item -and ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { Stop-Bootstrap "$Path is a symbolic link" }
165
+ $created = -not $item
166
+ if ($created) {
167
+ try { [void][IO.Directory]::CreateDirectory($Path) } catch { Stop-Bootstrap "$Path could not be created" }
168
+ $item = Get-Item -LiteralPath $Path -Force
169
+ }
170
+ if (-not $item.PSIsContainer) { Stop-Bootstrap "$Path is not a directory" }
171
+ $owner = (Get-Acl -LiteralPath $Path).GetOwner([Security.Principal.SecurityIdentifier]).Value
172
+ if (-not $created -and $owner -ne $User.Value) { Stop-Bootstrap "$Path is not owned by this User" }
173
+ try {
174
+ $security = New-Object Security.AccessControl.DirectorySecurity
175
+ if ($owner -ne $User.Value) { $security.SetOwner($User) }
176
+ $security.SetAccessRuleProtection($true, $false)
177
+ $security.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule($User, 'FullControl', 'ContainerInherit, ObjectInherit', 'None', 'Allow')))
178
+ $item.SetAccessControl($security)
179
+ } catch {
180
+ Stop-Bootstrap "$Path could not be owner-protected"
181
+ }
182
+ $item.FullName.TrimEnd('\')
183
+ }
184
+
185
+ function Invoke-KitHome([string] $Failure, [string] $Script) {
186
+ $ErrorActionPreference = 'Continue'
187
+ $output = @($input | & docker run -i --rm --network none --mount $KitHomeMount --entrypoint sh $Image -c $Script 2>&1 | ForEach-Object { "$_" })
188
+ if ($LASTEXITCODE -ne 0) { Stop-Bootstrap "${Failure}: $(@($output | Where-Object { $_ })[-1])" }
189
+ $output
190
+ }
191
+
192
+ function Read-KitHome {
193
+ $files = @{}
194
+ $listed = Invoke-KitHome 'the Kit home could not be read' 'cd /kit-home && for name in environment house kit.json; do if [ -f $name ]; then echo $name $(base64 -w 0 $name); fi; done && if [ -f credential ]; then echo credential; fi'
195
+ foreach ($line in @($listed | Where-Object { $_ -match '^(credential|environment|house|kit\.json)( [A-Za-z0-9+/=]+)?$' })) {
196
+ $file, $encoded = $line -split ' ', 2
197
+ $files[$file] = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String("$encoded"))
198
+ }
199
+ $files
200
+ }
201
+
202
+ function Write-KitHome([string] $File, [string] $Text) {
203
+ [void]([Convert]::ToBase64String((New-Object Text.UTF8Encoding($false)).GetBytes($Text)) | Invoke-KitHome "$File could not be updated" "umask 077 && base64 -di > /kit-home/$File")
204
+ }
205
+
206
+ function Read-Inspect([string] $Format) {
207
+ $value = & docker inspect --format $Format $Name
208
+ if ($LASTEXITCODE -ne 0) { Exit-Bootstrap $LASTEXITCODE }
209
+ "$value"
210
+ }
211
+
212
+ function Read-Source([string] $Destination) {
213
+ $source = Read-Inspect ('{{range .Mounts}}{{if eq .Destination `' + $Destination + '`}}{{.Source}}{{end}}{{end}}')
214
+ if ($source -match '^/run/desktop/mnt/host/([a-z])(/.*)?$') { $source = "$($Matches[1].ToUpper()):$($Matches[2] -replace '/', '\')" }
215
+ $source
216
+ }
217
+
218
+ function Test-Container {
219
+ (Invoke-Native docker @('container', 'inspect', $Name)).Status -eq 0
220
+ }
221
+
222
+ function Invoke-Login([string[]] $LoginArguments) {
223
+ if ($LoginArguments -contains '--manual') {
224
+ Invoke-Docker run -i --rm @Network @Common login @LoginArguments
225
+ return
226
+ }
227
+ & docker run @Published --rm @Network @Common login @LoginArguments --host-browser | ForEach-Object {
228
+ if ($_ -like 'Open *') {
229
+ Write-Host 'Opening House Login in the host browser.'
230
+ try { Start-Process -FilePath $_.Substring(5) } catch { Stop-Bootstrap 'host browser did not open; rerun with --manual' }
231
+ } else {
232
+ Write-Host $_
233
+ }
234
+ }
235
+ if ($LASTEXITCODE -ne 0) { Exit-Bootstrap $LASTEXITCODE }
236
+ }
237
+
238
+ function Start-Resident {
239
+ $resident = @()
240
+ if (-not $House.StartsWith('https://')) { $resident = @('--network', 'host') }
241
+ Invoke-Docker run -d --name $Name --restart unless-stopped @resident --mount $KitHomeMount --mount $WorkspaceMount --label "agentshouse.house=$House" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 $Image resident | Out-Null
242
+ }
243
+
244
+ function Connect-Kit {
245
+ $House = 'https://agents.house'
246
+ $Workspace = $null
247
+ $Manual = $false
248
+ $Skills = 'true'
249
+ $WorkspaceSelected = $false
250
+ $HouseSelected = $false
251
+ $Forwarding = $false
252
+ $Forwarded = 'kit'
253
+ $Forward = @()
254
+ $Arguments = $BootstrapArguments
255
+ if ($Arguments.Count -gt 0 -and ($Arguments[0] -ceq 'kit' -or $Arguments[0] -ceq 'house')) {
256
+ $Forwarding = $true
257
+ $Forwarded = $Arguments[0]
258
+ $Forward = @($Arguments | Select-Object -Skip 1 | ForEach-Object { ConvertTo-NativeArgument $_ })
259
+ $Arguments = @()
260
+ }
261
+ for ($index = 0; $index -lt $Arguments.Count; $index++) {
262
+ $argument = $Arguments[$index]
263
+ if ($argument -ceq '--workspace' -or $argument -ceq '--house') {
264
+ if ($index + 1 -ge $Arguments.Count) { Stop-Bootstrap "$argument requires one value" }
265
+ $index++
266
+ if ($argument -ceq '--workspace') {
267
+ if ($WorkspaceSelected) { Stop-Bootstrap 'supply exactly one workspace root' }
268
+ $Workspace = $Arguments[$index]
269
+ $WorkspaceSelected = $true
270
+ } else {
271
+ if ($HouseSelected) { Stop-Bootstrap 'supply exactly one House origin' }
272
+ $House = $Arguments[$index]
273
+ $HouseSelected = $true
274
+ }
275
+ } elseif ($argument -ceq '--manual') {
276
+ $Manual = $true
277
+ } elseif ($argument -ceq '--no-skills') {
278
+ $Skills = 'false'
279
+ } else {
280
+ Stop-Bootstrap "unknown argument $argument"
281
+ }
282
+ }
283
+
284
+ if ($Image -notmatch '^ghcr\.io/agentshouse/kit@sha256:[0-9a-f]{64}$') { Stop-Bootstrap 'this bootstrap has not been published with an immutable image digest' }
285
+ if ($env:OS -ne 'Windows_NT') { Stop-Bootstrap 'this bootstrap is for Windows 11; run connect-linux.sh on Linux and macOS' }
286
+ if (-not [Environment]::Is64BitProcess) { Stop-Bootstrap 'this is a 32-bit PowerShell; run this bootstrap from the 64-bit Windows PowerShell' }
287
+ $system = Get-CimInstance -ClassName Win32_OperatingSystem
288
+ if ([int]$system.ProductType -ne 1) { Stop-Bootstrap "$($system.Caption) is a Windows Server edition; this bootstrap supports Windows 11 on amd64" }
289
+ if ([int]$system.BuildNumber -lt 22000) { Stop-Bootstrap "$($system.Caption) is not Windows 11; this bootstrap supports Windows 11 on amd64" }
290
+ if ([int]$system.BuildNumber -lt $MinimumBuild) { Stop-Bootstrap "Docker Desktop needs Windows 11 23H2 (build $MinimumBuild) or later, not build $($system.BuildNumber); update Windows, then rerun this bootstrap" }
291
+ $processor = [int]@(Get-CimInstance -ClassName Win32_Processor)[0].Architecture
292
+ if ($processor -ne 9) {
293
+ $described = @{ 0 = 'x86'; 5 = 'ARM'; 12 = 'ARM64' }[$processor]
294
+ if (-not $described) { $described = "architecture $processor" }
295
+ Stop-Bootstrap "Windows on $described is not supported; this bootstrap supports Windows 11 on amd64"
296
+ }
297
+ if ($WorkspaceSelected -and $Workspace -notmatch '^[A-Za-z]:\\') { Stop-Bootstrap 'workspace root must be an absolute path' }
298
+
299
+ $docker = Initialize-DockerDesktop
300
+ if (-not $docker.Platform.StartsWith('linux/')) { Stop-Bootstrap "Docker runs $($docker.Platform.Split('/')[0]) containers; switch Docker Desktop to Linux containers, then rerun this bootstrap" }
301
+ if ($docker.System -ne 'Docker Desktop') { Stop-Bootstrap "Docker answers from $($docker.System), not Docker Desktop; stop that engine or switch Docker to Docker Desktop, then rerun this bootstrap" }
302
+ if ($docker.Kernel -notlike '*WSL2*') { Stop-Bootstrap "Docker Desktop does not use its WSL 2 engine; turn on Use the WSL 2 based engine in its settings, then rerun this bootstrap" }
303
+ if ($docker.Platform -ne 'linux/x86_64' -and $docker.Platform -ne 'linux/amd64') { Stop-Bootstrap "host linux/amd64 does not match Docker Desktop $($docker.Platform)" }
304
+ if ($Forwarding) {
305
+ if (-not (Test-Container)) { Stop-Bootstrap 'House Kit is not installed; run this bootstrap without arguments first' }
306
+ if ((Read-Inspect '{{.Config.Image}}') -cne $Image) { Stop-Bootstrap 'an installed Kit uses a different image; rerun this bootstrap without arguments to update it' }
307
+ $Workspace = Read-Source '/agents/house'
308
+ if ($Workspace -notmatch '^[A-Za-z]:\\' -or -not (Test-Path -LiteralPath $Workspace -PathType Container)) {
309
+ Stop-Bootstrap "the installed Kit's workspace root $Workspace is not a directory on this computer; rerun this bootstrap without arguments"
310
+ }
311
+ } else {
312
+ Invoke-Docker volume create $KitHome | Out-Null
313
+ if ((Invoke-Native docker @('image', 'inspect', '--format', '{{.Id}}', $Image)).Status -ne 0) { Invoke-Docker pull --quiet --platform linux/amd64 $Image | Out-Null }
314
+ }
315
+ if (-not $Workspace) { $Workspace = Join-Path $env:USERPROFILE 'AgentsHouse' }
316
+
317
+ $User = [Security.Principal.WindowsIdentity]::GetCurrent().User
318
+ $Workspace = Protect-Directory $Workspace
319
+ $WorkspaceMount = "type=bind,src=$Workspace,dst=/agents/house"
320
+ $stored = Read-KitHome
321
+ if (($stored.ContainsKey('credential') -or $stored.ContainsKey('environment')) -and -not $stored.ContainsKey('house')) {
322
+ Stop-Bootstrap 'stored House origin is missing for an enrolled Environment'
323
+ }
324
+ if ($stored.ContainsKey('house')) {
325
+ $boundHouse = @($stored['house'] -split '\r?\n')[0]
326
+ if (-not $boundHouse) { Stop-Bootstrap 'stored House origin is unreadable' }
327
+ if ($HouseSelected -and $House -cne $boundHouse) { Stop-Bootstrap 'this Environment is bound to another House origin' }
328
+ $House = $boundHouse
329
+ }
330
+ if ($House -notmatch '^(https://|http://127\.0\.0\.1:|http://localhost:)') { Stop-Bootstrap 'House origin must use HTTPS or local loopback HTTP' }
331
+ if (-not $Forwarding) {
332
+ $text = "{`"availability`":`"intermittent`",`"execution_authority`":`"allow`",`"migrators`":[],`"instances`":[],`"clients`":[],`"host`":null,`"skills`":$Skills}`n"
333
+ if ($stored.ContainsKey('kit.json')) {
334
+ $text = $stored['kit.json']
335
+ if ($text -match '"skills"\s*:') {
336
+ $text = $text -replace '("skills"\s*:\s*)(true|false)', "`${1}$Skills"
337
+ } else {
338
+ $text = $text -replace '^(\s*)\{', "`${1}{`"skills`":$Skills,"
339
+ }
340
+ }
341
+ if ($text -notmatch "`"skills`"\s*:\s*$Skills") { Stop-Bootstrap 'kit.json could not be updated' }
342
+ Write-KitHome 'kit.json' $text
343
+ }
344
+ $Network = @('--network', 'host')
345
+ $Published = @()
346
+ if ($House.StartsWith('https://')) {
347
+ $Network = @()
348
+ $port = Get-Random -Minimum 49152 -Maximum 65536
349
+ $Published = @('--publish', "127.0.0.1:${port}:$port", '--env', "HOUSE_KIT_LOGIN_PORT=$port")
350
+ }
351
+ $Common = @('--mount', $KitHomeMount, '--mount', $WorkspaceMount, '--env', 'HOME=/kit-home', '--env', 'HOUSE_KIT_HOME=/kit-home', '--env', 'HOUSE_KIT_CONTAINER=1', '--env', "HOUSE_KIT_HOSTNAME=$([Net.Dns]::GetHostName())", $Image)
352
+
353
+ if (Test-Container) {
354
+ $installed = Read-Inspect '{{.Config.Image}}'
355
+ if ((Read-Inspect '{{.HostConfig.RestartPolicy.Name}}') -cne 'unless-stopped') { Stop-Bootstrap 'the installed Kit has a different lifecycle' }
356
+ if ((Read-Inspect '{{len .Mounts}}') -cne '2') { Stop-Bootstrap 'the installed Kit has unexpected mounts' }
357
+ if ((Read-Inspect '{{range .Mounts}}{{if eq .Destination `/kit-home`}}{{.Name}}{{end}}{{end}}') -cne $KitHome) { Stop-Bootstrap 'the installed Kit uses another Kit home' }
358
+ if ((Read-Inspect '{{range .Mounts}}{{if eq .Destination `/kit-home`}}{{.Type}}/{{.RW}}{{end}}{{end}}') -cne 'volume/true') { Stop-Bootstrap 'the installed Kit home is not a writable volume' }
359
+ if ((Read-Source '/agents/house') -ine $Workspace) { Stop-Bootstrap 'the installed Kit uses another workspace root' }
360
+ if ((Read-Inspect '{{range .Mounts}}{{if eq .Destination `/agents/house`}}{{.Type}}/{{.RW}}{{end}}{{end}}') -cne 'bind/true') { Stop-Bootstrap 'the installed workspace is not a writable bind mount' }
361
+ if ((Read-Inspect '{{index .Config.Labels `agentshouse.house`}}') -cne $House) { Stop-Bootstrap 'the installed Kit uses another House origin' }
362
+ if (-not ($stored.ContainsKey('credential') -and $stored.ContainsKey('environment') -and $stored.ContainsKey('house'))) { Stop-Bootstrap 'the installed Kit has no enrolled authority' }
363
+ if ($Forwarding) {
364
+ if ($Forwarded -ceq 'kit' -and $Forward.Count -gt 0 -and $Forward[0] -ceq 'login') {
365
+ Invoke-Login @($Forward | Select-Object -Skip 1)
366
+ return
367
+ }
368
+ $placed = @()
369
+ $entry = @()
370
+ if ($Forwarded -ceq 'house') {
371
+ $here = (Get-Location).ProviderPath.TrimEnd('\')
372
+ if ($here -ine $Workspace -and -not $here.StartsWith("$Workspace\", [StringComparison]::OrdinalIgnoreCase)) {
373
+ Stop-Bootstrap "the current directory is outside the workspace $Workspace"
374
+ }
375
+ $placed = @('--workdir', "/agents/house$($here.Substring($Workspace.Length).Replace('\', '/'))")
376
+ $entry = @('--entrypoint', 'house')
377
+ }
378
+ $terminal = @()
379
+ if (-not [Console]::IsInputRedirected) { $terminal = @('-t') }
380
+ if ((Read-Inspect '{{.State.Running}}') -ceq 'true') {
381
+ & docker exec -i @terminal @placed $Name $Forwarded @Forward
382
+ } else {
383
+ & docker run -i @terminal @placed @entry --rm @Network @Common @Forward
384
+ }
385
+ Exit-Bootstrap $LASTEXITCODE
386
+ }
387
+ if ($installed -cne $Image) {
388
+ if ((Read-Inspect '{{.State.Running}}') -ceq 'true') { Invoke-Docker exec $Name kit resident drain }
389
+ Invoke-Docker rm -f $Name | Out-Null
390
+ [void](Invoke-KitHome 'the Kit home could not be updated' 'rm -f /kit-home/state/update-drain')
391
+ Start-Resident
392
+ Write-Host "House Kit updated for Environment $($stored['environment'].Trim())."
393
+ return
394
+ }
395
+ Invoke-Docker run --rm @Network @Common credential check
396
+ if ((Read-Inspect '{{.State.Running}}') -ceq 'true') {
397
+ Write-Host "House Kit is already running for Environment $($stored['environment'].Trim())."
398
+ } else {
399
+ Invoke-Docker start $Name | Out-Null
400
+ Write-Host "House Kit restarted for Environment $($stored['environment'].Trim())."
401
+ }
402
+ return
403
+ }
404
+
405
+ if ($stored.ContainsKey('credential')) {
406
+ Invoke-Docker run --rm @Network @Common credential check
407
+ } else {
408
+ $login = @()
409
+ if ($HouseSelected) { $login += @('--house', $House) }
410
+ if ($Manual) { $login += '--manual' }
411
+ Invoke-Login $login
412
+ }
413
+ Invoke-Docker run --rm @Network @Common apply
414
+ Start-Resident
415
+ Write-Host "House Kit connected for Environment $((Read-KitHome)['environment'].Trim())."
416
+ }
417
+
418
+ $consoleEncoding = [Console]::OutputEncoding
419
+ try {
420
+ try {
421
+ [Console]::OutputEncoding = New-Object Text.UTF8Encoding($false)
422
+ } catch {
423
+ Stop-Bootstrap 'this PowerShell has no console; run this bootstrap from a Windows PowerShell console window'
424
+ }
425
+ Connect-Kit
426
+ $global:LASTEXITCODE = 0
427
+ } catch {
428
+ $message = "$($_.Exception.Message)"
429
+ if ($message -match '^kit_bootstrap_exit: (\d+)$') {
430
+ $global:LASTEXITCODE = [int]$Matches[1]
431
+ } elseif ($message.StartsWith('kit_bootstrap_refused: ')) {
432
+ [Console]::Error.WriteLine($message)
433
+ $global:LASTEXITCODE = 1
434
+ } else {
435
+ $global:LASTEXITCODE = 1
436
+ throw
437
+ }
438
+ } finally {
439
+ try { [Console]::OutputEncoding = $consoleEncoding } catch { }
440
+ }