# nobrainer-tech-flow 2.3.2: install, update and undo without false alarms or leftovers

Release date: 2026-10-18.

Run undo twice and the second run no longer fails. Undo leaves your profile as it was
before the setup, including the empty folders the installer had to create. An update
through `install.sh` delivers the new instruction block. A repeat apply says that
nothing changed instead of asking you to restart your client. These six fixes came
from a test of a fresh install that followed the README commands on 2.3.1. A review of
them and a Windows test run found the further fixes listed after them: undo now also
works after you edit the instruction file, the default block no longer names a private
skill, the npm release copy moves out of the folder a Git clone may use, and the
Windows hook wrapper no longer runs a `bash.exe` from the current folder.

## What changes

- **A second undo is a clear no-op.** `--undo` and `--undo --apply` with no setup
  record print `NOTHING TO UNDO`, name the client and the record they looked for,
  write nothing and exit 0, in `install.sh`, `install.py` and the npm command. In
  2.3.1 they exited 3 with `rollback state not found`. A damaged or unreadable
  record, a folder or link where the record should be, and a record that only the
  Python undo can read still stop with an error. So does a record in a folder this
  account cannot search, such as a home without search permission while
  `CLAUDE_CONFIG_DIR` points elsewhere: undo exits 3 instead of `NOTHING TO UNDO`.
- **Undo removes the folders the setup created.** The setup record gains
  `created_dirs`, the folders the setup had to create (links resolved, an empty list
  included). Undo removes each one that is still empty, deepest first. A folder that
  existed before the setup, or holds anything else, stays. A record from 2.3.1 or
  earlier has no list and keeps the earlier rule, so its undo removes only an empty
  skills folder. A list that names a folder the setup cannot have made (a relative
  path, `..`, the root, a folder beside the setup's own, a duplicate, an entry that
  is not text) is refused with exit 3 and nothing is changed. The `.bak` copy the
  setup made of an instruction file you already had is kept, as before.
- **`install.sh` replaces a block of an earlier version.** When the managed block
  differs from this version's text, `install.sh` replaces it from the start marker to
  the end marker and keeps every byte outside them (line endings, a byte order mark,
  a missing final line break). The file as it was is saved as a `.bak` copy first,
  the preview shows the lines that change, and undo still removes the block or puts
  back the file from before the setup. A block that carries an option (a standing
  authorization, a saved wiki root or a preference), or whose markers do not stand on
  lines of their own, is still left as it is with a note that points to
  `install_personalization.py --keep-options`; `install.py` keeps rewriting such
  blocks and keeping their options.
- **The block names the module the way the client lists it.** Codex 0.157.1 lists the
  module as `nobrainer-tech-flow:nobrainer-tech-flow` when a plugin manifest sits
  above the folder a link resolves to, which is the case for a git checkout and for the
  npm package, and as `nobrainer-tech-flow` for a copy. It adds the module text to the
  request only for the name it lists. The entrypoint line of the block and the install,
  naming, testing and compatibility docs now say to use the listed name and give both
  Codex forms. The README and the repository instruction files (`AGENTS.md` and
  `CLAUDE.md`) give the same rule. Existing blocks differ from the new text, so the
  next apply updates them (`install.sh` through the replacement above, `install.py`
  as before).
- **A repeat apply says nothing changed.** When no link and no instruction file was
  written, apply prints `UP TO DATE` and no restart hint. `INSTALLED` and the restart
  hint stay for a run that changed something.
- **The setup record and file modes are documented.** The installer writes
  `.nobrainer-flow-onboarding-<client>.json` in the chosen home with mode 0600. A new
  instruction file is created with mode 0600 as well; an existing file keeps its mode
  and so does its `.bak` copy. `install.sh` now also updates a read-only file (mode
  0444) and leaves it read-only, as the Python installer already did; it used to stop
  with exit 4. When the setup record cannot be saved, the run puts such a file back as it was
  instead of leaving the new block behind. Its undo takes the block out of such a file the same way and leaves it
  read-only; it used to stop with exit 4 after removing the links. Undo deletes the record. [Installation](../INSTALL.md)
  describes the file and its fields.

- **Undo removes the skill links even after you edited the instruction file.** In
  2.3.1 an edit to the instruction file after the setup made undo stop with exit 3
  before it unlinked anything. Now `install.sh`, `install.py`, the npm command and the
  guided `--rollback` remove the links the setup made, print `PARTLY UNDONE`, leave the
  edited file exactly as it is (managed block included) and still exit 3. The record
  stays, with no links in it, and a hint says how to take the block out as well: apply
  again, which records the file as it is now, then undo again; your edits outside the
  block stay. Deleting the lines between the markers by hand works too: when the file
  holds neither marker any more, the next undo finishes, deletes the record and the
  empty folders the setup made, and leaves the file as it is. A missing `.bak` copy no
  longer stops the undo of an edited file, since that file does not get the block back;
  `install.sh` and the guided undo used to stop on it before unlinking anything. With
  that copy gone or unreadable, the hint offers only the removal by hand, because
  applying again keeps that copy in the record and the undo after it would stop on it. A link
  that changed since the setup still stops the undo before anything changes. So does
  an instruction file or `.bak` copy the undo cannot read: both undos stop with exit 3
  and an `ERROR: cannot read` line before removing a link or the record. `install.sh`
  used to take an unreadable file for one without the block, remove every link and
  delete the record while the block stayed.
