Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .githooks/allowed-secrets
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
# prove the venue listing is asked for once
0x0000000000000000000000000000000000000123
0x0000000000000000000000000000000000000def
# not addresses; two same-symbol tokens told apart by address, and a second vault
0x1111111100000000000000000000000000000001
0x2222222200000000000000000000000000000002
0x00000000000000000000000000000000000000ff

# Binance's own published HMAC-SHA256 worked example, used to prove the signing
# code matches the vendor's documented output. Public in their API docs.
Expand Down
16 changes: 14 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ jobs:
# The install path ships to users the same way the binary does, and it is
# the one piece nothing else exercises until a tag is pushed.
- run: bash scripts/install-test.sh
- name: every release target still cross-compiles
run: bash scripts/release-build.sh dist/release

# The release job runs `bun run check` on macOS, where the binary is signed, so
# the same command runs here: a failure only macOS shows has to fail the pull
Expand All @@ -54,6 +52,20 @@ jobs:
node-version: 22
- run: bun install --frozen-lockfile
- run: bun run check
# Here, not on Linux: the darwin targets are re-signed with codesign, and
# release-build.sh refuses without it.
- name: every release target still cross-compiles
run: bash scripts/release-build.sh dist/release
# Launched, not only signature-checked; the Intel build under Rosetta.
- name: both macOS builds start
run: |
set -euo pipefail
/usr/bin/pgrep -q oahd || sudo softwareupdate --install-rosetta --agree-to-license
for a in arm64:arm64 x64:x86_64; do
work=$(mktemp -d)
tar -xzf dist/release/tula-v*-darwin-"${a%%:*}".tar.gz -C "$work"
arch "-${a##*:}" "$work/tula" --version
done

# The site is the published origin of install.sh and of every claim a reader
# sees before installing anything. Vercel builds it too, but a preview that
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/injection-eval.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,21 @@ permissions:
jobs:
eval:
runs-on: ubuntu-latest
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.2.16
- run: bun install --frozen-lockfile

# A fork has no secret and never will. Saying so is the point: a run that
# failed here would read as a model that followed the payload.
# On this step alone, so `bun install`'s lifecycle scripts never see it.
- name: run the eval, or say why it did not
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
set -euo pipefail
if [ -z "${ANTHROPIC_API_KEY:-}" ]; then
Expand Down
19 changes: 14 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ jobs:
echo "configured=true" >>"$GITHUB_OUTPUT"
else
echo "configured=false" >>"$GITHUB_OUTPUT"
echo "::warning::Apple signing is not configured; macOS binaries ship unsigned."
echo "::warning::Apple signing is not configured; macOS binaries ship ad-hoc signed."
fi

- name: sign and notarize macOS binaries
Expand Down Expand Up @@ -218,14 +218,23 @@ jobs:
# verifies, and a binary that starts. A signed Mach-O that Gatekeeper or a
# bad re-tar has broken passes every check above and fails on first run.
- name: the shipped archives verify and run
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
cd dist/release
shasum -a 256 -c checksums.txt
work=$(mktemp -d)
tar -xzf "tula-v${{ steps.version.outputs.version }}-darwin-arm64.tar.gz" -C "$work"
"$work/tula" --version
rm -rf "$work"
# The runner's macOS may still run a binary with a broken signature
# that a newer one kills, so launching it alone proves nothing. The
# Intel build runs under Rosetta, the path it takes on Apple silicon.
/usr/bin/pgrep -q oahd || sudo softwareupdate --install-rosetta --agree-to-license
for a in arm64:arm64 x64:x86_64; do
work=$(mktemp -d)
tar -xzf "tula-v$VERSION-darwin-${a%%:*}.tar.gz" -C "$work"
codesign --verify --strict --verbose=2 "$work/tula"
arch "-${a##*:}" "$work/tula" --version
rm -rf "$work"
done

# Signed by GitHub's workflow identity through sigstore: keyless, so there
# is no signing key for this project to generate, publish, rotate or lose.
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@ dist/
# Belt and braces: credentials live in ~/.config/tula, never the repo.
# This catches a stray copy before it reaches a commit.
*.key
*.pem
.npmrc
credentials.json
.env
.env.*
# What TULA_CONFIG_DIR holds, should a scratch run point it inside the tree.
history.jsonl
state.json
preferences.json

