diff --git a/README.md b/README.md
index 0716433..1062488 100644
--- a/README.md
+++ b/README.md
@@ -1,217 +1,68 @@
-> Caboodle is becoming **Tambo**. The transition release includes both `tambo`
-> and `caboodle`, backed by the same CLI. Existing plan and state paths remain
-> compatible. New release archives use `tambo-vVERSION-TARGET.tar.gz` with a
-> sibling SHA-256 checksum; supported archives contain both commands.
->
-> Repository and registry publication, package-manager migration, and installed
-> host verification are separate release steps. The compatibility command stays
-> available for at least one release.
+

-
-
-
+# Tambo
-
-
-
-
-tambo
-
-
- π§° Agent Platform Toolkit - installs and configures the quipu stack
-
-
-
-
-
-
-
-
-**Caboodle installs and configures a stack built around Quipu, a governed,
-bitemporal knowledge store for people and AI coding agents.** Shared facts,
-decisions and work history stay available for your agents to query. Its CLI
-installs the tools from a reviewable plan and proves each one works; an agent
-skill guides you through the same workflow.
-
-## Why you would want it
-
-- Give your agent memory and code context that survive a session.
-- Review one plan, then resume installation if it is interrupted.
-- Know each tool passed a functional check before you rely on it.
-
-[What the quipu stack is, and three ways to start](https://scbrown.github.io/caboodle/introduction.html).
+Install the Quipu stack from a plan you can review, then prove the selected tools
+work. Give an agent durable memory and code context, without assembling every tool
+by hand. Tambo is a CLI and [agent skill](skills/tambo/SKILL.md).
+The compatibility command `caboodle` remains available.
## Install
-### Checksummed archives
-
-Checksummed releases: Linux x86_64, macOS arm64 and macOS x86_64.
-The installer verifies SHA256 before placing the binary in `~/.cargo/bin`.
-
-```bash
-curl -fsSL https://raw.githubusercontent.com/scbrown/caboodle/main/scripts/install.sh | bash
-```
-
-To pin a release, pass the version to the receiving shell:
-
```bash
-curl -fsSL https://raw.githubusercontent.com/scbrown/caboodle/main/scripts/install.sh | CABOODLE_VERSION=vX.Y.Z bash
+curl -fsSL https://raw.githubusercontent.com/scbrown/tambo/main/install.sh | sh
```
-Add the install directory to this shell's PATH:
-
-```bash
-export PATH="${CARGO_HOME:-$HOME/.cargo}/bin:$PATH"
-```
-
-The `export` lasts for this shell only. To keep it, add the same line once to
-your shell's startup file: `~/.bashrc` for bash, `~/.zshrc` for zsh (the macOS
-default). For bash:
-
-```bash
-echo 'export PATH="${CARGO_HOME:-$HOME/.cargo}/bin:$PATH"' >> ~/.bashrc
-```
-
-Apps started outside a terminal, such as the Claude desktop app or an IDE, may
-not read that file, which is why the agent step below registers servers by
-absolute path.
-
-Or build the latest release from source with Rust (`cargo install --git
-https://github.com/scbrown/caboodle --tag vX.Y.Z --locked`). Either way,
-`caboodle --version` prints `caboodle X.Y.Z` for the release you installed.
-
-### macOS with Homebrew
-
-```bash
-brew install scbrown/tap/caboodle
-caboodle --version
-```
-
-Homebrew owns the installed command; Caboodle still composes and verifies the
-stack. Update it with `brew upgrade scbrown/tap/caboodle`. To hold the installed
-version, use `brew pin caboodle`; resume updates with `brew unpin caboodle`.
-The [tap channel guide](https://github.com/scbrown/homebrew-tap#caboodle-preview-channel)
-explains the opt-in nightly channel and switching back to stable.
+Downloads the latest release, verifies its SHA-256 and version, and installs
+`tambo` and `caboodle` in `${CARGO_HOME:-$HOME/.cargo}/bin`. Requires `curl`,
+`tar`, Python 3 and `sha256sum` or `shasum`. Add that directory to `PATH`.
+Published archives support Linux x86_64 and macOS arm64/x86_64; **Linux arm64
+has no published archive**. Source builds, pinned versions, removal and the
+Homebrew alternative are in the [installation reference](https://scbrown.github.io/caboodle/installation-reference.html).
## First success: local, single-user
-After installing Tambo, run this in an empty directory you will keep:
+Run in an empty directory you intend to keep:
```bash
tambo quickstart
```
-Tambo saves a retrieval plan, installs Quipu, Camayoc and Bobbin, and verifies
-all three. There are no interview questions, issued tokens, accounts or running
-server prerequisites. Your knowledge store is local at `.caboodle/quipu.db`.
-Keep `caboodle-plan.toml` and `.caboodle/` to resume or update the same install.
-An existing plan is reused and validated; quickstart never replaces it.
-
-**Success check:** exit 0 and `quipu: verified`, `camayoc: verified`, and
-`bobbin: verified`. Each tool passes an isolated functional round trip, including
-a negative control and a read after writing. Recheck with one command:
+This saves a reviewed retrieval plan, installs Quipu, Camayoc and Bobbin, and
+checks each tool. Success is exit 0 and `quipu: verified`, `camayoc: verified`,
+`bobbin: verified`. Keep `caboodle-plan.toml` and `.caboodle/` to resume.
+Recheck the same install:
```bash
tambo verify
```
-Quipu's verification uses a temporary embedded database. Camayoc's check starts
-and stops its own temporary loopback server; you do not supply or manage one.
-This proves the installed tools, not a connection to a remote knowledge service.
-Optional Shantytown hook messages do not require a crew installation.
+### Advanced: shared services and agents
-### Advanced: connect to a shared, authenticated Quipu server
+For issued credentials, remote stores and registering Bobbin with your agent,
+use the [agent and installation reference](https://scbrown.github.io/caboodle/installation-reference.html).
+The default quickstart is local and needs no account or remote server.
-Remote mode is opt-in: set `QUIPU_SERVER` to the server you intend to use, or
-create a plan with `--quipu-mcp-url`. Obtain an administrator-issued token and
-provision it before verification:
+From the code repository you want searchable, initialize and index it before
+registering the server by absolute path:
```bash
-tambo provision-quipu-token --from /secure/issued-token --server https://your-quipu-server.example
-export QUIPU_SERVER=https://your-quipu-server.example
-tambo verify
-```
-
-Remote verification requires the canonical credential with `0400/0700`
-permissions, proves that unauthenticated access is refused and the selected
-credential is accepted, and refuses token rotation. Missing or rejected
-credentials stop verification. Never put a token in the plan or a server URL.
-Unset `QUIPU_SERVER` to use the local default; an explicit `quipu_mcp` plan
-remains remote until you remove that configuration deliberately.
-
-To customize the tools and intended-use contract instead, use
-`tambo init --guided` or `tambo plan --profile retrieval --intent caboodle-intent.toml`,
-review the resulting plan, then run `tambo install`. See the
-[walkthrough](https://scbrown.github.io/caboodle/getting-started.html).
-
-## On your own code
-
-| I want to⦠| Command |
-|---|---|
-| Choose tools and describe my own use | `caboodle init --guided` |
-| Find install blockers before changing anything | `caboodle doctor` |
-| Install and prove the reviewed selection | `caboodle install` |
-| Recheck the selected tools | `caboodle verify` |
-| Compare against this build's reviewed versions | `caboodle check-updates` |
-
-[Full command and configuration reference](https://scbrown.github.io/caboodle/reference.html).
-
-## Wire it into your agent
-
-Tambo is a CLI, with an [agent skill](skills/tambo/SKILL.md), not an MCP
-server. Bobbin, which the `retrieval` profile installed above, is one. It
-searches one repository: the code you want your agent to work on, such as your
-own project's git checkout. Do not use `~/caboodle`, which holds only the plan.
-Index that repository, then register bobbin from inside it:
-
-```bash
-cd ~/path/to/your/repo
bobbin init
bobbin index
claude mcp add bobbin -- "$(command -v bobbin)" serve "$PWD"
```
-`$(command -v bobbin)` and `$PWD` are expanded once, now, so Claude Code
-starts the server by absolute path and serves this repository whatever its own
-`PATH` or working directory. Registering a bare `bobbin` fails with `ENOENT` when
-Claude Code was not started from a shell that has `~/.cargo/bin` on `PATH`.
-
-`claude mcp add` uses the default `local` scope: the server is registered for
-this project only, so start Claude Code in this repository to see it (`claude
-mcp list` there shows `bobbin: ... β Connected`). Repeat the four lines in each
-repository you want searchable. `bobbin init` creates `.bobbin/`
-in the repository and, if the repository has a `.gitignore`, appends `.bobbin/`
-to it; review that change before you commit. Without `bobbin init`, `bobbin
-serve` exits with `Bobbin not initialized` and Claude Code reports the server as
-failed to connect.
-
-MCP (Model Context Protocol) lets your agent call the installed server.
-[Agent setup](https://scbrown.github.io/caboodle/agents.html) covers indexing, Yupana, other clients
-and managed crew registration through `caboodle project-settings`.
-
-## Before you start
-
-| Platform | Caboodle | Quipu | Yupana |
-|---|---|---|---|
-| Linux x86_64 | Release | Release | Release |
-| macOS arm64 / x86_64 | Release | Release | Release |
-| Linux arm64 | Build with Rust | Release | Build with Rust |
-
-Have `curl`, `tar`, `git`, `bash`, `python3` and `sha256sum` on PATH
-(`brew install coreutils` supplies `sha256sum` on macOS).
-The `everything` profile also needs Go. No account or remote server is needed.
-[Installation details](https://scbrown.github.io/caboodle/installing.html) cover source builds,
-installed files and removal; `caboodle doctor` names missing prerequisites.
+## Where to go next
-## What's next
+- [Choose a profile](https://scbrown.github.io/caboodle/profiles.html) or use `tambo init --guided`.
+- [Camayoc](https://github.com/scbrown/camayoc): vocabulary, shapes and governed agent memory.
+- [Getting started](https://scbrown.github.io/caboodle/getting-started.html) and [command reference](https://scbrown.github.io/caboodle/reference.html).
+- **Load this repository's graph:** repository-pendant publication is pending.
+ When a release provides the archive, checksum and provenance, follow Quipu's
+ [verified import workflow](https://scbrown.github.io/quipu/sharing/index.html).
+ Import stages knowledge; it does not promote it into ROOT automatically.
-- [Read the book](https://scbrown.github.io/caboodle/introduction.html)
-- [Find a topic in the docs map](https://scbrown.github.io/caboodle/docs-map.html)
-- [Choose your profile](https://scbrown.github.io/caboodle/profiles.html)
-
-## π§Ί The stack
-
-Caboodle installs these together and proves each one works; every tool also stands alone.
+## The stack
@@ -222,9 +73,11 @@ when.
| tool | what it gives your agents |
|---|---|
-| [caboodle](https://github.com/scbrown/caboodle) | π§° Agent Platform Toolkit - installs and configures the quipu stack |
+| [tambo](https://github.com/scbrown/tambo) | π§° Agent Platform Toolkit - installs and configures the quipu stack |
| [quipu](https://github.com/scbrown/quipu) | stores shared facts with provenance and history; can check writes against your rules |
| [camayoc](https://github.com/scbrown/camayoc) | prepares knowledge and vocabulary for Quipu, with checks before it is admitted |
+| [quechua](https://github.com/scbrown/quechua) | shared vocabulary and shapes for interoperable knowledge |
+| [chaski](https://github.com/scbrown/chaski) | reacts to graph changes and delivers governed notifications |
| [bobbin](https://github.com/scbrown/bobbin) | searches your code and can include related knowledge from Quipu |
| [yupana](https://github.com/scbrown/yupana) | shows code impact and can read policies from Quipu to check changes |
| [desire-path](https://github.com/scbrown/desire-path) | turns agent mistakes into feature requests; works on its own without Quipu |
@@ -237,16 +90,6 @@ when.
## Contributing
-```bash
-just book
-just check
-just docs-check
-```
-
-## π License
-
-[MIT](LICENSE)
+Run `just check`, `just book` and `just docs-check`.
-
-
-
+[MIT license](LICENSE).
diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md
index 27f8b77..4592478 100644
--- a/docs/book/src/SUMMARY.md
+++ b/docs/book/src/SUMMARY.md
@@ -4,6 +4,7 @@
- [Getting started](getting-started.md)
- [Installing and removing](installing.md)
+ - [Installation and agent reference](installation-reference.md)
- [Using it with agents](agents.md)
- [Using Caboodle](using.md)
- [Reference](reference.md)
diff --git a/docs/book/src/installation-reference.md b/docs/book/src/installation-reference.md
new file mode 100644
index 0000000..4b859cb
--- /dev/null
+++ b/docs/book/src/installation-reference.md
@@ -0,0 +1,171 @@
+# Installation and agent reference
+
+For Debian/Ubuntu packages, use the [signed apt repository guide](https://github.com/scbrown/apt#channels-and-pinning), including its fingerprint check, allowed update origins and rollback. The archive installer below is independent of apt.
+
+
+### Checksummed archives
+
+Checksummed releases: Linux x86_64, macOS arm64 and macOS x86_64.
+The installer verifies SHA256 before placing the binary in `~/.cargo/bin`.
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/scbrown/caboodle/main/scripts/install.sh | bash
+```
+
+To pin a release, pass the version to the receiving shell:
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/scbrown/caboodle/main/scripts/install.sh | CABOODLE_VERSION=vX.Y.Z bash
+```
+
+Add the install directory to this shell's PATH:
+
+```bash
+export PATH="${CARGO_HOME:-$HOME/.cargo}/bin:$PATH"
+```
+
+The `export` lasts for this shell only. To keep it, add the same line once to
+your shell's startup file: `~/.bashrc` for bash, `~/.zshrc` for zsh (the macOS
+default). For bash:
+
+```bash
+echo 'export PATH="${CARGO_HOME:-$HOME/.cargo}/bin:$PATH"' >> ~/.bashrc
+```
+
+Apps started outside a terminal, such as the Claude desktop app or an IDE, may
+not read that file, which is why the agent step below registers servers by
+absolute path.
+
+Or build the latest release from source with Rust (`cargo install --git
+https://github.com/scbrown/caboodle --tag vX.Y.Z --locked`). Either way,
+`caboodle --version` prints `caboodle X.Y.Z` for the release you installed.
+
+### macOS with Homebrew
+
+```bash
+brew install scbrown/tap/caboodle
+caboodle --version
+```
+
+Homebrew owns the installed command; Caboodle still composes and verifies the
+stack. Update it with `brew upgrade scbrown/tap/caboodle`. To hold the installed
+version, use `brew pin caboodle`; resume updates with `brew unpin caboodle`.
+The [tap channel guide](https://github.com/scbrown/homebrew-tap#caboodle-preview-channel)
+explains the opt-in nightly channel and switching back to stable.
+
+## First success: local, single-user
+
+After installing Tambo, run this in an empty directory you will keep:
+
+```bash
+tambo quickstart
+```
+
+Tambo saves a retrieval plan, installs Quipu, Camayoc and Bobbin, and verifies
+all three. There are no interview questions, issued tokens, accounts or running
+server prerequisites. Your knowledge store is local at `.caboodle/quipu.db`.
+Keep `caboodle-plan.toml` and `.caboodle/` to resume or update the same install.
+An existing plan is reused and validated; quickstart never replaces it.
+
+**Success check:** exit 0 and `quipu: verified`, `camayoc: verified`, and
+`bobbin: verified`. Each tool passes an isolated functional round trip, including
+a negative control and a read after writing. Recheck with one command:
+
+```bash
+tambo verify
+```
+
+Quipu's verification uses a temporary embedded database. Camayoc's check starts
+and stops its own temporary loopback server; you do not supply or manage one.
+This proves the installed tools, not a connection to a remote knowledge service.
+Optional Shantytown hook messages do not require a crew installation.
+
+### Advanced: connect to a shared, authenticated Quipu server
+
+Remote mode is opt-in: set `QUIPU_SERVER` to the server you intend to use, or
+create a plan with `--quipu-mcp-url`. Obtain an administrator-issued token and
+provision it before verification:
+
+```bash
+tambo provision-quipu-token --from /secure/issued-token --server https://your-quipu-server.example
+export QUIPU_SERVER=https://your-quipu-server.example
+tambo verify
+```
+
+Remote verification requires the canonical credential with `0400/0700`
+permissions, proves that unauthenticated access is refused and the selected
+credential is accepted, and refuses token rotation. Missing or rejected
+credentials stop verification. Never put a token in the plan or a server URL.
+Unset `QUIPU_SERVER` to use the local default; an explicit `quipu_mcp` plan
+remains remote until you remove that configuration deliberately.
+
+To customize the tools and intended-use contract instead, use
+`tambo init --guided` or `tambo plan --profile retrieval --intent caboodle-intent.toml`,
+review the resulting plan, then run `tambo install`. See the
+[walkthrough](https://scbrown.github.io/caboodle/getting-started.html).
+
+## On your own code
+
+| I want to⦠| Command |
+|---|---|
+| Choose tools and describe my own use | `caboodle init --guided` |
+| Find install blockers before changing anything | `caboodle doctor` |
+| Install and prove the reviewed selection | `caboodle install` |
+| Recheck the selected tools | `caboodle verify` |
+| Compare against this build's reviewed versions | `caboodle check-updates` |
+
+[Full command and configuration reference](https://scbrown.github.io/caboodle/reference.html).
+
+## Wire it into your agent
+
+Tambo is a CLI, with an [agent skill](https://github.com/scbrown/tambo/blob/main/skills/tambo/SKILL.md), not an MCP
+server. Bobbin, which the `retrieval` profile installed above, is one. It
+searches one repository: the code you want your agent to work on, such as your
+own project's git checkout. Do not use `~/caboodle`, which holds only the plan.
+Index that repository, then register bobbin from inside it:
+
+```bash
+cd ~/path/to/your/repo
+bobbin init
+bobbin index
+claude mcp add bobbin -- "$(command -v bobbin)" serve "$PWD"
+```
+
+`$(command -v bobbin)` and `$PWD` are expanded once, now, so Claude Code
+starts the server by absolute path and serves this repository whatever its own
+`PATH` or working directory. Registering a bare `bobbin` fails with `ENOENT` when
+Claude Code was not started from a shell that has `~/.cargo/bin` on `PATH`.
+
+`claude mcp add` uses the default `local` scope: the server is registered for
+this project only, so start Claude Code in this repository to see it (`claude
+mcp list` there shows `bobbin: ... β Connected`). Repeat the four lines in each
+repository you want searchable. `bobbin init` creates `.bobbin/`
+in the repository and, if the repository has a `.gitignore`, appends `.bobbin/`
+to it; review that change before you commit. Without `bobbin init`, `bobbin
+serve` exits with `Bobbin not initialized` and Claude Code reports the server as
+failed to connect.
+
+MCP (Model Context Protocol) lets your agent call the installed server.
+[Agent setup](https://scbrown.github.io/caboodle/agents.html) covers indexing, Yupana, other clients
+and managed crew registration through `caboodle project-settings`.
+
+## Before you start
+
+| Platform | Caboodle | Quipu | Yupana |
+|---|---|---|---|
+| Linux x86_64 | Release | Release | Release |
+| macOS arm64 / x86_64 | Release | Release | Release |
+| Linux arm64 | Build with Rust | Release | Build with Rust |
+
+Have `curl`, `tar`, `git`, `bash`, `python3` and `sha256sum` on PATH
+(`brew install coreutils` supplies `sha256sum` on macOS).
+The `everything` profile also needs Go. No account or remote server is needed.
+[Installation details](https://scbrown.github.io/caboodle/installing.html) cover source builds,
+installed files and removal; `caboodle doctor` names missing prerequisites.
+
+## What's next
+
+- [Read the book](https://scbrown.github.io/caboodle/introduction.html)
+- [Find a topic in the docs map](https://scbrown.github.io/caboodle/docs-map.html)
+- [Choose your profile](https://scbrown.github.io/caboodle/profiles.html)
+
diff --git a/docs/demos/install-bootstrap.md b/docs/demos/install-bootstrap.md
new file mode 100644
index 0000000..436c47a
--- /dev/null
+++ b/docs/demos/install-bootstrap.md
@@ -0,0 +1,21 @@
+# Verified bootstrap demonstration
+
+Previously the bootstrap checked the installed version after replacing files.
+It now checks the staged executable first, so a mismatched release preserves
+previous bytes. A standalone root entrypoint supports the short README command.
+
+[Recorded fixture run](install-bootstrap.txt) Β· [timing](install-bootstrap.timing).
+Ten controls pass, including valid current/compatibility archives, corrupt
+checksum, missing asset, unsupported platform and wrong-version refusal.
+These are synthetic local release fixtures, not cross-platform binary proof.
+
+Replay downloaded files with:
+
+```sh
+scriptreplay --log-out install-bootstrap.txt --log-timing install-bootstrap.timing
+```
+
+Repeat the controls with `python3 scripts/test-install-bootstrap.py`.
+Separately, the actual published Linux x86_64 v0.4.0 archive was checksum-verified,
+installed in a temporary prefix, and both commands reported version 0.4.0.
+No host installation or new release was performed.
diff --git a/docs/demos/install-bootstrap.timing b/docs/demos/install-bootstrap.timing
new file mode 100644
index 0000000..bb64703
--- /dev/null
+++ b/docs/demos/install-bootstrap.timing
@@ -0,0 +1,24 @@
+0.046478 119
+0.064428 4
+0.000023 149
+0.133871 4
+0.000006 117
+0.042938 4
+0.000007 105
+0.113277 4
+0.000017 105
+0.180362 4
+0.000020 101
+0.043462 4
+0.000005 79
+0.115002 4
+0.000016 131
+0.000556 4
+0.000016 131
+0.002756 4
+0.000007 117
+0.090240 4
+0.000035 2
+0.000010 72
+0.000009 26
+0.000028 4
diff --git a/docs/demos/install-bootstrap.txt b/docs/demos/install-bootstrap.txt
new file mode 100644
index 0000000..06940fd
--- /dev/null
+++ b/docs/demos/install-bootstrap.txt
@@ -0,0 +1,18 @@
+Script started on 2026-10-20 19:22:31-04:00 [COMMAND="python3 scripts/test-install-bootstrap.py" ]
+test_bad_checksum_never_falls_back_or_replaces (__main__.Bootstrap.test_bad_checksum_never_falls_back_or_replaces) ... ok
+test_default_uses_compatibility_repository_and_legacy_release (__main__.Bootstrap.test_default_uses_compatibility_repository_and_legacy_release) ... ok
+test_missing_assets_preserve_existing_install (__main__.Bootstrap.test_missing_assets_preserve_existing_install) ... ok
+test_new_archive_installs_both_commands (__main__.Bootstrap.test_new_archive_installs_both_commands) ... ok
+test_new_archive_requires_both_commands (__main__.Bootstrap.test_new_archive_requires_both_commands) ... ok
+test_non_404_failure_never_falls_back (__main__.Bootstrap.test_non_404_failure_never_falls_back) ... ok
+test_pinned_legacy_release (__main__.Bootstrap.test_pinned_legacy_release) ... ok
+test_root_installer_matches_compatibility_entrypoint (__main__.Bootstrap.test_root_installer_matches_compatibility_entrypoint) ... ok
+test_unsupported_platform_preserves_existing_install (__main__.Bootstrap.test_unsupported_platform_preserves_existing_install) ... ok
+test_wrong_version_preserves_existing_install (__main__.Bootstrap.test_wrong_version_preserves_existing_install) ... ok
+
+----------------------------------------------------------------------
+Ran 10 tests in 0.787s
+
+OK
+
+Script done on 2026-10-20 19:22:32-04:00 [COMMAND_EXIT_CODE="0"]
diff --git a/docs/stack-table.md b/docs/stack-table.md
index 373f99a..3a36d77 100644
--- a/docs/stack-table.md
+++ b/docs/stack-table.md
@@ -1,6 +1,6 @@
# Canonical stack table
-This is the source for the marked stack overview in the nine tool READMEs.
+This is the source for the marked stack overview in the twelve tool READMEs.
@@ -12,9 +12,11 @@ when.
| tool | what it gives your agents |
|---|---|
-| [caboodle](https://github.com/scbrown/caboodle) | π§° Agent Platform Toolkit - installs and configures the quipu stack |
+| [tambo](https://github.com/scbrown/tambo) | π§° Agent Platform Toolkit - installs and configures the quipu stack |
| [quipu](https://github.com/scbrown/quipu) | stores shared facts with provenance and history; can check writes against your rules |
| [camayoc](https://github.com/scbrown/camayoc) | prepares knowledge and vocabulary for Quipu, with checks before it is admitted |
+| [quechua](https://github.com/scbrown/quechua) | shared vocabulary and shapes for interoperable knowledge |
+| [chaski](https://github.com/scbrown/chaski) | reacts to graph changes and delivers governed notifications |
| [bobbin](https://github.com/scbrown/bobbin) | searches your code and can include related knowledge from Quipu |
| [yupana](https://github.com/scbrown/yupana) | shows code impact and can read policies from Quipu to check changes |
| [desire-path](https://github.com/scbrown/desire-path) | turns agent mistakes into feature requests; works on its own without Quipu |
@@ -27,5 +29,5 @@ when.
Run `python3 scripts/check-stack-table.py` to check this repository, or add
-`--remote` to compare all nine published READMEs. Update the marked blocks
+`--remote` to compare all twelve published READMEs. Update the marked blocks
together when changing this table.
diff --git a/docs/stack/README-TEMPLATE.md b/docs/stack/README-TEMPLATE.md
index 4495c2e..84d5ae6 100644
--- a/docs/stack/README-TEMPLATE.md
+++ b/docs/stack/README-TEMPLATE.md
@@ -126,9 +126,11 @@ when.
| tool | what it gives your agents |
|---|---|
-| [caboodle](https://github.com/scbrown/caboodle) | π§° Agent Platform Toolkit - installs and configures the quipu stack |
+| [tambo](https://github.com/scbrown/tambo) | π§° Agent Platform Toolkit - installs and configures the quipu stack |
| [quipu](https://github.com/scbrown/quipu) | stores shared facts with provenance and history; can check writes against your rules |
| [camayoc](https://github.com/scbrown/camayoc) | prepares knowledge and vocabulary for Quipu, with checks before it is admitted |
+| [quechua](https://github.com/scbrown/quechua) | shared vocabulary and shapes for interoperable knowledge |
+| [chaski](https://github.com/scbrown/chaski) | reacts to graph changes and delivers governed notifications |
| [bobbin](https://github.com/scbrown/bobbin) | searches your code and can include related knowledge from Quipu |
| [yupana](https://github.com/scbrown/yupana) | shows code impact and can read policies from Quipu to check changes |
| [desire-path](https://github.com/scbrown/desire-path) | turns agent mistakes into feature requests; works on its own without Quipu |
diff --git a/install.sh b/install.sh
new file mode 100755
index 0000000..9b46f7c
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,126 @@
+#!/bin/sh
+set -eu
+
+# This compatibility URL also redirects after the repository rename.
+repo=${TAMBO_REPOSITORY:-${CABOODLE_REPOSITORY:-scbrown/caboodle}}
+version=${TAMBO_VERSION:-${CABOODLE_VERSION:-}}
+install_dir=${TAMBO_INSTALL_DIR:-${CABOODLE_INSTALL_DIR:-${CARGO_HOME:-$HOME/.cargo}/bin}}
+
+if [ -z "$version" ]; then
+ version=$(curl -fsSL --connect-timeout 10 --max-time 120 "https://api.github.com/repos/$repo/releases/latest" |
+ sed -n 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)
+fi
+case "$version" in
+ v[0-9]*[!A-Za-z0-9._-]*) printf '%s\n' "invalid release tag" >&2; exit 1 ;;
+ v[0-9]*) ;;
+ *) printf '%s\n' "invalid or unavailable Tambo release version: $version" >&2; exit 1 ;;
+esac
+
+if [ -n "${TAMBO_TARGET:-${CABOODLE_TARGET:-}}" ]; then
+ target=${TAMBO_TARGET:-$CABOODLE_TARGET}
+else
+ machine=$(uname -m)
+ system=$(uname -s)
+ case "$system:$machine" in
+ Linux:x86_64) target=x86_64-unknown-linux-gnu ;;
+ Darwin:x86_64) target=x86_64-apple-darwin ;;
+ Darwin:arm64) target=aarch64-apple-darwin ;;
+ *) printf '%s\n' "unsupported Tambo release target: $system $machine" >&2; exit 1 ;;
+ esac
+fi
+
+case "$target" in
+ x86_64-unknown-linux-gnu|x86_64-apple-darwin|aarch64-apple-darwin) ;;
+ *) printf '%s\n' "unsupported Tambo release target: $target" >&2; exit 1 ;;
+esac
+
+archive="tambo-$version-$target.tar.gz"
+binaries='tambo caboodle'
+base=${TAMBO_RELEASE_BASE_URL:-${CABOODLE_RELEASE_BASE_URL:-https://github.com/$repo/releases/download/$version}}
+work=$(mktemp -d)
+trap 'rm -rf "$work"' EXIT HUP INT TERM
+
+if ! status=$(curl -sSL --connect-timeout 10 --max-time 120 \
+ -w '%{http_code}' "$base/$archive" -o "$work/$archive"); then
+ printf '%s\n' 'release archive transfer failed' >&2
+ exit 1
+fi
+case "$status" in
+ 200) ;;
+ 000)
+ # Local release fixtures use file://, which has no HTTP status.
+ case "$base" in file://*) ;; *) exit 1 ;; esac
+ ;;
+ 404)
+ archive="caboodle-$version-$target.tar.gz"
+ binaries=caboodle
+ curl -fsSL --connect-timeout 10 --max-time 120 "$base/$archive" -o "$work/$archive"
+ ;;
+ *) printf '%s\n' "release archive unavailable (HTTP $status)" >&2; exit 1 ;;
+esac
+curl -fsSL --connect-timeout 10 --max-time 120 "$base/$archive.sha256" -o "$work/$archive.sha256"
+python3 - "$work/$archive.sha256" "$archive" <<'PYCHECKSUM'
+import pathlib, re, sys
+fields = pathlib.Path(sys.argv[1]).read_text().split()
+if (len(fields) != 2 or not re.fullmatch(r"[0-9a-fA-F]{64}", fields[0])
+ or fields[1].lstrip("*") != sys.argv[2]):
+ raise SystemExit("invalid release checksum manifest")
+PYCHECKSUM
+expected=$(awk '{print $1}' "$work/$archive.sha256" | tr 'A-F' 'a-f')
+if command -v sha256sum >/dev/null; then
+ actual=$(sha256sum "$work/$archive" | awk '{print $1}')
+else
+ actual=$(shasum -a 256 "$work/$archive" | awk '{print $1}')
+fi
+test "$actual" = "$expected" || {
+ printf '%s\n' "$archive: checksum mismatch" >&2
+ exit 1
+}
+# Only the declared regular executables may leave the verified archive.
+python3 - "$work" "$archive" $binaries <<'PYARCHIVE'
+import pathlib, sys, tarfile
+root, name = pathlib.Path(sys.argv[1]), sys.argv[2]
+expected = set(sys.argv[3:])
+with tarfile.open(root / name, "r:gz") as bundle:
+ members = bundle.getmembers()
+ if len(members) != len(expected) or {m.name for m in members} != expected:
+ raise SystemExit("unexpected release archive members")
+ for member in members:
+ if not member.isfile() or member.size > 256 * 1024 * 1024:
+ raise SystemExit("invalid release executable")
+ source = bundle.extractfile(member)
+ if source is None:
+ raise SystemExit("missing release executable")
+ (root / member.name).write_bytes(source.read())
+ (root / member.name).chmod(0o755)
+PYARCHIVE
+for binary in $binaries; do
+ test -f "$work/$binary" && test ! -L "$work/$binary" && test -x "$work/$binary"
+ reported=$("$work/$binary" --version)
+ case "$reported" in
+ "$binary ${version#v}"|"$binary ${version#v} "*) ;;
+ *) printf '%s\n' "release version mismatch: $binary" >&2; exit 1 ;;
+ esac
+done
+mkdir -p "$install_dir"
+for binary in $binaries; do
+ staged="$install_dir/.$binary.$$.tmp"
+ cp "$work/$binary" "$staged"
+ chmod 0755 "$staged"
+ mv "$staged" "$install_dir/$binary"
+done
+for binary in $binaries; do
+ "$install_dir/$binary" --version
+ printf '%s\n' "installed $install_dir/$binary"
+done
+
+# Name a stale executable for either command; version equality cannot prove
+# that PATH resolves to the binary just installed.
+for binary in $binaries; do
+ resolved=$(command -v "$binary" 2>/dev/null || true)
+ if [ -n "$resolved" ] && ! [ "$resolved" -ef "$install_dir/$binary" ]; then
+ printf '%s\n' "WARNING: \`$binary\` on PATH is $resolved ($("$resolved" --version 2>&1 | head -n 1)), not the copy just installed at $install_dir/$binary. Remove the stale copy or put $install_dir earlier on PATH." >&2
+ elif [ -z "$resolved" ]; then
+ printf '%s\n' "note: $install_dir is not on PATH; add it to run \`$binary\`" >&2
+ fi
+done
diff --git a/scripts/check-stack-table.py b/scripts/check-stack-table.py
index 4559e23..2260af0 100644
--- a/scripts/check-stack-table.py
+++ b/scripts/check-stack-table.py
@@ -10,7 +10,7 @@
from urllib.request import Request, urlopen
ROOT = Path(__file__).resolve().parents[1]
-REPOS = ("quipu", "bobbin", "camayoc", "yupana", "desire-path", "seeds", "caboodle", "shantytown", "rikuy")
+REPOS = ("tambo", "quipu", "camayoc", "quechua", "chaski", "bobbin", "yupana", "seeds", "shantytown", "shuttle", "rikuy", "desire-path")
START = b""
END = b""
MAX_BYTES = 256 * 1024
@@ -56,7 +56,7 @@ def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("readmes", nargs="*", type=Path, help="local README copies")
parser.add_argument("--canonical", type=Path, default=ROOT / "docs/stack-table.md")
- parser.add_argument("--remote", action="store_true", help="also audit all nine public main branches")
+ parser.add_argument("--remote", action="store_true", help="also audit all twelve public main branches")
args = parser.parse_args()
try:
canonical = marked_table(args.canonical.read_bytes(), str(args.canonical))
diff --git a/scripts/install.sh b/scripts/install.sh
index 93e64b6..9b46f7c 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -11,6 +11,7 @@ if [ -z "$version" ]; then
sed -n 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)
fi
case "$version" in
+ v[0-9]*[!A-Za-z0-9._-]*) printf '%s\n' "invalid release tag" >&2; exit 1 ;;
v[0-9]*) ;;
*) printf '%s\n' "invalid or unavailable Tambo release version: $version" >&2; exit 1 ;;
esac
@@ -28,6 +29,11 @@ else
esac
fi
+case "$target" in
+ x86_64-unknown-linux-gnu|x86_64-apple-darwin|aarch64-apple-darwin) ;;
+ *) printf '%s\n' "unsupported Tambo release target: $target" >&2; exit 1 ;;
+esac
+
archive="tambo-$version-$target.tar.gz"
binaries='tambo caboodle'
base=${TAMBO_RELEASE_BASE_URL:-${CABOODLE_RELEASE_BASE_URL:-https://github.com/$repo/releases/download/$version}}
@@ -53,19 +59,48 @@ case "$status" in
*) printf '%s\n' "release archive unavailable (HTTP $status)" >&2; exit 1 ;;
esac
curl -fsSL --connect-timeout 10 --max-time 120 "$base/$archive.sha256" -o "$work/$archive.sha256"
+python3 - "$work/$archive.sha256" "$archive" <<'PYCHECKSUM'
+import pathlib, re, sys
+fields = pathlib.Path(sys.argv[1]).read_text().split()
+if (len(fields) != 2 or not re.fullmatch(r"[0-9a-fA-F]{64}", fields[0])
+ or fields[1].lstrip("*") != sys.argv[2]):
+ raise SystemExit("invalid release checksum manifest")
+PYCHECKSUM
+expected=$(awk '{print $1}' "$work/$archive.sha256" | tr 'A-F' 'a-f')
if command -v sha256sum >/dev/null; then
- (cd "$work" && sha256sum -c "$archive.sha256")
+ actual=$(sha256sum "$work/$archive" | awk '{print $1}')
else
- expected=$(awk '{print $1}' "$work/$archive.sha256")
actual=$(shasum -a 256 "$work/$archive" | awk '{print $1}')
- test "$actual" = "$expected" || {
- printf '%s\n' "$archive: checksum mismatch" >&2
- exit 1
- }
fi
-tar -xzf "$work/$archive" -C "$work"
+test "$actual" = "$expected" || {
+ printf '%s\n' "$archive: checksum mismatch" >&2
+ exit 1
+}
+# Only the declared regular executables may leave the verified archive.
+python3 - "$work" "$archive" $binaries <<'PYARCHIVE'
+import pathlib, sys, tarfile
+root, name = pathlib.Path(sys.argv[1]), sys.argv[2]
+expected = set(sys.argv[3:])
+with tarfile.open(root / name, "r:gz") as bundle:
+ members = bundle.getmembers()
+ if len(members) != len(expected) or {m.name for m in members} != expected:
+ raise SystemExit("unexpected release archive members")
+ for member in members:
+ if not member.isfile() or member.size > 256 * 1024 * 1024:
+ raise SystemExit("invalid release executable")
+ source = bundle.extractfile(member)
+ if source is None:
+ raise SystemExit("missing release executable")
+ (root / member.name).write_bytes(source.read())
+ (root / member.name).chmod(0o755)
+PYARCHIVE
for binary in $binaries; do
- test -x "$work/$binary"
+ test -f "$work/$binary" && test ! -L "$work/$binary" && test -x "$work/$binary"
+ reported=$("$work/$binary" --version)
+ case "$reported" in
+ "$binary ${version#v}"|"$binary ${version#v} "*) ;;
+ *) printf '%s\n' "release version mismatch: $binary" >&2; exit 1 ;;
+ esac
done
mkdir -p "$install_dir"
for binary in $binaries; do
diff --git a/scripts/test-install-bootstrap.py b/scripts/test-install-bootstrap.py
index 0176e06..467dfc8 100644
--- a/scripts/test-install-bootstrap.py
+++ b/scripts/test-install-bootstrap.py
@@ -128,6 +128,37 @@ def test_bad_checksum_never_falls_back_or_replaces(self):
self.assertEqual(existing.read_bytes(), b'prior installed bytes')
self.assertNotIn('/caboodle-', self.requests.read_text())
+ def test_root_installer_matches_compatibility_entrypoint(self):
+ self.assertEqual((ROOT / 'install.sh').read_bytes(),
+ (ROOT / 'scripts/install.sh').read_bytes())
+
+ def test_unsupported_platform_preserves_existing_install(self):
+ self.target = 'aarch64-unknown-linux-gnu'
+ existing = self.root / 'installed/tambo'
+ existing.write_bytes(b'prior installed bytes')
+ self.assertNotEqual(self.run_installer('v0.4.0').returncode, 0)
+ self.assertIn('unsupported', self.result.stderr)
+ self.assertEqual(existing.read_bytes(), b'prior installed bytes')
+
+ def test_missing_assets_preserve_existing_install(self):
+ existing = self.root / 'installed/tambo'
+ existing.write_bytes(b'prior installed bytes')
+ self.assertNotEqual(self.run_installer('v0.4.0').returncode, 0)
+ self.assertEqual(existing.read_bytes(), b'prior installed bytes')
+
+ def test_wrong_version_preserves_existing_install(self):
+ self.archive('tambo', 'v0.3.0', ['tambo', 'caboodle'])
+ for url, route in list(self.routes.items()):
+ self.routes[url.replace('v0.3.0', 'v0.4.0')] = route
+ # Rewrite only the checksum filename: verified bytes still report 0.3.0.
+ url = f'{self.base}/tambo-v0.4.0-{self.target}.tar.gz.sha256'
+ self.routes[url]['body'] = self.routes[url]['body'].replace('v0.3.0', 'v0.4.0')
+ existing = self.root / 'installed/tambo'
+ existing.write_bytes(b'prior installed bytes')
+ self.assertNotEqual(self.run_installer('v0.4.0').returncode, 0)
+ self.assertIn('version mismatch', self.result.stderr)
+ self.assertEqual(existing.read_bytes(), b'prior installed bytes')
+
def test_non_404_failure_never_falls_back(self):
self.archive('caboodle', 'v0.3.0', ['caboodle'])
for response in ({'status': 500}, {'status': 0, 'exit': 7}):
diff --git a/scripts/test-readme.sh b/scripts/test-readme.sh
index ce57481..28e26bc 100755
--- a/scripts/test-readme.sh
+++ b/scripts/test-readme.sh
@@ -25,6 +25,8 @@ text = pathlib.Path(sys.argv[1]).read_text()
install = text.split("## Install", 1)[1].split("## First success", 1)[0]
local = text.split("## First success", 1)[1].split("### Advanced:", 1)[0]
bootstrap = re.findall(r"```bash\n(.*?)```", install, re.S)[0]
+# Exercise this proposal's installer rather than fetching unrelated main bytes.
+bootstrap = bootstrap.replace('https://raw.githubusercontent.com/scbrown/tambo/main/install.sh', pathlib.Path(sys.argv[1]).parent.joinpath('install.sh').as_uri())
commands = re.findall(r"```bash\n(.*?)```", local, re.S)
assert commands == ["tambo quickstart\n", "tambo verify\n"], commands
pathlib.Path(sys.argv[2]).write_text(bootstrap + '\nexport PATH="${CARGO_HOME:-$HOME/.cargo}/bin:$PATH"\n' + "\n".join(commands))
diff --git a/scripts/test-stack-table.py b/scripts/test-stack-table.py
index 98c91fa..9893a4f 100644
--- a/scripts/test-stack-table.py
+++ b/scripts/test-stack-table.py
@@ -82,14 +82,14 @@ def read(repo):
self.assertEqual(CHECK.main(), expected)
self.assertEqual(seen.count("shantytown"), 1)
self.assertEqual(seen.count("rikuy"), 1)
- self.assertEqual(len(seen), 9)
+ self.assertEqual(len(seen), 12)
def test_remote_response_has_a_size_limit(self):
response = MagicMock()
response.__enter__.return_value.read.return_value = b"x" * (CHECK.MAX_BYTES + 1)
with patch.object(CHECK, "urlopen", return_value=response):
with self.assertRaisesRegex(ValueError, "size limit"):
- CHECK.remote_readme("caboodle")
+ CHECK.remote_readme("tambo")
if __name__ == "__main__":