- **The default block no longer names a private skill.** The last line of the block
  told every user to load `nobrainer-ak` (`nbak`), which is not one of the eighteen
  modules. It now reads: use `nobrainer-writing` for material writing when available,
  with the same guidance on technical documentation, facts, language and verification.
  A block written by 2.3.1 is still recognised as a Flow-managed block of an earlier
  version: the next apply replaces it (through `install.sh` only when the block holds
  no option; `install.py` always, keeping its options), and undo gives back the file
  from before.
- **npm keeps its release copy beside `~/.nobrainer-tech-flow`, not inside it.** The
  npm command now copies the release to `~/.nobrainer-tech-flow-npm/<version>` (under
  `--home` when given). The README suggests a Git clone in `~/.nobrainer-tech-flow`,
  and a `git clean -fdx` there deleted the copy that the skill links point to. No
  published release wrote the new folder before; an undo that finds only an earlier
  release's copy in `.nobrainer-tech-flow/npm` says so and asks you to undo with the
  release that made the setup.
- **The Windows hook wrapper finds Git Bash safely.** `hooks/run-hook.cmd` looks for
  Git Bash in its install folders (`%ProgramFiles%`, `%ProgramFiles(x86)%`,
  `%LOCALAPPDATA%\Programs`), then beside the `git.exe` on PATH, then for `bash.exe`
  in the PATH folders only. It no longer runs a `bash.exe` from the current folder. It
  returns the hook's own exit code; it used to report the code from before bash ran.
  Without bash it prints a message on stderr and exits 1, as `session-start` does when
  it cannot add the bootstrap, so the client reports a hook error that blocks nothing
  and the session continues.
- **Undo handles damaged or rearranged profiles more safely.** A folder in the record
  that now lies under a link (for example `~/.config` replaced by a link after the
  setup) is left where the link points, with a `PRESERVED` line; both undos used to
  remove empty folders of the same name in the other tree. A home that did not exist
  and was made only to hold the record, because `CLAUDE_CONFIG_DIR` or
  `XDG_CONFIG_HOME` put the skills elsewhere, is now listed and removed by undo.
  That home is made before anything else is written, so when it cannot be made the setup
  stops without leaving links or a block that no record describes. A repeat apply keeps the folders an earlier run recorded as they were
  recorded; it used to resolve them again, so a folder swapped for a link to another
  tree passed that tree's folders to the undo, which removed them. A client variable
  that reaches an existing folder through one that does not exist yet (for example
  `CLAUDE_CONFIG_DIR=profile/missing/../config`) no longer gets that existing folder
  recorded as made by the setup, so the undo removes `missing` and `config` and keeps
  `profile`.
  `install.sh` treats a dangling link in place of the older
  `.nobrainer-flow-onboarding.json` as a damaged record and stops with exit 3 instead
  of `NOTHING TO UNDO`, and a record that names a path without a leading slash is
  refused as damaged with exit 3; `install.sh` used to hang on it. On Windows the guided
  undo treats a directory junction like a link and leaves it, with the folder it points
  to; it used to remove the junction. The npm undo looks for a record before it inspects the retained
  copy, so a partial or changed copy no longer blocks the `NOTHING TO UNDO` no-op. In
  Git Bash a recorded folder with `..` is refused as in the other installers; `cygpath`
  used to remove the `..` before the check. The Windows smoke tests also compare the
  paths Git Bash prints by their end, since it spells them its own way.

Both installers behave the same. The scripts, the npm command that calls
`install.py` and the guided `--rollback` read and write one record.

### Instruction fixes

Five instruction fixes from a review of 2.3.1 also ship. The README leads with the
Git install while the npm package is unpublished. Flow owns one corrective-attempt
budget (two attempts per observed failure), and Build and RCA link to it instead of
saying three. Dispatcher accepts the worker ID a host returns for a hidden native
worker as the send receipt, so default delegation can be dispatched; visible sessions
still go through Sessions. Saying "Use NBFlow" turns on session health observation
only: a restart or archive needs separate owner consent, and the quick path skips
startup care. Stale status lines, an undefined "team cap" and two lifecycle names are
corrected, and every approval-gate list includes contacting people.

## Verification record

Local checks on the installer and 2.3.2 metadata (macOS, Python 3.14.5):

- `python3 scripts/validate_skills.py` and
  `python3 scripts/validate_skills.py --suite`: both passed.
- `python3 -m unittest tests.test_shell_installer -v`: 55 tests passed with
  `/bin/sh`, dash, bash in POSIX mode and zsh emulating sh. The new cases cover the
  second undo, the folder list, the block replacement (CRLF line endings, a byte
  order mark and a missing final line break included), the `UP TO DATE` message
  and the file modes, and check that undo gives back the profile as it was.
