mahbot 0.7.2

An autonomous agentic engineering system that manages software development through role separation, subagents, and deterministic diagnostics.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
# MahBot's own installer for Windows.
#
# It downloads this system's ready-made release file, puts it in place, makes
# the location visible to the owner's own environment and start menu, and starts
# the product. It never builds anything, and needs nothing beyond what Windows
# itself ships.
#
# The release contract it mirrors — one contract in four places: this script, the
# unix-like one (`install.sh`), the product's own updater (`src/self_update.rs`)
# and the workflow that publishes the files (`.github/workflows/release.yml`):
#
#   newest release:  {base}/releases/latest/download/version.txt, body `<version>\n`
#                    — the newest release that is not a test release, and nothing
#                    at all while only test releases exist
#   exact version:   {base}/releases/download/v{version}/{asset}
#   asset:           mahbot-{version}-windows-{arch}.zip, arch in x86_64/aarch64,
#                    holding exactly one file named `mahbot.exe` at its root
#   version:         the newest release above, or one named as this script's own
#                    `-Version` argument, or in MAHBOT_INSTALL_VERSION — either
#                    spelling, `0.7.0` or the release mark's own `v0.7.0`
#   install:         %LOCALAPPDATA%\Programs\MahBot\mahbot.exe — the per-user
#                    programs directory `src/util/managed_bin.rs::mahbot_install_dir`
#                    names on this platform
#
# Making the location visible is the product's own edit of the owner's `Path`
# value in `HKCU\Environment` (`src/util/owner_path.rs`): the same value, the
# same kind, and the references his own entries contain left unexpanded, because
# writing them back expanded would fix his own environment into the value for
# good. An entry that already names the folder is recognised — spelled out, or
# through a `%REF%` the entry holds — so it is written back not at all and the
# folder never appears twice; the value never gains an empty entry either, since
# one names the current directory, and that is exactly what must not happen to
# his search path.
#
# The ARM64 floor is Windows 11's, not Windows 10's: an ARM64 file exists, but
# nothing below build 22000 — every Windows 10 on ARM — is a system it is built
# for.
#
# Nothing here ends the session it was run from. The way this script is normally
# run — `irm … | iex` — puts its body in that session, so the body is one function
# and a failure only prints the one sentence saying what happened: no exit code is
# set from inside it, because which form it was run in cannot be told from that
# scope without guessing and a wrong guess closes the owner's window. That function,
# and everything else the script adds to the session, is removed again when the run
# ends, so the session is left as it was — apart from two product-qualified names it
# cannot avoid touching: the `MahBotVersion` parameter, bound in that scope and
# removed again at the end (a variable of that name the caller already had is taken
# over while the run lasts), and the `Win32.NativeMethods` type the environment
# broadcast defines, which PowerShell offers no way to define without leaving it
# behind and which appears only when the search path really changed.
#
# MAHBOT_RELEASE_BASE_URL is a test-only hook, the one `src/self_update.rs` also
# honours: it moves the release base off the constant below, which is how the
# published files are exercised end to end against another release host; with it
# unset nothing differs. MAHBOT_INSTALL_VERSION names the version to install when
# the script is piped into `iex`, where no argument can reach it.

param(
    # Product-qualified on purpose: under the documented `irm … | iex` form this
    # runs in the caller's scope, where a name as ordinary as `Version` would be
    # created — or one of his own overwritten. `-Version` still binds, through the
    # alias, so the `-File` form is unchanged.
    [Alias('Version')]
    [string]$MahBotVersion
)

