Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
a22443f
docs(ai): add the local-stack workflow for multi-PR specs
artkorotkikh-dfinity Aug 24, 2026
0719304
docs(ai): record the spike findings the local rig already settled
artkorotkikh-dfinity Aug 24, 2026
a1ddee4
docs(ai): record the spike results proven against the local ledger
artkorotkikh-dfinity Aug 24, 2026
ae4d24f
docs(ai): close the tips spike against the mainnet ledgers
artkorotkikh-dfinity Aug 25, 2026
aa7b4dc
docs(ai): record the built stack and what it still owes
artkorotkikh-dfinity Aug 25, 2026
dc409e1
docs(ai): record the deploy mechanics and where be1 actually stands
artkorotkikh-dfinity Aug 27, 2026
4f99f53
docs(ai): track what still stands between the tips stack and its PRs
artkorotkikh-dfinity Aug 29, 2026
a402988
docs(ai): close out the five open decisions and the two PR blockers
artkorotkikh-dfinity Aug 29, 2026
00d7065
docs(ai): record why the mid-flight upgrade case cannot be tested here
artkorotkikh-dfinity Aug 30, 2026
e2901ac
docs(ai): re-measure the stack after the pre-PR fixes
artkorotkikh-dfinity Aug 30, 2026
d397e4c
docs(ai): design signed off in both themes
artkorotkikh-dfinity Aug 30, 2026
e66f53e
docs(ai): downgrade the compliance item and clear the people blockers
artkorotkikh-dfinity Aug 30, 2026
ef9a5a8
docs(ai): correct the pocket-ic claim, and close open question 4
artkorotkikh-dfinity Aug 30, 2026
7139655
chore(ai): drop a build artifact that was swept into the docs branch
artkorotkikh-dfinity Aug 30, 2026
eb7efb1
docs(ai): record the eighth PR and what review changed
artkorotkikh-dfinity Aug 31, 2026
4164efc
Merge origin/main into docs/ai/stacked-builds
artkorotkikh-dfinity Sep 1, 2026
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
5 changes: 5 additions & 0 deletions docs/ai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ docs/ai/
├── README.md ← you are here
├── governance.md Truth hierarchy, boundaries, capabilities, meta-update rule
├── pr-and-ci.md PR title regex, body template, CI cheatsheet, local gates
├── PRODUCT.md Living description of all current product behaviours
├── spec-driven-development/ Spec-first workflow (Cowork → Claude Code)
│ ├── workflow.md The workflow itself, step by step
│ ├── stacked-builds.md Building a multi-PR spec as a local stack
│ └── specs/ One spec per feature / improvement / bugfix
├── integrations/ Third-party APIs/services: what data we fetch & where
│ ├── README.md Provider index (Alchemy, Infura, Etherscan, OnRamper, …)
│ └── <provider>.md One reference doc per provider
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Build plan — tips via link or QR code

Working state for [`2026-08-05-feat-tips-via-link.md`](../2026-08-05-feat-tips-via-link.md),
built as a local stack per [`stacked-builds.md`](../../stacked-builds.md).