- `tests.test_installer` (55 tests), `tests.test_one_command_installer` (29),
  `tests.test_personalization` (36), `tests.test_guided_onboarding` (30, 1 skipped) and
  `tests.test_npm_distribution` (7) passed.
- `python3 -m unittest discover -s tests -v`: 434 tests ran in 1261 seconds; all
  passed except 2 platform-specific skips. This includes the exported-source
  acceptance check without Git metadata.
- An end-to-end run of the 2.3.1 and the 2.3.2 scripts in disposable homes, under
  a macOS sandbox that allowed writes only to a scratch folder and no network: 44
  scenarios and 186 installer runs for Codex and OpenCode, with `install.sh`,
  `install.py` and the npm command, on an empty profile and on one that already had
  a mode 0640 instruction file. With 2.3.2, apply, apply again (`UP TO DATE`, no
  restart hint), undo (the profile equal to the starting one apart from the kept
  `.bak` copy, and no `~/.agents` or `~/.config/opencode` left) and a second undo
  (`NOTHING TO UNDO`, exit 0, writes to the home denied by the sandbox) behaved as
  described in all 8 combinations. The same scenarios on 2.3.1 ended the second
  undo with exit 3, and undo left the empty folders in the 6 combinations where
  the setup had created them. Updating a 2.3.1 setup in place with 2.3.2 replaced
  the block in all 4 runs with every other byte unchanged, and undo gave back the
  file from before the setup. A non-empty `~/.agents` stayed after undo.
- Syntax checks of `scripts/install.sh` in the four shells and `shellcheck -S error`
  passed. The changed Python files compiled, and the npm command read back
  `2.3.2`.
- A readback of how Codex 0.157.1 names and loads the module, with no model
  involved, as described in the compatibility record below.
- A current-tree secret scan with the repository's gitleaks configuration found
  no leaks.

After the review fixes (macOS, Python 3.14.5):

- `python3 scripts/validate_skills.py` and `--suite`: both passed.
- `python3 -m unittest discover -s tests`: 443 tests ran; all passed except 4
  platform-specific skips. The new regression tests that run on macOS failed on the
  code before their fixes, except the check of the hook wrapper's shell half, which was
  already correct.
- `bash -n`, `sh -n` and `dash -n` on `scripts/install.sh` and `hooks/run-hook.cmd`, and
  `shellcheck -S error` on `scripts/install.sh`, passed.
- The batch half of `hooks/run-hook.cmd` and the Git Bash path checks run only on
  Windows; the Windows smoke job in CI is their proof, not this local run.

After the undo review fixes (macOS, Python 3.14.5):

- `python3 -m unittest tests.test_shell_installer tests.test_installer
  tests.test_one_command_installer`: 150 tests passed. `tests.test_guided_onboarding`
  and `tests.test_personalization`: 67 tests passed, 1 skipped. The three new checks
  (a folder reached through `..`, an unreadable instruction file or backup, and the
  undo of a read-only file) failed on the code before their fixes in 9 subtests.
- `python3 scripts/validate_skills.py` and `--suite`, `bash -n`, `sh -n` and `dash -n`
  on `scripts/install.sh`, `shellcheck -S error` and `git diff --check`: all passed.

These checks prove local installer behavior and package metadata. They do not
prove publication, a fresh target-client import or a performance gain.

## Scope and distribution

This is an installer, documentation and instruction-consistency fix. The module set
stays at eighteen; module text changes only where the instruction fixes above resolve
a contradiction. No model-routing or performance change is claimed.

Two limits remain. The guided `recommend_flow_setup.py --rollback` with no record
still exits 3 with `rollback state not found`, and an npm undo that finds a record but
no retained release files still exits 3. Neither is part of the second-undo case.

The Codex naming check covers Codex 0.157.1 on macOS, with a typed name and a linked
mention sent through `codex exec` to a local stand-in server. It does not show what a
model does when it sees the plain form in a listing that names the module under the
plugin, and it did not drive the terminal or desktop mention pickers, other Codex
versions or operating systems, or a plugin-channel install. The
[compatibility record](../COMPATIBILITY.md) has the readback.

The native plugin 2.3.2 will carry the same installer. Plugin export, publication and
target-client import each require their own readback. npm registry publication is
still pending; source release files and a locally packed tarball do not establish
registry availability.

## Rollback

Return to the reviewed v2.3.1 source through the documented preview and undo flow.
Undo a 2.3.2 setup with 2.3.2 scripts when you can. The 2.3.1 `install.sh` cannot read
a 2.3.2 record and points you to the Python undo. That undo reads the record and
removes the links, the block and the record, but it does not know the folder list, so
the empty folders the setup created stay. A setup recorded by 2.3.1 and undone by
2.3.2 leaves them too, because its record has no folder list. Keep retained source
files while any client links refer to them.