# The site is a separate package: its dependencies never enter the binary.
site/node_modules
Expand Down
53 changes: 25 additions & 28 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,12 @@ into a directory anyone can replace it in. None of those failed a type check.

## Versioning

[SemVer](https://semver.org), and the version describes a **release**, never a
plan — the milestones live in `ROADMAP.md` precisely so a reordered plan cannot
make a published number wrong. Pre-1.0: **patch** for fixes, security hardening
and doc or site corrections; **minor** for a new venue, command or capability,
and for anything breaking; **major** is reserved for `1.0.0` and `2.0.0`.

A hyphen means pre-release. It is the only signal — `src/version.ts` derives
`IS_PRE_RELEASE` from `APP_VERSION`, and `release.yml` reads the same hyphen to
choose `--prerelease` over `--latest` and the npm dist-tag. They were once a
hand-set boolean apart and had already drifted into a stable release whose
binary called itself a pre-release; `guard.sh` now fails if the derivation is
replaced by a literal.
The version describes a **release**, never a plan; the bump rules are in
`ROADMAP.md`'s Versions section. A hyphen means pre-release and is the only
signal: `src/version.ts` derives `IS_PRE_RELEASE` from `APP_VERSION`, and
`release.yml` reads the same hyphen for `--prerelease` and the npm dist-tag.
`guard.sh` fails if the derivation is replaced by a literal — a hand-set boolean
once shipped a stable release whose binary called itself a pre-release.

## Stack

Expand Down Expand Up @@ -198,7 +192,7 @@ src/
version.ts # APP_NAME, APP_VERSION, IS_PRE_RELEASE, REPO_URL, SITE_URL,
# APP_DESCRIPTION — single source; guard.sh reads four of them
core/
position.ts # canonical schema: Position, NetExposure, LiquidationParams
position.ts # canonical schema: Position, NetExposure, LiquidationParams; rowIdentity
untrusted.ts # visible() — the one filter over text somebody else wrote
exposure.ts # netExposure, portfolioValue — equity, never a perp's notional; oldest
risk.ts # liquidation distance, scenario shocks, what breaks first
Expand Down Expand Up @@ -379,9 +373,9 @@ Two rules, and they are the reason the architecture exists:
called in *in order*, so a venue in the book with an unread area that hides a
liquidation makes the order wrong rather than short, and they say so.
`ALTERED`, the fifth thing a view says about itself, is the only one about
text rather than holdings: a venue spelled an asset in characters this build
could not print, so the block names the venue, what was
done and the *bounded* name — never what the venue sent, which is the string
text rather than holdings: a venue spelled an asset, product or held-as name
in characters this build could not print, so the block names the venue, what
was done and the *bounded* name — never what the venue sent, which is the string
the bound exists to keep off a terminal. `LoadResult.altered` carries it, and
the first line says nothing is missing, or a reader has five states to tell
apart and four of them mean go and fetch something.
Expand All @@ -390,9 +384,8 @@ Two rules, and they are the reason the architecture exists:
goes — and `src/coverage-plan.test.ts` fails the build on a path that is not a
real task, on one already `done`, on a liquidation-hiding gap filed under the
aggregator, and on any plan `ROADMAP.md`'s table does not name. Declaring a
gap costs one object and used to create no obligation at all, which is how
thirty accumulated with thirteen in no plan and two named in no file in the
repository. The declaration and the plan are one edit now.
gap costs one object, so without this gaps accumulate with no plan behind
them. The declaration and the plan are one edit.
- **`src/index.ts` reaches the terminal UI only through a dynamic import.** A
one-shot command draws its tables through `src/ui/table.ts` and never needs a
reconciler; loading Ink and React for one cost 82ms against 56ms on
Expand Down Expand Up @@ -618,7 +611,8 @@ venue in it.
the error text of a venue or a price source, the error text of the model
provider, and a Hyperliquid builder dex name, which is a venue label and so
is refused unless it matches `DEX_NAME` rather than capped. The symbol is capped in
`src/cli/session.ts`, where every connector arrives; the error text is capped
`src/cli/session.ts`, where every connector arrives, and so are the venue's
`heldAs` spelling and `product` name beside it; the error text is capped
by `remote()` in `src/core/errors.ts` where it enters, at the connector or
the price source that received it, which is the only place that can tell
tula's own words from somebody else's — and by `explain()` in
Expand Down Expand Up @@ -657,6 +651,11 @@ endpoint — including "validate only" variants. The absence is the product.
7. Give it a colour in `src/ui/brand.ts`. A venue without one renders a hole
beside the rest, and `src/ui/brand.test.ts` fails on it.
8. Do not sort — the command layer does that.
9. No two rows may read alike but for their figures. Where one label holds an
asset twice, say what the venue calls the difference: `product` for the
contract, margin book, vault or staking state, `heldAs` for the venue's own
spelling of an asset counted as another. Its test asserts `rowIdentity` is
unique over what it returns, as `kraken.test.ts` does.

## Working from tasks/

Expand Down Expand Up @@ -1014,10 +1013,9 @@ before pasting keys tied to their net worth.
- **Never document an install path that does not work yet.** A published command
that fetches nothing is an impersonation surface, not a convenience.

### Before the first release
### What a release needs outside the repository

None of this lives in the repository, and each missing piece fails a different
channel at a different moment. The tap and the npm scope fail *after* the GitHub
Each missing piece fails a different channel at a different moment. The tap and the npm scope fail *after* the GitHub
release is already public, while the site is telling people to use them.

| What | Why it blocks | Check |
Expand All @@ -1029,16 +1027,15 @@ release is already public, while the site is telling people to use them.
| `usetu.la` resolves, HTTPS enforced | the install command is the domain | `curl -sI https://usetu.la/install.sh` |
| The Vercel root directory is `site` | `vercel.json`'s headers are read from there and nowhere else | `curl -sI https://usetu.la/` |
| Vercel includes files outside that root | the build script copies `install.sh` in from there; without it the build fails | `curl -sI https://usetu.la/install.sh` |
| `APPLE_*` secrets | optional; without them macOS ships unsigned | `gh secret list` |
| `APPLE_*` secrets | optional; without them macOS ships ad-hoc signed | `gh secret list` |
| A `release` environment with a required reviewer | `release.yml` names it on all three jobs, and naming it does nothing until it exists — GitHub silently creates an unprotected one on first use, and the run publishes unreviewed | `curl -s -o /dev/null -w '%{http_code}\n' https://api.github.com/repos/hsnice16/tula/environments/release` |

Set each variable last, after its token exists: `true` without the token turns a
skipped job into a failed one, and it fails after the GitHub release is public.

The site is already deployed and already names all three channels, so the table
above is not a checklist for later — every row that is not true when the tag is
pushed is a published command that fetches nothing for as long as it takes to
notice. Each can be checked without `gh` and without being signed in, which is
The site names all three channels, so every row above that is not true when a
tag is pushed is a published command that fetches nothing for as long as it
takes to notice. Each can be checked without `gh` and without being signed in, which is
also how a reader would find out before you do:

```bash
Expand Down
22 changes: 21 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,25 @@ CI and build plumbing, refactors, and doc-only edits — stays in commit message

## [Unreleased]

## [0.3.2] - 2026-09-22

### Fixed

- **tula starts on macOS 27.** The macOS binaries shipped with a code signature that did not verify, which earlier releases of macOS let run and macOS 27 kills on launch — `zsh: killed tula`. They are signed on the build machine now, and a release whose binaries do not verify is not published. A tula that is killed cannot update itself: re-run the install line, `brew upgrade`, or `npm install -g @hsnice16/tula`.
- **No two rows read alike.** Wherever tula lists holdings — the tables, the lines under `/shock` and `/breaks`, a venue's `status`, and what the model is handed — two rows now differ in something other than a number.
- An asset counted as another says what the venue calls it: `ETH (as WETH)`, `USDT0 (as USDT)`, `PEPE (as kPEPE)`.
- A `PRODUCT` column names the contract, margin book, vault or staking state a row is held in: Binance's cross and isolated margin; each Coinbase perp; Kraken's margin pairs; Hyperliquid's vaults, and staked HYPE under the names its staking panel gives it, Total Staked and Available to Stake.
- A venue's own views gain a `VENUE` column wherever its rows come from sub-accounts, builder dexes, markets or chains: `/<venue> positions`, `/<venue> breaks`, and the borrowing table in `/<venue> status`.
- Two Kraken wallets of one type are numbered, `kraken-spot-1` and `kraken-spot-2`. Several margin positions on one pair and side are one row, summed as Kraken's own `consolidation=market` sums them.
- A token calling itself the chain's gas token, and two Aave reserves one market would list under one name, carry their contract.
- **Two addresses on one Aave market are two health factors under `/shock`.** They were pooled into one market: the second address's factor was never printed, and the one shown moved on collateral from both.
- **An update that does not start is never installed.** `install.sh` and `/update install` run the new build once before pointing `tula` at it; one that fails leaves you on the version you had.
- **tula runs on every Mac from macOS 13.** The Intel build no longer needs AVX2, so it also runs under Rosetta on macOS 13 and 14. The installer fetches the native build on Apple silicon even from a shell running under Rosetta. The installer, Homebrew and npm say so on an older macOS.
- **An arrow key pressed while typing a key at `tula connect` is not saved into it.** Its escape sequence was stored as part of the secret, and the venue then refused a key that looked right.
- **A Kraken wallet tula did not read is named.** When Kraken's wallet list fails to load, only the default wallet is read, and the book now says so instead of reading as whole.
- **A Hyperliquid spot token called `WETH` is not ether.** It netted with ETH and took ether's price; any deployer can list a token under that name.
- **`tula connect --help` prints connect's usage.** It answered `Unknown venue "--help"`.

## [0.3.1] - 2026-09-16

### Fixed
Expand Down Expand Up @@ -353,7 +372,8 @@ what breaks first.
- `KeyScope` is tri-state. Kraken exposes no endpoint reporting a key's permissions, and every endpoint gated on trade permission mutates an order, so `canTrade` is `unknown` rather than guessed at. Withdraw scope is provable, and is proven.
- Kraken margin and open orders are not read yet, so on a margin account this is not a complete Kraken picture.

[Unreleased]: https://github.com/hsnice16/tula/compare/v0.3.1...HEAD
[Unreleased]: https://github.com/hsnice16/tula/compare/v0.3.2...HEAD
[0.3.2]: https://github.com/hsnice16/tula/compare/v0.3.1...v0.3.2
[0.3.1]: https://github.com/hsnice16/tula/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/hsnice16/tula/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/hsnice16/tula/compare/v0.1.3...v0.2.0
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ goes on telling people to use that channel, so the run prints a warning naming
each channel that is off. Read it before you announce anything.

```bash
bash scripts/release-build.sh dist/release # the same artifacts, locally
bash scripts/release-build.sh dist/release # the same artifacts; macOS only, it needs codesign
bash scripts/install-test.sh # runs install.sh against a fake release
bash scripts/npm-pack.sh dist/release # the npm tree that would be published
bash scripts/homebrew-formula.sh dist/release tula # the formula, real checksums
Expand All @@ -134,7 +134,7 @@ Two ways, neither of which publishes anything by accident.
`publish` off. It builds all four targets, verifies them and runs the installer
against them — then stops, and leaves the artifacts and `checksums.txt` on the
run to inspect. It signs only where `secrets.APPLE_CERT_P12` is set; without it
the macOS binaries are unsigned and the run says so. Publishing is off by default
the macOS binaries ship ad-hoc signed and the run says so. Publishing is off by default
because `GITHUB_REF_TYPE` is `branch` on a manual run, so the tag-matches-version
check cannot protect it; without the gate, a manual run would cut a real release
from whatever was on the branch. **It does not attest.** That step is gated with
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,14 @@ curl --proto '=https' --tlsv1.2 -LsSf https://usetu.la/install.sh | sh
brew install hsnice16/tap/tula # or: npm install -g @hsnice16/tula
```

macOS and Linux, on 64-bit Intel and ARM. Alpine and other musl systems are not
macOS 13 or later and Linux, on 64-bit Intel and ARM. Alpine and other musl systems are not
supported, and there is no native Windows build — install inside WSL. The
installer always checks the download against its published checksum, and checks
the sigstore-backed attestation proving this repository's release workflow built
it wherever the GitHub CLI can — saying so either way. Check one by hand:

```bash
gh attestation verify tula-v0.3.1-darwin-arm64.tar.gz --repo hsnice16/tula \
gh attestation verify tula-v0.3.2-darwin-arm64.tar.gz --repo hsnice16/tula \
--signer-workflow hsnice16/tula/.github/workflows/release.yml
```

Expand Down
Loading
Loading