Disposable by design: this file leaves with the rest of the folder at
[Step 7 — Post-merge cleanup](../../workflow.md#step-7--post-merge-cleanup-claude-code).
The spec stays the source of truth for **what** to build; this only records
**where** each piece lives.

## The stack

| # | Branch | Spec PR | Contains | Status |
| --- | ------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| 1 | `feat/tips-1-backend` | PR-1 | tip store, `create_tip` / `get_tip` / `claim_tip` / `cancel_tip` / `get_my_tips`, claim-code hashing, atomic claim, expiry, rate limiter, pruning, candid + `npm run generate` | not started |
| 2 | `feat/tips-2-service` | PR-2 | `tip.api.ts` + `tip.services.ts`: approve → record with idempotent retry, link + QR construction, claim, cancel | not started |
| 3 | `feat/tips-3-sender-ui` | PR-3 | `Issue Tip` menu entry, intro modal, token picker + empty state, Issue Tip step, share screen, `tip.*` i18n | not started |
| 4 | `feat/tips-4-recipient-ui` | PR-4 | `/tip/<id>` landing, Tip Status modal, II hand-off across the fragment, claim review, success, **unavailable** | not started |
| 5 | `feat/tips-5-history` | PR-5 | History with all five statuses, claimer principal on claimed rows, cancel action | not started |
| 6 | `feat/tips-6-reserved-balance` | PR-2b | subtract reserved amounts once, in the derived store the token list, send, swap and both MAX controls read | not started |

**Why reserved-balance sits at the top, not at position 3 as the spec lists it.**
It touches the most load-bearing derived state in the app, so it is the slowest
branch to review — and nothing else in the stack depends on it. At the top it
blocks nothing; at position 3 it would hold up four UI PRs behind its review.

Land bottom-up. Each branch is cut from its parent, and a fix goes to the branch
that owns the code, then cascades down.

## PR-0 — the spike

Throwaway, no branch, no PR. Everything above waits on it. It runs entirely
against a local replica: the local ck-ledgers are the real
`ic-icrc1-ledger.wasm` from `dfinity/ic` at the commit pinned in
[`scripts/download.ckbtc.sh`](../../../../../scripts/download.ckbtc.sh), so the
ICRC-2 mechanics proved locally are the mainnet ones.

What it must answer:

- [ ] **Open question 1** — `approve` to `{owner: backend, subaccount: H(tip_id)}`
followed by `icrc2_transfer_from` with the matching `spender_subaccount`,
against a live ledger.
- [ ] **Open question 3** — `expected_allowance` on replace, so a retried
creation cannot destroy a live reservation.
- [ ] **Open question 2** — which ledgers advertise ICRC-2 via
`icrc1_supported_standards`. The answer **is** the v1 token list, and it
settles the ckETH pending decision. This is the one part the local rig
cannot answer: local proves the wasm supports it, not what mainnet has
deployed. Query the mainnet ledgers read-only.

Already established from the repo, so nobody re-derives it:

- `spender_subaccount` exists on `TransferFromArgs` in
[`src/cycles_ledger/types/src/lib.rs`](../../../../../src/cycles_ledger/types/src/lib.rs)
(`Option<serde_bytes::ByteBuf>`) — generated from a ledger candid, in a
canister this backend already calls ICRC-2 against.
- The frontend SDK exposes it too: `TransferFromParams.spender_subaccount` in
`@icp-sdk/canisters/ledger/icrc`, the package
[`icrc-ledger.api.ts`](../../../../../src/frontend/src/icp/api/icrc-ledger.api.ts)
already wraps.

So the spec's premise for open question 1 — that the field is absent from every
vendored candid in this repo — is wrong. What remains is behavioural: does a
real ledger honour it.

## The local rig

Everything needed is already in the repo. `npm run deploy` brings up the backend,
a local `internet_identity`, and the real `icp_ledger`, `ckbtc_ledger`,
`cketh_ledger` and `ckusdc_ledger` — which is the entire v1 token set.

| Step | Command |
| ----------------------------------------------------------- | ------------------------------------------- |
| Toolchain (pinned in [`dfx.json`](../../../../../dfx.json)) | `dfxvm install 0.26.1` |
| Replica | `dfx start --clean --background` |
| Canisters | `npm run deploy` |
| Fund the sender | `scripts/send.tokens.sh <sender-principal>` |
| App | `npm run dev` |

Use **two local Internet Identity anchors in two browser profiles**: one sender,
one recipient that has never existed before. That is not a convenience — it is
the only way to test
[open questions 5 and 6](../2026-08-05-feat-tips-via-link.md#open-questions-facts-to-confirm)
(a brand-new principal claiming, and a fresh identity seeing the received token
without manual setup) for real. No unit test can fake a principal with no
profile.

## Blocked on people, not on code

These do not block building, but they block landing. Longest lead time first.

| What | Gates | Owner |
| -------------------------------------------------------------------------------------------------------------------------- | ---------------- | -------------------------------- |
| Compliance sign-off on OISY holding a bounded, revocable authorisation over user funds for up to a week (open question 11) | branch 1 | — |
| The `Uncovered` wording — it is information about the sender | branch 4 | whoever owns the privacy promise |
| The five undrawn states, plus a light theme for every screen (the Figma page is dark-only) | branches 3, 4, 5 | design |
125 changes: 125 additions & 0 deletions docs/ai/spec-driven-development/stacked-builds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Building a multi-PR spec as a local stack

A spec from [`workflow.md`](./workflow.md) normally plans several atomic PRs —
commandments [2 and 3](../../../AGENTS.md#2-the-10-commandments-read-before-every-change)
require it. That creates a tension: each PR has to land on its own and stay small
enough to review, but the feature only works end to end once the last one exists.

This page is how to resolve it locally — one branch per planned PR, stacked
linearly, so the tip of the stack is the whole feature.

## The shape

```
main
└── feat/<slug>-1-<what> PR 1
└── feat/<slug>-2-<what> PR 2
└── feat/<slug>-3-<what> PR 3
```

Every branch is created from its **parent**, never from `main`. Three
consequences, and they are the whole point:

- The **tip is the complete feature.** Check it out and the flow runs locally.
There is no integration branch to build and no merge to redo.
- **Each diff against its parent is exactly that PR's change**, so review stays
as small as the spec intended.
- **Stack order is dependency order.** If PR 2 needs the declarations PR 1
generates with `npm run generate`, it simply has them.

### Why linear, and not a tree

Sibling branches off a shared parent look tidier, but then nothing contains the
whole feature: you need a separate integration branch that merges the siblings,
and it has to be rebuilt after every fix. A linear stack gets that integration
for free. The cost is that review serialises.

Prefer linear. Branch sideways only when two PRs are genuinely independent
**and** both need review in parallel.

## Naming

`<type>/<spec-slug>-<position>-<what>`, using the spec's own type prefix and
short slug:

| Spec | Branch |
| --------------------------------------------- | -------------------------- |
| `2026-08-05-feat-tips-via-link.md`, PR 1 of 6 | `feat/tips-1-backend` |
| same spec, PR 4 of 6 | `feat/tips-4-recipient-ui` |

The position number is what makes `git branch --list 'feat/tips-*'` print the
stack in order — which is the cheapest possible status view.

## The three operations

**Add the next branch** — from the current tip, not from `main`:

```bash
git checkout -b feat/tips-3-sender-ui feat/tips-2-service
```

**Propagate a fix — downward, never sideways.** Fix on the branch that _owns_
the code, then merge that branch into each descendant in order:

```bash
git checkout feat/tips-3-sender-ui && git merge --no-edit feat/tips-2-service
```

Do not fix a bug at the tip just because that is where you noticed it. The fix
would land in the wrong PR and the earlier one would ship broken. This is the
single rule that keeps a stack honest, and the only one worth being strict about.

**Sync with `main`** — merge into the bottom branch, then cascade:

```bash
git checkout feat/tips-1-backend && git merge --no-edit origin/main
```

Merge, never rebase: rewriting a pushed branch is forbidden by
[pr-and-ci.md §7](../pr-and-ci.md#7-updating-an-existing-pr). Only sync when you
actually need something from `main` — being behind is fine, the merge queue
handles it at merge time.

## On the remote

- Open each PR with **its parent branch as base**, not `main`. GitHub then shows
only that PR's own diff, and retargets the child to `main` by itself when the
parent merges.
- Stacked PRs are the sanctioned alternative to force-pushing — see
[pr-and-ci.md §7](../pr-and-ci.md#7-updating-an-existing-pr). Never rebase a
stack to tidy it.
- **Land bottom-up.** A PR whose parent has not merged is not ready for the
merge queue.
- Name the parent PR in each body, so a reviewer knows what they are standing on.

## Where the gates run

| Where | What must pass |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Every branch, before it is pushed | the full [local gates](../pr-and-ci.md#4-local-quality-gates) — plus the backend scripts if Rust changed |
| The tip | the manual end-to-end run of the feature against a local replica ([HACKING.md → Local development](../../../HACKING.md#local-development)) |

A branch that is green on its own but only _works_ with its descendants present
is a sign the split is wrong — either the dependency runs the wrong way, or the
two branches are one PR.

## Folding

If a branch turns out to be a handful of lines, fold it into its parent before
pushing instead of opening a PR nobody benefits from reviewing separately. The
spec's PR list is a plan, not a contract: commandment 3 asks for small PRs, not
for a specific number of them.

## Tracking

Keep the stack table in the spec's **asset folder** — `specs/<spec-name>/build-plan.md`
— with one row per PR: branch, what it contains, status.

It goes there rather than in the spec itself because it is disposable working
state, and the asset folder is already removed at
[Step 7 — Post-merge cleanup](./workflow.md#step-7--post-merge-cleanup-claude-code).
The tracker dies with the thing it tracks, which is the only way it never goes
stale in `main`.

The spec stays the source of truth for **what** to build. The build plan only
records **where** each piece currently lives.
3 changes: 3 additions & 0 deletions docs/ai/spec-driven-development/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dfinity/oisy-wallet/
├── PRODUCT.md # Living description of all current product behaviors
└── spec-driven-development/
├── workflow.md # This document
├── stacked-builds.md # Building a multi-PR spec as a local stack
└── specs/
├── 2026-05-10-feat-add-token-swapping.md
├── 2026-05-24-fix-wallet-sync-race.md
Expand Down Expand Up @@ -87,6 +88,8 @@ Open Claude Code in the oisy-wallet repo and say:

Claude Code reads the spec, reads `docs/ai/PRODUCT.md` for system context, and begins building. It has the GitHub MCP configured and can open PRs, create branches, and interact with issues directly.

**Specs that plan more than one PR** — most of them do — are built as a local stack, one branch per planned PR, so the tip of the stack is the whole feature and can be tested end to end before anything is pushed. See [`stacked-builds.md`](./stacked-builds.md).

**Update `docs/ai/PRODUCT.md` in the same PR** as the behaviour change, not afterwards. Claude Code is best placed to write the description because by the time the PR is ready, it has the implementation context (what _actually_ shipped, including any [Step 5 — Adjust](#step-5--adjust-claude-code--spec) deviations from the spec). Landing PRODUCT.md alongside the code also keeps `main` from briefly disagreeing with itself between merge and the cleanup PR. Cowork can still review the draft if a product re-think emerges.

### Step 5 — Adjust (Claude Code ↔ Spec)
Expand Down
Loading