function Install-MahBot {
    param([string]$Version)

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    # A plain refusal: the sentence is the whole of what the owner is told, and it
    # stops the installation without stopping the session it was run from. Nested
    # here on purpose — a bare `Fail` left in the owner's session could shadow one
    # of his own.
    function Fail([string]$Message) {
        throw $Message
    }

    # ── The release ───────────────────────────────────────────────────────────

    # The base every download is built from: the repository this project publishes
    # to, unless the test-only hook the header names moved it.
    $ReleaseBase = [Environment]::GetEnvironmentVariable('MAHBOT_RELEASE_BASE_URL')
    if (-not $ReleaseBase) {
        $ReleaseBase = 'https://github.com/edezhic/mahbot'
    }

    $InstallDir = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'Programs\MahBot'
    $InstalledExe = Join-Path $InstallDir 'mahbot.exe'

    # Printed for every system the project publishes no file for: one prefix and the
    # plain refusal built from it, which the two floor sentences below extend.
    $NoFilePrefix = 'MahBot has no file for this system'
    $NoFile = "$NoFilePrefix, so nothing was installed."

    # ── 1. Refuse while the product is running ────────────────────────────────

    # Scoped to the data directory this home's product uses, so a product running
    # under another home is not this script's business. Opening the lock file
    # exclusively is how the platform itself proves it is in use: the product holds
    # it open for its whole run (`crate::util::lock`), so the open fails while it
    # does. A lock file that is not there is not a held one — that open would fail
    # for its absence, not for the product's hold — so only a file that exists is
    # worth trying. The directory is resolved the way the product resolves its own
    # storage root (`crate::config::default_config_dir`): HOME when the environment
    # has one, the account's home directory when it does not.
    $ProductHome = [Environment]::GetEnvironmentVariable('HOME')
    if (-not $ProductHome) {
        $ProductHome = $HOME
    }
    $LockPath = Join-Path $ProductHome '.mahbot\mahbot.lock'
    $Held = $false
    if (Test-Path -LiteralPath $LockPath) {
        try {
            $LockHandle = [IO.File]::Open($LockPath, 'Open', 'ReadWrite', 'None')
            $LockHandle.Close()
        } catch {
            $Held = $true
        }
    }
    if ($Held) {
        Fail "MahBot is already running; updating a running installation is MahBot's own update path, so nothing was installed."
    }

    # ── 2. Work out the system ────────────────────────────────────────────────

    # The machine's own architecture, not this process's: a 32-bit process on a
    # 64-bit system reports x86 and names the real one in PROCESSOR_ARCHITEW6432,
    # which is set nowhere else.
    $Processor = [Environment]::GetEnvironmentVariable('PROCESSOR_ARCHITEW6432')
    if (-not $Processor) {
        $Processor = [Environment]::GetEnvironmentVariable('PROCESSOR_ARCHITECTURE')
    }
    $Arch = switch ($Processor) {
        'AMD64' { 'x86_64' }
        'ARM64' { 'aarch64' }
        default { $null }
    }
    if (-not $Arch) {
        Fail $NoFile
    }

    # The build floor per architecture, read from the operating system's own record
    # of itself: `Win32_OperatingSystem` through CIM, else the registry value that
    # record is written from. `[Environment]::OSVersion` is deliberately not used for
    # it: on a 32-bit process it reports the version that process is told it runs on,
    # and without a compatibility manifest it reports 6.2.9200 whatever the machine
    # is — the same reason the product reads the build through `RtlGetVersion`
    # (src/self_update.rs). A build that cannot be read is not refused for: nothing
    # here is refused on a guess, and an unread `$Build` would otherwise compare as
    # below every floor. An ARM64 file exists, but Windows 10 on ARM — every build
    # before 22000 — is not a system it is built for.
    $Build = $null
    try {
        $Build = [int]((Get-CimInstance -ClassName Win32_OperatingSystem).Version.Split('.')[2])
    } catch {
        $Build = $null
    }
    if (-not $Build) {
        try {
            $Build = [int](Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name CurrentBuildNumber).CurrentBuildNumber
        } catch {
            $Build = $null
        }
    }
    if ($Build) {
        if ($Arch -eq 'aarch64') {
            if ($Build -lt 22000) {
                Fail "${NoFilePrefix}: Windows on ARM is supported from Windows 11 (build 22000) onwards, so nothing was installed."
            }
        } elseif ($Build -lt 17763) {
            Fail "${NoFilePrefix}: Windows 10 version 1809 (build 17763) or newer is required, so nothing was installed."
        }
    }

    # ── 3. The version to install ─────────────────────────────────────────────

    if (-not $Version) {
        $Version = [Environment]::GetEnvironmentVariable('MAHBOT_INSTALL_VERSION')
    }
    if (-not $Version) {
        # The newest release's tiny version file: its body is the version and a
        # newline. It names the newest release that is not a test release — and
        # nothing at all while only test releases exist, where one is installed by
        # naming its version instead.
        try {
            $Response = Invoke-WebRequest -Uri "$ReleaseBase/releases/latest/download/version.txt" -UseBasicParsing
            # A release file is served as a download rather than as text, so this
            # PowerShell hands the body back as bytes and a `Trim` on them throws;
            # as text it hands back a string. Both are read as the version, which is
            # what the file holds either way.
            $Body = $Response.Content
            if ($Body -is [byte[]]) {
                $Body = [System.Text.Encoding]::UTF8.GetString($Body)
            }
            $Version = ([string]$Body).Trim()
        } catch {
            Fail "the newest MahBot release could not be looked up, so nothing was installed (MAHBOT_INSTALL_VERSION names one particular release)."
        }
    }
    # A version named by hand — as this script's own argument, or through
    # MAHBOT_INSTALL_VERSION — may be spelled the way the release's own mark is
    # (`v0.7.0`), while the file's name and its address are built from the version
    # itself: the mark's own `v` is dropped here rather than landing in either.
    if ($Version) {
        $Version = $Version.Trim() -replace '^v', ''
    }
    if (-not $Version) {
        Fail "the MahBot release to install could not be determined, so nothing was installed."
    }

    # A pre-release part says plainly what it is: such a release is not what the
    # owner is given normally.
    if ($Version.Contains('-')) {
        Write-Host "The MahBot $Version release is a test release, not a normal one; installing it."
    }

    # ── 4. Download the file and put it in place ──────────────────────────────

    $Asset = "mahbot-$Version-windows-$Arch.zip"
    $Temp = Join-Path ([IO.Path]::GetTempPath()) ("mahbot-install-" + [Guid]::NewGuid().ToString('N'))
    # A half-written command is never left in place: the file is staged beside its
    # destination and moved onto it, so what sits at the destination is either the
    # version that was there or the new one whole — the same staging `install.sh` does.
    $Staged = Join-Path $InstallDir 'mahbot.exe.new'
    # What already sits at the destination is moved aside rather than overwritten:
    # Windows lets an image that is running be renamed but neither overwritten nor
    # deleted, so a product running from this folder — through another data directory
    # — is replaced exactly as it is on unix, the way the product's own swap replaces
    # a file that is in use there (`src/util/managed_bin.rs::rename_aside_swap`). The
    # copy moved aside is taken away once the new one is in place; while it is still
    # the running image that cannot be done, and a name a leftover still holds could
    # not be renamed onto again — so each run takes a name of its own, and sweeps the
    # ones earlier runs left behind.
    $Aside = Join-Path $InstallDir ('mahbot.exe.old-' + [Guid]::NewGuid().ToString('N'))
    # Whether the copy that was there could not be put back: only a restore that failed
    # leaves it so, and the sentence the owner is given must name that rather than the
    # generic one.
    $Lost = $false
    try {
        New-Item -ItemType Directory -Path $Temp -Force | Out-Null
    } catch {
        Fail "a temporary directory could not be created, so nothing was installed."
    }
    try {
        try {
            Invoke-WebRequest -Uri "$ReleaseBase/releases/download/v$Version/$Asset" -OutFile (Join-Path $Temp $Asset) -UseBasicParsing
        } catch {
            Fail "the MahBot $Version file for this system could not be downloaded, so nothing was installed."
        }
        try {
            Expand-Archive -LiteralPath (Join-Path $Temp $Asset) -DestinationPath $Temp -Force
        } catch {
            Fail "the downloaded MahBot file could not be opened, so nothing was installed."
        }
        try {
            New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
            Copy-Item -LiteralPath (Join-Path $Temp 'mahbot.exe') -Destination $Staged -Force
            if (Test-Path -LiteralPath $InstalledExe) {
                Move-Item -LiteralPath $InstalledExe -Destination $Aside
                try {
                    Move-Item -LiteralPath $Staged -Destination $InstalledExe
                } catch {
                    # Nothing is left broken: the copy that was there goes back, with
                    # the replace semantics the product's own rename-aside has.
                    try {
                        Move-Item -LiteralPath $Aside -Destination $InstalledExe -Force
                    } catch {
                        $Lost = $true
                    }
                    throw
                }
                Remove-Item -LiteralPath $Aside -Force -ErrorAction SilentlyContinue
            } else {
                Move-Item -LiteralPath $Staged -Destination $InstalledExe
            }
            # Asides earlier runs left behind go only now, with the new file already
            # in place: the copy in one can be the last on disk (a run interrupted
            # between its move-aside and its move-in leaves no `mahbot.exe` at all),
            # and nothing here takes a copy away before it has put one there. Files
            # only, and a failure is swallowed — the destination already holds the new
            # file, so a leftover that stays is not this install failing.
            try {
                Get-ChildItem -LiteralPath $InstallDir -Filter 'mahbot.exe.old*' -File -Force -ErrorAction SilentlyContinue |
                    Remove-Item -Force -ErrorAction SilentlyContinue
            } catch {
                # Nothing to say: a leftover that stays is harmless.
            }
        } catch {
            if ($Lost) {
                Fail "the MahBot $Version file could not be put in place, and the copy that was there could not be put back — it is gone and must be put back by hand."
            }
            Fail "the MahBot $Version file could not be put in place, so nothing was installed."
        }
    } finally {
        # No leftover staging or temp files, on either way out.
        Remove-Item -LiteralPath $Staged -Force -ErrorAction SilentlyContinue
        Remove-Item -LiteralPath $Temp -Recurse -Force -ErrorAction SilentlyContinue
    }

    # ── 5. Make the location visible ──────────────────────────────────────────

    # The owner's own `Path` value in his own environment key — the one the product
    # edits itself. It is read without expanding the references his entries contain
    # (`DoNotExpandEnvironmentNames`), and written back with the kind it already
    # has, so a value that is an ExpandString stays one and a reference he adds
    # later still expands. Only the folder's own absence is a reason to write: a
    # value that already names it is left exactly as it is, and one is never created
    # holding nothing — an empty `Path` entry names the current directory, which is
    # the last thing his search path needs.
    $Folder = ($InstallDir -replace '/', '\').TrimEnd([char]'\')
    $ValueWritten = $false
    $EnvironmentKey = $null
    try {
        $EnvironmentKey = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true)
        if (-not $EnvironmentKey) {
            throw 'the environment key could not be opened'
        }
        $HasValue = $false
        foreach ($Name in $EnvironmentKey.GetValueNames()) {
            if ($Name -ieq 'Path') { $HasValue = $true }
        }
        if ($HasValue) {
            $Raw = $EnvironmentKey.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
            $Kind = $EnvironmentKey.GetValueKind('Path')
            if (($Kind -ne [Microsoft.Win32.RegistryValueKind]::String) -and ($Kind -ne [Microsoft.Win32.RegistryValueKind]::ExpandString)) {
                throw 'the Path value is not a string one'
            }
        } else {
            $Raw = $null
            # The kind the system's own writers give a value that is not there yet.
            $Kind = [Microsoft.Win32.RegistryValueKind]::ExpandString
        }

        # Compared the way the product compares an entry (`src/util/owner_path.rs`):
        # case-insensitively, with either of a path's separators and a trailing one
        # naming the same folder, and with the `%REF%`s an entry holds resolved — so
        # a value that spells the folder through a reference is recognised rather
        # than gaining a second entry naming it. Resolution is for recognising only:
        # nothing is ever written out expanded. A value that already names the
        # folder is written back not at all, so it stays byte for byte what he made
        # it.
        $AlreadyThere = $false
        if ($null -ne $Raw) {
            foreach ($Entry in $Raw.Split(';')) {
                foreach ($Spelling in @($Entry, [Environment]::ExpandEnvironmentVariables($Entry))) {
                    if ((($Spelling -replace '/', '\').TrimEnd([char]'\')) -ieq $Folder) {
                        $AlreadyThere = $true
                    }
                }
            }
        }
        if (-not $AlreadyThere) {
            # `%` expands and `;` splits a search-path value, so a folder holding
            # either would not name itself once written — the product's own edit of
            # his `Path` refuses exactly that (`src/util/owner_path.rs`). Said
            # plainly, and the value is left as it is.
            if ($Folder.IndexOfAny([char[]]'%;') -ge 0) {
                Write-Host "MahBot's own folder holds a character a search-path entry cannot carry, so it was not added to the owner's own search path."
            } else {
                if ($null -eq $Raw) {
                    # No value at all: the folder is the whole of it, and no empty entry
                    # is anywhere in the value.
                    $NewValue = $Folder
                } else {
                    # His value is there, so every entry it holds stays as it is — an
                    # empty one included, which names the current directory and is his
                    # own arrangement rather than ours to tidy — and the folder follows
                    # them, which is what the product's own edit does.
                    $NewValue = $Raw + ';' + $Folder
                }
                $EnvironmentKey.SetValue('Path', $NewValue, $Kind)
                $ValueWritten = $true
            }
        }
    } catch {
        Write-Host "the owner's own search path could not be updated, so a terminal may not find MahBot until MahBot itself brings it there."
    } finally {
        if ($EnvironmentKey) { $EnvironmentKey.Close() }
    }

    # Tell the programs already running that the environment changed — the same
    # broadcast `setx` makes (`src/util/owner_path.rs`), so a terminal opened
    # afterwards sees the new value without a logoff. Best effort, and inline, so
    # nothing has to be downloaded to make it.
    if ($ValueWritten) {
        try {
            if (-not ('Win32.NativeMethods' -as [type])) {
                Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition @'
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto, SetLastError = true)]
public static extern System.IntPtr SendMessageTimeout(System.IntPtr hWnd, uint Msg, System.UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out System.UIntPtr lpdwResult);
'@
            }
            $BroadcastResult = [System.UIntPtr]::Zero
            [void][Win32.NativeMethods]::SendMessageTimeout([System.IntPtr]0xffff, 0x001A, [System.UIntPtr]::Zero, 'Environment', 0x0002, 5000, [ref]$BroadcastResult)
        } catch {
            Write-Host "the change to the owner's environment could not be announced to the programs already running."
        }
    }

    # ── 6. The start menu entry ───────────────────────────────────────────────

    # Windows' own shortcut object, pointed at the file just installed. One that
    # cannot be made is said plainly and changes nothing else.
    try {
        $Shell = New-Object -ComObject WScript.Shell
        $Shortcut = $Shell.CreateShortcut((Join-Path ([Environment]::GetFolderPath('Programs')) 'MahBot.lnk'))
        $Shortcut.TargetPath = $InstalledExe
        $Shortcut.Save()
    } catch {
        Write-Host 'the start menu entry for MahBot could not be created.'
    }

    # ── 7. Remove the copy the old way of installing left behind ──────────────

    # Installing from the package registry (`cargo install mahbot`) puts the command
    # in the toolchain's own directory. That copy is the owner's file, so a removal
    # that fails is said plainly and the install goes on.
    $CargoHome = [Environment]::GetEnvironmentVariable('CARGO_HOME')
    if (-not $CargoHome) {
        $CargoHome = Join-Path $HOME '.cargo'
    }
    $Legacy = Join-Path (Join-Path $CargoHome 'bin') 'mahbot.exe'
    # The two locations compared as the files they are, not as text: a `CARGO_HOME`
    # spelled with a redundant separator or another capitalisation names the very file
    # just installed, and removing that would leave the owner with nothing at all.
    if ((Test-Path -LiteralPath $Legacy) -and
        ([IO.Path]::GetFullPath($Legacy) -ine [IO.Path]::GetFullPath($InstalledExe))) {
        try {
            Remove-Item -LiteralPath $Legacy -Force
        } catch {
            Write-Host "the copy the old way of installing left at $Legacy could not be removed; MahBot is installed at $InstalledExe."
        }
    }

    # ── 8. Smart App Control, when it is on ───────────────────────────────────

    # Said honestly rather than silently: the policy lets no exception be made for a
    # single program, so the only remedy is turning it off altogether — and it
    # blocks the browser helper and the runtime MahBot installs besides.
    $SmartAppControl = $null
    try {
        $SmartAppControl = (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\CI\Policy' -Name 'VerifiedAndReputablePolicyState' -ErrorAction Stop).VerifiedAndReputablePolicyState
    } catch {
        $SmartAppControl = $null
    }
    if ($SmartAppControl -eq 1) {
        Write-Host 'Smart App Control is on, and it allows no exception for a single program: the only remedy is turning it off wholesale, and it also blocks the browser helper and the runtime MahBot installs.'
    }

    # ── 9. Start the product ──────────────────────────────────────────────────

    try {
        Start-Process -FilePath $InstalledExe
    } catch {
        Fail 'MahBot could not be started.'
    }
}

# ── Run ───────────────────────────────────────────────────────────────────────

# A failure prints its one sentence and nothing else happens. No exit code is set
# from here on purpose: whether this file is itself the program being run cannot be
# told from the script's own scope without guessing — a body piped into a session
# carries the enclosing script's own command — and guessing wrong runs `exit` in the
# owner's session, closing his window. The sentence is the whole of what he is told,
# in every form this script is run in.
try {
    Install-MahBot -Version $MahBotVersion
} catch {
    [Console]::Error.WriteLine($_.Exception.Message)
}
# A session this was piped into keeps nothing of the run: the function and the
# version it was told are all this script adds to it, so both go.
Remove-Item -Path Function:\Install-MahBot -ErrorAction SilentlyContinue
Remove-Variable -Name MahBotVersion -ErrorAction SilentlyContinue