@zigrivers/scaffold 3.26.0 → 3.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +21 -7
  2. package/content/knowledge/core/ai-memory-management.md +17 -0
  3. package/content/knowledge/core/claude-md-patterns.md +2 -2
  4. package/content/knowledge/core/coding-conventions.md +2 -2
  5. package/content/knowledge/core/task-decomposition.md +4 -4
  6. package/content/knowledge/core/task-tracking.md +120 -29
  7. package/content/knowledge/core/user-stories.md +1 -1
  8. package/content/knowledge/execution/multi-agent-coordination.md +118 -0
  9. package/content/knowledge/execution/task-claiming-strategy.md +15 -3
  10. package/content/knowledge/execution/worktree-management.md +5 -3
  11. package/content/knowledge/web3/web3-access-control.md +189 -0
  12. package/content/knowledge/web3/web3-architecture.md +162 -0
  13. package/content/knowledge/web3/web3-audit-workflow.md +151 -0
  14. package/content/knowledge/web3/web3-common-vulnerabilities.md +171 -0
  15. package/content/knowledge/web3/web3-conventions.md +162 -0
  16. package/content/knowledge/web3/web3-deployment-and-verification.md +216 -0
  17. package/content/knowledge/web3/web3-dev-environment.md +150 -0
  18. package/content/knowledge/web3/web3-gas-optimization.md +165 -0
  19. package/content/knowledge/web3/web3-oracles-and-external-data.md +155 -0
  20. package/content/knowledge/web3/web3-project-structure.md +212 -0
  21. package/content/knowledge/web3/web3-requirements.md +152 -0
  22. package/content/knowledge/web3/web3-security.md +163 -0
  23. package/content/knowledge/web3/web3-testing.md +180 -0
  24. package/content/knowledge/web3/web3-upgradeability.md +189 -0
  25. package/content/methodology/web3-overlay.yml +40 -0
  26. package/content/pipeline/build/multi-agent-resume.md +27 -7
  27. package/content/pipeline/build/multi-agent-start.md +35 -7
  28. package/content/pipeline/build/new-enhancement.md +8 -1
  29. package/content/pipeline/build/quick-task.md +9 -0
  30. package/content/pipeline/build/single-agent-resume.md +11 -4
  31. package/content/pipeline/build/single-agent-start.md +13 -4
  32. package/content/pipeline/consolidation/workflow-audit.md +1 -1
  33. package/content/pipeline/environment/git-workflow.md +2 -2
  34. package/content/pipeline/foundation/beads.md +148 -22
  35. package/content/pipeline/foundation/coding-standards.md +1 -1
  36. package/content/tools/post-implementation-review.md +6 -6
  37. package/content/tools/prompt-pipeline.md +1 -1
  38. package/content/tools/release.md +5 -5
  39. package/content/tools/review-code.md +347 -3
  40. package/content/tools/review-pr.md +349 -7
  41. package/content/tools/version-bump.md +5 -5
  42. package/dist/cli/commands/observe.d.ts +2 -0
  43. package/dist/cli/commands/observe.d.ts.map +1 -1
  44. package/dist/cli/commands/observe.js +9 -1
  45. package/dist/cli/commands/observe.js.map +1 -1
  46. package/dist/cli/commands/observe.test.js +36 -0
  47. package/dist/cli/commands/observe.test.js.map +1 -1
  48. package/dist/config/schema.d.ts +672 -126
  49. package/dist/config/schema.d.ts.map +1 -1
  50. package/dist/config/schema.js +8 -1
  51. package/dist/config/schema.js.map +1 -1
  52. package/dist/config/schema.test.js +2 -2
  53. package/dist/config/schema.test.js.map +1 -1
  54. package/dist/config/validators/index.d.ts.map +1 -1
  55. package/dist/config/validators/index.js +2 -0
  56. package/dist/config/validators/index.js.map +1 -1
  57. package/dist/config/validators/web3.d.ts +4 -0
  58. package/dist/config/validators/web3.d.ts.map +1 -0
  59. package/dist/config/validators/web3.js +15 -0
  60. package/dist/config/validators/web3.js.map +1 -0
  61. package/dist/e2e/project-type-overlays.test.js +76 -0
  62. package/dist/e2e/project-type-overlays.test.js.map +1 -1
  63. package/dist/observability/adapters/beads.d.ts +4 -0
  64. package/dist/observability/adapters/beads.d.ts.map +1 -1
  65. package/dist/observability/adapters/beads.js +25 -2
  66. package/dist/observability/adapters/beads.js.map +1 -1
  67. package/dist/observability/adapters/beads.test.js +40 -2
  68. package/dist/observability/adapters/beads.test.js.map +1 -1
  69. package/dist/observability/engine/ledger-writer.d.ts +11 -1
  70. package/dist/observability/engine/ledger-writer.d.ts.map +1 -1
  71. package/dist/observability/engine/ledger-writer.js +6 -0
  72. package/dist/observability/engine/ledger-writer.js.map +1 -1
  73. package/dist/observability/engine/llm-dispatcher.d.ts.map +1 -1
  74. package/dist/observability/engine/llm-dispatcher.js +36 -5
  75. package/dist/observability/engine/llm-dispatcher.js.map +1 -1
  76. package/dist/observability/engine/llm-dispatcher.test.js +23 -0
  77. package/dist/observability/engine/llm-dispatcher.test.js.map +1 -1
  78. package/dist/project/adopt.d.ts.map +1 -1
  79. package/dist/project/adopt.js +3 -1
  80. package/dist/project/adopt.js.map +1 -1
  81. package/dist/project/detectors/coverage.test.js +3 -2
  82. package/dist/project/detectors/coverage.test.js.map +1 -1
  83. package/dist/project/detectors/disambiguate.js +1 -1
  84. package/dist/project/detectors/disambiguate.js.map +1 -1
  85. package/dist/project/detectors/index.d.ts.map +1 -1
  86. package/dist/project/detectors/index.js +2 -0
  87. package/dist/project/detectors/index.js.map +1 -1
  88. package/dist/project/detectors/resolve-detection.test.js +57 -0
  89. package/dist/project/detectors/resolve-detection.test.js.map +1 -1
  90. package/dist/project/detectors/types.d.ts +6 -2
  91. package/dist/project/detectors/types.d.ts.map +1 -1
  92. package/dist/project/detectors/types.js.map +1 -1
  93. package/dist/project/detectors/web3.d.ts +4 -0
  94. package/dist/project/detectors/web3.d.ts.map +1 -0
  95. package/dist/project/detectors/web3.js +37 -0
  96. package/dist/project/detectors/web3.js.map +1 -0
  97. package/dist/project/detectors/web3.test.d.ts +2 -0
  98. package/dist/project/detectors/web3.test.d.ts.map +1 -0
  99. package/dist/project/detectors/web3.test.js +75 -0
  100. package/dist/project/detectors/web3.test.js.map +1 -0
  101. package/dist/types/config.d.ts +8 -1
  102. package/dist/types/config.d.ts.map +1 -1
  103. package/dist/wizard/copy/core.d.ts.map +1 -1
  104. package/dist/wizard/copy/core.js +4 -0
  105. package/dist/wizard/copy/core.js.map +1 -1
  106. package/dist/wizard/copy/index.d.ts.map +1 -1
  107. package/dist/wizard/copy/index.js +2 -0
  108. package/dist/wizard/copy/index.js.map +1 -1
  109. package/dist/wizard/copy/types.d.ts +5 -1
  110. package/dist/wizard/copy/types.d.ts.map +1 -1
  111. package/dist/wizard/copy/types.test-d.js +7 -0
  112. package/dist/wizard/copy/types.test-d.js.map +1 -1
  113. package/dist/wizard/copy/web3.d.ts +3 -0
  114. package/dist/wizard/copy/web3.d.ts.map +1 -0
  115. package/dist/wizard/copy/web3.js +15 -0
  116. package/dist/wizard/copy/web3.js.map +1 -0
  117. package/dist/wizard/questions.d.ts +2 -1
  118. package/dist/wizard/questions.d.ts.map +1 -1
  119. package/dist/wizard/questions.js +8 -1
  120. package/dist/wizard/questions.js.map +1 -1
  121. package/dist/wizard/questions.test.js +14 -0
  122. package/dist/wizard/questions.test.js.map +1 -1
  123. package/dist/wizard/wizard.d.ts.map +1 -1
  124. package/dist/wizard/wizard.js +1 -0
  125. package/dist/wizard/wizard.js.map +1 -1
  126. package/package.json +1 -1
@@ -0,0 +1,180 @@
1
+ ---
2
+ name: web3-testing
3
+ description: Testing discipline for Foundry smart contracts — unit, fuzz, invariant, and fork tests with coverage and gas snapshots wired into CI
4
+ topics: [web3, testing, foundry, fuzz, invariants, fork-tests]
5
+ ---
6
+
7
+ Smart contracts must work the first time. Once deployed, a bug is a bounty for the next adversary who reads your storage layout, and "we'll patch it next sprint" is not an option when the only patch path is a migration to a brand-new address. Foundry's `forge test` runner makes property-level confidence cheap: unit tests pin known behavior, fuzz tests stress the boundaries you forgot, invariants assert the laws that must hold across every state sequence, and fork tests replay against real mainnet state. Use all four — none of them substitutes for any of the others.
8
+
9
+ ## Summary
10
+
11
+ Treat Foundry testing as four layers with distinct jobs. Use **unit tests** (`test_` prefix, `Test`-extended contracts) for deterministic state-change, event, and revert assertions. Use **fuzz tests** (`testFuzz_` prefix) to let Foundry generate adversarial inputs; constrain with `vm.assume` and bump `[fuzz] runs` when the property warrants. Use **invariant tests** (`invariant_` functions with a `Handler` contract) to assert system-wide properties that must hold across every reachable state. Use **fork tests** (`--fork-url` with a pinned block number) as the contract equivalent of e2e — replay against real protocols, real liquidity, real attackers. Wire `forge coverage` and `forge snapshot --check` into CI so coverage and gas regressions fail the build.
12
+
13
+ ## Deep Guidance
14
+
15
+ ### Unit tests
16
+
17
+ Every external function in a contract has at least three unit tests: happy path, every revert branch, and every emitted event. Foundry's `Test` base contract supplies `vm.expectRevert`, `vm.expectEmit`, and `vm.prank` for the three; `setUp()` deploys fresh contracts before each test so state never leaks across tests.
18
+
19
+ ```solidity
20
+ // test/Vault.t.sol
21
+ import {Test} from "forge-std/Test.sol";
22
+ import {Vault} from "../src/Vault.sol";
23
+
24
+ contract VaultTest is Test {
25
+ Vault vault;
26
+ address alice = makeAddr("alice");
27
+
28
+ event Deposited(address indexed user, uint256 amount);
29
+
30
+ function setUp() public {
31
+ vault = new Vault();
32
+ vm.deal(alice, 10 ether);
33
+ }
34
+
35
+ function test_Deposit_IncrementsBalance() public {
36
+ vm.prank(alice);
37
+ vault.deposit{value: 1 ether}();
38
+ assertEq(vault.balanceOf(alice), 1 ether);
39
+ }
40
+
41
+ function test_Deposit_EmitsEvent() public {
42
+ vm.expectEmit(true, false, false, true);
43
+ emit Deposited(alice, 1 ether);
44
+ vm.prank(alice);
45
+ vault.deposit{value: 1 ether}();
46
+ }
47
+
48
+ function test_RevertWhen_DepositZero() public {
49
+ vm.prank(alice);
50
+ vm.expectRevert(Vault.ZeroAmount.selector);
51
+ vault.deposit{value: 0}();
52
+ }
53
+ }
54
+ ```
55
+
56
+ Name tests after the behavior, not the function: `test_Deposit_IncrementsBalance` and `test_RevertWhen_DepositZero` read as sentences in CI output. Use `makeAddr("alice")` instead of hard-coded addresses so labels appear in traces. Run with `forge test -vv` locally; bump to `-vvvv` when a failing test needs full call traces.
57
+
58
+ ### Fuzz tests
59
+
60
+ A fuzz test takes parameters and Foundry feeds it pseudo-random values — 256 runs by default. The job is to encode a property that must hold for every input in the valid range, then let the fuzzer try to break it. Use `vm.assume` to reject inputs outside the domain (overflow, zero address) without polluting the test body with conditionals.
61
+
62
+ ```solidity
63
+ function testFuzz_Deposit_BalanceMatchesInput(uint96 amount) public {
64
+ vm.assume(amount > 0 && amount <= 1000 ether);
65
+ vm.deal(alice, amount);
66
+ vm.prank(alice);
67
+ vault.deposit{value: amount}();
68
+ assertEq(vault.balanceOf(alice), amount);
69
+ }
70
+ ```
71
+
72
+ Prefer narrow types (`uint96`, `uint128`) when the domain is bounded — they generate more interesting values than `uint256` and naturally avoid overflow. Bump the run count for properties that matter via `foundry.toml`:
73
+
74
+ ```toml
75
+ [fuzz]
76
+ runs = 1024
77
+ max_test_rejects = 65536
78
+ ```
79
+
80
+ When a fuzz test fails, Foundry prints the failing seed; rerun with `forge test --fuzz-seed <seed>` to reproduce.
81
+
82
+ ### Invariant tests
83
+
84
+ Unit and fuzz tests assert behavior for a single call. Invariants assert properties that must hold *across every state sequence the system can reach*. The fuzzer calls random functions on random target contracts with random arguments; after each sequence the invariant function is checked. The classic invariant: total supply equals the sum of balances, no matter what trades, transfers, or deposits happened.
85
+
86
+ Constrain the actor surface with a `Handler` contract — without one, the fuzzer wastes runs on reverting calls.
87
+
88
+ ```solidity
89
+ // test/handlers/VaultHandler.sol
90
+ contract VaultHandler is Test {
91
+ Vault public vault;
92
+ uint256 public ghost_totalDeposited;
93
+ address[] public actors;
94
+
95
+ constructor(Vault _vault) {
96
+ vault = _vault;
97
+ for (uint256 i; i < 5; ++i) actors.push(makeAddr(string.concat("actor", vm.toString(i))));
98
+ }
99
+
100
+ function deposit(uint256 actorSeed, uint96 amount) external {
101
+ address actor = actors[actorSeed % actors.length];
102
+ amount = uint96(bound(amount, 1, 100 ether));
103
+ vm.deal(actor, amount);
104
+ vm.prank(actor);
105
+ vault.deposit{value: amount}();
106
+ ghost_totalDeposited += amount;
107
+ }
108
+ }
109
+
110
+ // test/Vault.invariant.t.sol
111
+ contract VaultInvariantTest is Test {
112
+ Vault vault;
113
+ VaultHandler handler;
114
+
115
+ function setUp() public {
116
+ vault = new Vault();
117
+ handler = new VaultHandler(vault);
118
+ targetContract(address(handler));
119
+ }
120
+
121
+ function invariant_TotalSupplyEqualsSumOfBalances() public view {
122
+ assertEq(address(vault).balance, handler.ghost_totalDeposited());
123
+ }
124
+ }
125
+ ```
126
+
127
+ Run with `forge test --invariant` or tune via `[invariant] runs`, `depth`, and `fail_on_revert` in `foundry.toml`. Set `fail_on_revert = true` while writing handlers so silent reverts surface; flip to `false` once the handler is realistic.
128
+
129
+ ### Fork tests
130
+
131
+ A fork test runs against a snapshot of a real chain. This is how you assert "our adapter actually works against the live Uniswap pool" without redeploying the entire DeFi stack to a local node. Always pin the block number — without one, the test will silently behave differently as mainnet state drifts.
132
+
133
+ ```solidity
134
+ contract UniswapAdapterForkTest is Test {
135
+ uint256 mainnetFork;
136
+ address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
137
+
138
+ function setUp() public {
139
+ mainnetFork = vm.createSelectFork(vm.envString("MAINNET_RPC"), 19_000_000);
140
+ }
141
+
142
+ function test_SwapAgainstLiveLiquidity() public {
143
+ // ... interacts with real USDC, real Uniswap V3 pool, real prices
144
+ }
145
+ }
146
+ ```
147
+
148
+ Run with `forge test --fork-url $MAINNET_RPC --fork-block-number 19000000` for one-off fork suites, or use `vm.createSelectFork` per-test for multi-chain tests. Use `vm.makePersistent(address)` to keep a deployed test contract alive across `selectFork` calls. Fork tests are slow — mark them with a separate profile in `foundry.toml` and run nightly, not on every commit.
149
+
150
+ ### Coverage and gas snapshots
151
+
152
+ `forge coverage` reports line, branch, and function coverage. Target >90% line coverage and >80% branch coverage on `src/` — anything lower means a code path has no test pinning it.
153
+
154
+ ```bash
155
+ forge coverage --report lcov --report summary
156
+ forge coverage --report lcov --no-match-coverage "(test|script)"
157
+ ```
158
+
159
+ Gas costs are part of the contract's contract with the world. `forge snapshot` writes `.gas-snapshot`; commit it. `forge snapshot --check` fails CI when any test's gas cost regresses beyond the tolerance, so an "innocent refactor" that doubles the gas of a hot function gets caught in review.
160
+
161
+ ```bash
162
+ forge snapshot # record
163
+ forge snapshot --check --tolerance 1 # CI gate (1% drift allowed)
164
+ forge test --gas-report # per-function gas table
165
+ ```
166
+
167
+ ### Cheatcodes reference
168
+
169
+ A short list of the cheatcodes you reach for daily, all under the `vm` namespace from `forge-std/Test.sol`:
170
+
171
+ - `vm.prank(addr)` / `vm.startPrank(addr)` / `vm.stopPrank()` — set `msg.sender` for the next call or a range of calls.
172
+ - `vm.expectRevert(selector)` — assert the next call reverts with the given custom-error selector or string.
173
+ - `vm.expectEmit(checkTopic1, checkTopic2, checkTopic3, checkData)` — emit the expected event, then make the call.
174
+ - `vm.warp(timestamp)` / `vm.roll(blockNumber)` — fast-forward `block.timestamp` and `block.number`.
175
+ - `vm.deal(addr, amount)` — set an account's ETH balance directly.
176
+ - `vm.assume(condition)` — discard fuzz inputs that violate a precondition.
177
+ - `vm.label(addr, "name")` and `makeAddr("name")` — readable addresses in traces.
178
+ - `vm.createSelectFork(url, block)` / `vm.makePersistent(addr)` — fork-test plumbing.
179
+
180
+ When a test gets noisy with cheatcode plumbing, that is a signal to extract a helper into the `Test` base or a shared utility, not to keep stacking lines.
@@ -0,0 +1,189 @@
1
+ ---
2
+ name: web3-upgradeability
3
+ description: Upgradeable contracts — when not to upgrade, UUPS vs Transparent vs Beacon, OpenZeppelin Upgrades with the Foundry plugin, storage gaps, ERC-7201, initializers, timelocked authorization
4
+ topics: [web3, upgradeability, proxy, openzeppelin, storage]
5
+ ---
6
+
7
+ Upgradeable contracts trade simplicity for the ability to fix bugs after deployment. The cost is real: a new threat surface (the upgrade key itself), a class of storage-layout bugs that do not exist in immutable contracts, and a permanent dependency on whoever holds upgrade rights. An "upgradeable" protocol is one whose trust assumptions include a future action by an admin — every user of the protocol is implicitly trusting that admin to behave, in perpetuity. The honest default for most protocols is: do not upgrade unless you have to.
8
+
9
+ ## Summary
10
+
11
+ Default to immutable contracts and reach for upgradeability only when the protocol genuinely needs it — regulatory shifts, post-audit critical fixes, or planned feature evolution. When you do upgrade, prefer UUPS over the Transparent proxy: it is gas-cheaper and the upgrade logic lives on the implementation where it can be audited as a normal contract function. Use OpenZeppelin's `@openzeppelin/contracts-upgradeable` library with the `openzeppelin-foundry-upgrades` plugin so storage-layout validation runs as part of CI — skipping that validation is how protocols brick themselves. For new protocols, use ERC-7201 namespaced storage instead of the `__gap` pattern; it eliminates an entire category of storage-collision bugs at the cost of slightly more verbose accessors. Always gate `_authorizeUpgrade` behind `onlyRole(UPGRADER_ROLE)` and route the role through a `TimelockController` so users have an exit window.
12
+
13
+ ## Deep Guidance
14
+
15
+ ### When to upgrade (and when not to)
16
+
17
+ Immutable contracts have a smaller attack surface, a cleaner decentralization story, and a much shorter trust prompt: "the code you see is the code that will run, forever." Upgradeable contracts can never make that claim. Every governance-related FAQ, every audit report, every user-facing risk disclosure has to spend a paragraph on the upgrade path — who can call it, how long it takes, what they can change. That is not free.
18
+
19
+ Reach for upgradeability when the trade is worth it: a regulated stablecoin issuer who must be able to comply with new sanctions regimes, a lending market whose risk parameters need to evolve as collateral assets change, a protocol whose audit recommended a fix that the team wants to be able to ship after launch without forcing every integrator to migrate. Do not reach for upgradeability because "we might want to change things later" — that is the same impulse that makes throwaway scripts grow into production systems by accident. If the only reason you can articulate for upgradeability is optionality, ship immutable and earn the trust dividend.
20
+
21
+ A useful test: write the sentence "users must trust ___ to ___" and see if you can defend it. "Users must trust the team's 4-of-7 Safe, behind a 72h timelock, to upgrade the protocol only when an audit-validated patch is needed" is defensible. "Users must trust the team" is not.
22
+
23
+ A pragmatic middle ground worth considering before reaching for full proxy upgradeability: ship immutable core contracts with a clearly-scoped admin surface (set fee, set oracle, set risk parameter), and migrate users to a new immutable deployment if the core logic itself ever needs to change. Most protocols that think they need upgradeability actually need parameter governance, which is much simpler and considerably less dangerous. Reserve true upgradeability for code paths where a migration would be operationally impossible — token contracts with millions of existing holders, NFT collections with active marketplaces, deeply-integrated infrastructure that other protocols import as a dependency.
24
+
25
+ When a migration is feasible, prefer it. Migrations make the trust boundary explicit (users opt in by moving funds), they let the new contract address be re-audited as if it were a fresh deployment, and they avoid the perpetual "what if the upgrade key gets compromised" tail risk. The cost is real — coordinating wallet UIs, third-party integrations, exchange listings, indexer pipelines, and user education is non-trivial — but it is a one-time cost, paid in exchange for permanent simplicity of the trust model. Compare that to the recurring cost of every upgrade requiring a fresh community review, fresh audit, fresh announcement, and fresh timelock execution. Migrations are often cheaper in total over a multi-year horizon than the cumulative cost of running an upgradeable system safely.
26
+
27
+ ### Proxy patterns
28
+
29
+ Three proxy patterns dominate the OpenZeppelin ecosystem:
30
+
31
+ - **`TransparentUpgradeableProxy`** — Separates the admin (who can call upgrade functions on the proxy) from users (whose calls are forwarded to the implementation). Requires a `ProxyAdmin` contract sitting beside the proxy. Slightly more expensive per call because of the admin-vs-user check; safer in older Solidity versions where function-selector clashes were a worry.
32
+ - **`UUPSUpgradeable`** — The upgrade function lives on the implementation contract itself, not the proxy. The proxy is minimal — just `delegatecall` and a storage slot for the implementation address. Cheaper at runtime, simpler bytecode, and the upgrade authorization is a normal Solidity function on the implementation that auditors can reason about like any other access-controlled function.
33
+ - **`BeaconProxy`** — Many proxies share a single beacon contract that holds the implementation address. Upgrading the beacon upgrades every proxy that points at it. Useful when you deploy many instances of the same contract (per-user vaults, per-market lending pools) and want one upgrade to fan out to all of them.
34
+
35
+ UUPS is the right default for most protocols. The slimmer proxy is gas-cheaper for every user call for the entire life of the protocol — a non-trivial saving at scale — and the upgrade logic is a single auditable function rather than a separate `ProxyAdmin` contract. The catch: UUPS has a uniquely dangerous failure mode. If your v2 implementation forgets to inherit `UUPSUpgradeable` or accidentally removes the `_authorizeUpgrade` function, the proxy is permanently stuck on v2 with no upgrade path. The `openzeppelin-foundry-upgrades` plugin checks this for you; never deploy a UUPS upgrade without running it.
36
+
37
+ Reach for the Transparent proxy when the upgrade authorization model is materially different from the rest of the contract's access control — for example, a third party (like a foundation or DAO timelock) holds the upgrade key while the protocol team holds operational roles. The `ProxyAdmin` separation cleanly enforces "the upgrader cannot call user functions, even by accident" at the proxy layer rather than depending on Solidity-level modifiers. Reach for the Beacon pattern when you operate a contract factory — a yield-vault platform deploying one vault per strategy, a lending protocol deploying one pool per market — and want a single upgrade to propagate to every deployed instance atomically. Each pattern is correct in its niche; UUPS is the right answer when you do not have a strong reason to pick one of the other two.
38
+
39
+ A note on minimal proxies (EIP-1167 "clone" contracts): these are not upgradeable. Clones are a deployment-cost optimization — many cheap proxies forwarding all calls to a single immutable implementation — and changing the implementation requires deploying new clones. Do not confuse the two when picking a pattern. If you want one-shot cheap deployments without upgrade capability, use clones; if you want upgradeability across many deployed instances, use beacons.
40
+
41
+ ### OpenZeppelin Upgrades + Foundry plugin
42
+
43
+ Use `@openzeppelin/contracts-upgradeable` for the implementation and the `openzeppelin-foundry-upgrades` plugin to deploy and validate. The plugin parses the storage layout of both old and new implementations and refuses to deploy an upgrade that would corrupt state — reordered fields, changed types, removed variables without a gap. CI should fail any PR that triggers a layout-incompatible change without an explicit reviewer override.
44
+
45
+ ```solidity
46
+ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
47
+ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
48
+ import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
49
+
50
+ contract Protocol is Initializable, UUPSUpgradeable, AccessControlUpgradeable {
51
+ bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
52
+
53
+ /// @custom:oz-upgrades-unsafe-allow constructor
54
+ constructor() {
55
+ _disableInitializers(); // see "Initializers vs constructors" below
56
+ }
57
+
58
+ function initialize(address admin, address upgrader) external initializer {
59
+ __AccessControl_init();
60
+ __UUPSUpgradeable_init();
61
+ _grantRole(DEFAULT_ADMIN_ROLE, admin);
62
+ _grantRole(UPGRADER_ROLE, upgrader);
63
+ }
64
+
65
+ function _authorizeUpgrade(address newImpl) internal override onlyRole(UPGRADER_ROLE) {}
66
+ }
67
+ ```
68
+
69
+ Deploy via the plugin's `Upgrades.deployUUPSProxy(...)` helper, and run `Upgrades.upgradeProxy(...)` for subsequent versions. Both invocations validate storage layout against the previously-deployed implementation before broadcasting the transaction. Treat the validation output as a release-gating signal; the plugin is the single most effective defense against storage-corruption bugs you can wire into a CI run.
70
+
71
+ Wire the plugin into CI explicitly, not just into local dev. Add a job that runs the upgrade-validation step against the current main-branch deployment artifacts for every PR that touches an upgradeable contract — if the PR introduces an incompatible layout change, the CI job fails before review even starts. The plugin emits a structured error pointing at the specific variable that broke compatibility, which makes triage fast. Pair this with a deployment artifact convention: every deployed implementation address gets recorded in a `deployments/<chain>.json` file checked into the repo, so future upgrades have a stable reference to compare against. The plugin uses these artifacts as its source of truth for "what is currently deployed."
72
+
73
+ ### Storage collision and the gap pattern
74
+
75
+ In a proxy-based contract, state lives in the proxy's storage and is read through slot offsets baked into the implementation bytecode. When v2 of the implementation adds a state variable in the middle of the existing list, every later variable shifts by one slot — and the values that v1 wrote now appear in the wrong fields. `owner` becomes garbage, `totalSupply` reads from a different variable's slot, and the protocol silently corrupts.
76
+
77
+ The traditional fix is the storage gap: reserve a fixed array of unused slots at the end of every upgradeable contract so v2 can append fields by shrinking the gap rather than shifting existing slots.
78
+
79
+ ```solidity
80
+ contract ProtocolV1 is Initializable, UUPSUpgradeable {
81
+ uint256 public totalSupply;
82
+ mapping(address => uint256) public balances;
83
+ // ... other state ...
84
+
85
+ /// @custom:storage-location erc7201:none
86
+ uint256[50] private __gap; // reserve 50 slots for future fields
87
+ }
88
+
89
+ // In ProtocolV2:
90
+ contract ProtocolV2 is Initializable, UUPSUpgradeable {
91
+ uint256 public totalSupply;
92
+ mapping(address => uint256) public balances;
93
+ uint256 public newFeeBps; // new field consumes 1 slot from the gap
94
+ uint256[49] private __gap; // gap shrinks by exactly 1
95
+ }
96
+ ```
97
+
98
+ The discipline is exact: when you add a 1-slot field, shrink the gap by 1. When you add a `mapping`, shrink by 1 (mappings always take one slot, regardless of contents). When you add a `struct`, shrink by however many slots the struct packs into. The Foundry plugin verifies the math; do not eyeball it.
99
+
100
+ Two further hazards specific to the gap pattern. Inheritance chains compound the bookkeeping — every parent contract in the upgradeable hierarchy needs its own gap, sized for that parent's future evolution, and changes to a parent contract's storage have to respect every child's gap. This is one of the strongest arguments for ERC-7201 in any contract with non-trivial inheritance. And: do not "rescue" leftover gap space by inserting a field at the start of the contract's state. Adding `uint256 public newFee` at the top reshuffles every existing slot. The gap exists at the end of state precisely so additions land in unused, never-written slots; appending is safe, prepending is catastrophic.
101
+
102
+ ### ERC-7201 namespaced storage (preferred for new protocols)
103
+
104
+ ERC-7201 ("Namespaced Storage Layout") avoids the slot-collision problem entirely by storing state inside a library-style struct anchored to a deterministic, name-derived slot, instead of sequential slot 0 onward. Add fields freely without worrying about layout drift — there is no "next slot" to corrupt because each contract's state lives in its own keccak-derived corner of storage.
105
+
106
+ ```solidity
107
+ library ProtocolStorage {
108
+ /// @custom:storage-location erc7201:scaffold.protocol.main
109
+ struct Layout {
110
+ uint256 totalSupply;
111
+ mapping(address => uint256) balances;
112
+ uint256 newFeeBps; // added in v2 — no gap to shrink, no slot to collide
113
+ }
114
+
115
+ // keccak256(abi.encode(uint256(keccak256("scaffold.protocol.main")) - 1)) & ~bytes32(uint256(0xff))
116
+ bytes32 internal constant SLOT =
117
+ 0x...; // computed once, hardcoded
118
+
119
+ function layout() internal pure returns (Layout storage l) {
120
+ bytes32 slot = SLOT;
121
+ assembly { l.slot := slot }
122
+ }
123
+ }
124
+ ```
125
+
126
+ Implementations read and write via `ProtocolStorage.layout().totalSupply` rather than a direct state variable. The cost is verbosity — every storage access goes through the library — and slightly more bytecode. The benefit is that upgrade reviews stop being about slot math and start being about business logic. Prefer ERC-7201 for any new upgradeable protocol; the `__gap` pattern is fine for existing contracts that already shipped without namespaced storage but is no longer the recommended default.
127
+
128
+ ERC-7201 also makes mixin-style composition safe. A `Pausable` mixin can declare its own namespaced storage struct under `scaffold.protocol.pausable`, and a `Permit` mixin can declare its own under `scaffold.protocol.permit`, and the two will never collide regardless of inheritance order — each lives in a deterministic slot derived from its name, not from its position in the linearization. Contrast this with the gap pattern, where reordering parents in `is A, B, C` versus `is A, C, B` shifts storage slots in ways that can corrupt state without changing any field. Namespaced storage decouples logical composition from physical storage layout, which is exactly the invariant you want for a long-lived upgradeable system.
129
+
130
+ ### Initializers vs constructors
131
+
132
+ Proxies do not run the implementation's constructor — storage lives on the proxy, but a constructor only writes to the implementation contract's own storage, which the proxy never reads. Initialization runs through a regular function on the implementation, called via the proxy after deployment. Use the `initializer` modifier from `Initializable` to prevent the function from being called twice:
133
+
134
+ ```solidity
135
+ function initialize(address admin) external initializer {
136
+ __AccessControl_init();
137
+ __UUPSUpgradeable_init();
138
+ _grantRole(DEFAULT_ADMIN_ROLE, admin);
139
+ }
140
+ ```
141
+
142
+ Two related hazards. First, every parent contract that has its own initializer needs an explicit `__Parent_init()` call from your `initialize` function — forgetting one leaves that subsystem (AccessControl, Pausable, ERC20) in a half-initialized state where modifiers pass but invariants are wrong. Second, the implementation contract itself — independent of the proxy — is a normal deployed contract that an attacker can call directly. If you leave its `initialize` callable, the attacker becomes admin of the implementation. They cannot reach proxy state, but they can call self-destructing or upgrade-bricking functions on the implementation. Defend with `_disableInitializers()` in the constructor:
143
+
144
+ ```solidity
145
+ /// @custom:oz-upgrades-unsafe-allow constructor
146
+ constructor() {
147
+ _disableInitializers();
148
+ }
149
+ ```
150
+
151
+ The `unsafe-allow constructor` annotation tells the OpenZeppelin plugin that this constructor is intentional and safe — it touches no storage, it only locks down the implementation. Skipping `_disableInitializers()` for a UUPS implementation is a textbook audit finding; bake it into every upgradeable contract template.
152
+
153
+ When a v2 upgrade needs to write new state on first use (set a new fee parameter, initialize a new mapping, populate a new role), use the `reinitializer(version)` modifier rather than `initializer`:
154
+
155
+ ```solidity
156
+ function initializeV2(uint256 newFeeBps) external reinitializer(2) {
157
+ ProtocolStorage.layout().newFeeBps = newFeeBps;
158
+ }
159
+ ```
160
+
161
+ `reinitializer(2)` runs once and only once, and only if the contract has been initialized to a version strictly less than 2. Call it as the same transaction that performs the upgrade (the OpenZeppelin plugin supports this via `Upgrades.upgradeProxy(..., abi.encodeCall(Protocol.initializeV2, (newFee)))`) so the protocol is never observable in a partially-upgraded state. Skipping the atomic init-during-upgrade pattern leaves a window — sometimes minutes, sometimes blocks — where the proxy points at v2 logic but has not yet had `initializeV2` called, which is precisely the kind of inconsistency attackers monitor for.
162
+
163
+ ### Upgrade authorization
164
+
165
+ `_authorizeUpgrade(address)` is the single most dangerous function in an upgradeable protocol. Whoever can call it can replace your contract logic with anything — a backdoor, a rug pull, an oracle override. Treat it accordingly:
166
+
167
+ - Gate with `onlyRole(UPGRADER_ROLE)` (or `onlyOwner` if you have explicitly chosen the simpler model). Never leave it empty or default-public.
168
+ - Grant `UPGRADER_ROLE` to a `TimelockController` (see `web3-access-control.md`), never directly to a Safe or EOA. The timelock buys users a 48–72 hour window between proposal and execution to exit if the upgrade is malicious.
169
+ - Audit `_authorizeUpgrade` specifically. It is one line of Solidity that gates the entire contract; any reviewer should be able to recite its access check from memory.
170
+ - Emit an event on every upgrade — OpenZeppelin's `Upgraded(address)` from ERC-1967 fires automatically, but pair it with off-chain alerting so an unexpected upgrade pages your team within minutes.
171
+
172
+ Calibrate the timelock delay to the protocol's exit time, exactly as in the access-control doc: an upgrade users cannot react to in 24 hours should not run on a 24-hour timelock. The asymmetry — admin proposes fast, users react slow — is the entire point.
173
+
174
+ Two operational practices that strengthen this layer further. First, publish the proposed implementation address and its source code as soon as the upgrade is scheduled — not only on-chain via the timelock event, but in a human-readable post (Discord, governance forum, blog) explaining what changed and why. Users cannot evaluate the upgrade if all they see is an opaque bytecode hash. Second, simulate the upgrade against a forked mainnet state before the timelock window opens. The simulation produces a diff of every storage slot the upgrade touches; reviewers can audit that diff rather than re-reading the Solidity source. Both practices add hours of work to the upgrade process, which is precisely the point — slowing upgrades down is the security feature.
175
+
176
+ ### Common pitfalls
177
+
178
+ - **Forgetting `_disableInitializers()` in the constructor.** Lets an attacker take over the implementation contract directly.
179
+ - **Reusing storage slots.** Reordering, renaming with a type change, or deleting fields without a gap corrupts state. The Foundry plugin catches this; do not bypass it.
180
+ - **Changing variable types between versions.** `uint128` to `uint256` looks innocent but changes how the slot is packed. Treat any type change as a storage migration, not a refactor.
181
+ - **Forgetting to shrink the storage gap.** Adding a new field without subtracting from `__gap` shifts everything after the gap. Always update both in the same diff.
182
+ - **Removing `_authorizeUpgrade` in a v2 UUPS upgrade.** The proxy cannot be upgraded again. There is no recovery.
183
+ - **Granting `UPGRADER_ROLE` directly to an EOA "for now".** That EOA is one phishing email from being able to replace your protocol with arbitrary bytecode.
184
+ - **Skipping the OpenZeppelin Foundry plugin in CI.** The plugin is the canonical defense against storage and authorization bugs; running it manually means somebody will eventually forget.
185
+ - **Calling `selfdestruct` from an implementation, even by accident.** `delegatecall` runs the destruct in the proxy's context, deleting the proxy. The infamous Parity multisig freeze hit this exact pattern. Never include `selfdestruct` in an upgradeable implementation.
186
+ - **Letting the deployer EOA temporarily hold `UPGRADER_ROLE` after deploy.** The brief window between deploy and role transfer is exactly when phishing campaigns target your team. Grant `UPGRADER_ROLE` to the timelock from the constructor's `initialize` call, never to the deployer.
187
+ - **Treating the implementation as "internal."** Block explorers index implementation addresses and verify their source independently of the proxy. Anyone can call them directly. Assume every external function on the implementation is reachable by an attacker — and use `_disableInitializers` accordingly.
188
+
189
+ See `web3-access-control.md` for `UPGRADER_ROLE` and `TimelockController` wiring, `web3-security.md` for the broader security posture this upgrade model sits inside, and `web3-audit-workflow.md` for the upgrade-specific checks an auditor will run against your storage layout, initializer, and authorization function.
@@ -0,0 +1,40 @@
1
+ # methodology/web3-overlay.yml
2
+ name: web3
3
+ description: >
4
+ Web3 overlay — injects smart-contract domain knowledge (EVM chains) into
5
+ existing pipeline steps for contract architecture, security, testing,
6
+ upgradeability, gas optimization, and audit workflow.
7
+ project-type: web3
8
+
9
+ knowledge-overrides:
10
+ # Foundational
11
+ create-prd: { append: [web3-requirements] }
12
+ user-stories: { append: [web3-requirements] }
13
+ coding-standards: { append: [web3-conventions] }
14
+ project-structure: { append: [web3-project-structure] }
15
+ dev-env-setup: { append: [web3-dev-environment] }
16
+ git-workflow: { append: [web3-conventions] }
17
+
18
+ # Architecture & Design
19
+ system-architecture: { append: [web3-architecture, web3-access-control, web3-upgradeability, web3-oracles-and-external-data] }
20
+ tech-stack: { append: [web3-architecture, web3-dev-environment] }
21
+ adrs: { append: [web3-architecture, web3-upgradeability] }
22
+ domain-modeling: { append: [web3-architecture] }
23
+ api-contracts: { append: [web3-architecture] }
24
+ security: { append: [web3-security, web3-common-vulnerabilities, web3-access-control] }
25
+ operations: { append: [web3-deployment-and-verification, web3-gas-optimization] }
26
+
27
+ # Testing
28
+ tdd: { append: [web3-testing] }
29
+ add-e2e-testing: { append: [web3-testing] }
30
+ create-evals: { append: [web3-testing, web3-common-vulnerabilities] }
31
+
32
+ # Reviews
33
+ review-architecture: { append: [web3-architecture, web3-access-control, web3-upgradeability] }
34
+ review-api: { append: [web3-architecture] }
35
+ review-security: { append: [web3-security, web3-common-vulnerabilities, web3-audit-workflow] }
36
+ review-operations: { append: [web3-deployment-and-verification, web3-gas-optimization] }
37
+ review-testing: { append: [web3-testing, web3-audit-workflow] }
38
+
39
+ # Planning
40
+ implementation-plan: { append: [web3-architecture] }
@@ -9,7 +9,7 @@ outputs: []
9
9
  conditional: null
10
10
  stateless: true
11
11
  category: pipeline
12
- knowledge-base: [tdd-execution-loop, task-claiming-strategy, worktree-management]
12
+ knowledge-base: [tdd-execution-loop, task-claiming-strategy, worktree-management, multi-agent-coordination]
13
13
  reads: [coding-standards, tdd, git-workflow]
14
14
  argument-hint: "<agent-name>"
15
15
  ---
@@ -85,7 +85,7 @@ Before doing anything else, confirm the environment:
85
85
  - If NOT in a worktree, stop and instruct the user to set one up or navigate to the correct directory
86
86
 
87
87
  2. **Beads identity** (if `.beads/` exists)
88
- - `echo $BD_ACTOR` — should show `$ARGUMENTS`
88
+ - `echo $BEADS_ACTOR` — should show `$ARGUMENTS`
89
89
  - If not set, the worktree setup may be incomplete
90
90
 
91
91
  ### State Recovery
@@ -114,14 +114,14 @@ Recover your context by checking the current state of work:
114
114
  ### Beads Recovery
115
115
 
116
116
  **If Beads is configured** (`.beads/` exists):
117
- - `bd list --actor $ARGUMENTS` — check for tasks with `in_progress` status owned by this agent
118
- - If a PR shows as merged, close the corresponding task: `bd close <id> && bd sync`
117
+ - `bd list --assignee $ARGUMENTS` — check for tasks with `in_progress` status owned by this agent
118
+ - If a PR shows as merged, close the corresponding task: `bd close <id>`
119
119
  - If there is in-progress work, finish it (see "Resume In-Progress Work" below)
120
120
  - Otherwise, clean up and start fresh:
121
121
  - `git fetch origin --prune && git clean -fd`
122
122
  - Run the install command from CLAUDE.md Key Commands
123
- - `bd ready` to find the next available task
124
- - Continue working until `bd ready` shows no available tasks
123
+ - Atomically claim the next ready task: `TASK=$(bd ready --claim --json | jq -r '.id')` (sets `assignee=$BEADS_ACTOR` + `status=in_progress`; no race window between agents).
124
+ - Continue working until `bd ready --claim --json` returns no task.
125
125
 
126
126
  **Without Beads:**
127
127
  - Read `docs/implementation-playbook.md` as the primary task reference.
@@ -171,8 +171,28 @@ Once in-progress work is complete (or if there was none):
171
171
  - Fix any findings at or above `fix_threshold` before proceeding
172
172
 
173
173
  3. **Create PR** (if not already created for in-progress work)
174
+ - If Beads is configured, run the PR-readiness checklist first:
175
+ ```bash
176
+ if [ -d .beads ]; then
177
+ bd preflight
178
+ fi
179
+ ```
180
+ Fix any issues `bd preflight` flags before proceeding.
181
+ - **For 3+ parallel agents**, acquire the project's merge slot to serialize merge-time conflicts:
182
+ ```bash
183
+ if [ -d .beads ]; then
184
+ bd merge-slot acquire --wait # blocks if held; queues you in priority order
185
+ fi
186
+ ```
187
+ There is one merge slot per project; `--wait` blocks until you have it. Skip for single-agent or two-agent runs. See `content/knowledge/execution/multi-agent-coordination.md`.
174
188
  - Push the branch: `git push -u origin HEAD`
175
189
  - Create a pull request: `gh pr create`
190
+ - After the PR merges (or if you abandon the work), release the slot:
191
+ ```bash
192
+ if [ -d .beads ]; then
193
+ bd merge-slot release # holder verified via $BEADS_ACTOR
194
+ fi
195
+ ```
176
196
  - Include agent name in PR description for traceability
177
197
 
178
198
  4. **Run code reviews (MANDATORY)**
@@ -219,7 +239,7 @@ Once in-progress work is complete (or if there was none):
219
239
  - Push updates and re-request review
220
240
 
221
241
  **Task was completed by another agent:**
222
- - If Beads: `bd sync` will show updated task states
242
+ - If Beads: A `git pull` (and `bd dolt pull` if a Dolt remote is configured) brings the local DB current; run `bd doctor --fix` if anything looks stale.
223
243
  - Without Beads: check the plan/playbook for recently completed tasks and open PRs
224
244
  - Skip to the next available task
225
245
 
@@ -9,7 +9,7 @@ outputs: []
9
9
  conditional: null
10
10
  stateless: true
11
11
  category: pipeline
12
- knowledge-base: [tdd-execution-loop, task-claiming-strategy, worktree-management]
12
+ knowledge-base: [tdd-execution-loop, task-claiming-strategy, worktree-management, multi-agent-coordination]
13
13
  reads: [coding-standards, tdd, git-workflow]
14
14
  argument-hint: "<agent-name>"
15
15
  ---
@@ -90,7 +90,7 @@ Before writing any code, verify the worktree environment:
90
90
  - If on a feature branch with changes, redirect to `/scaffold:multi-agent-resume $ARGUMENTS`
91
91
 
92
92
  3. **Beads identity** (if `.beads/` exists)
93
- - `echo $BD_ACTOR` — should show `$ARGUMENTS`
93
+ - `echo $BEADS_ACTOR` — should show `$ARGUMENTS`
94
94
  - If not set, the worktree setup may be incomplete
95
95
 
96
96
  4. **Dependency check**
@@ -119,11 +119,13 @@ These rules are critical for multi-agent operation:
119
119
 
120
120
  **If Beads is configured** (`.beads/` exists):
121
121
  - Branch naming: `bd-<id>/<desc>`
122
- - Run `bd ready` to see available tasks
123
- - Pick the lowest-ID unblocked task
122
+ - Verify `$BEADS_ACTOR` is set per agent (echo it; bail if empty).
123
+ - Atomically claim the next ready task: `TASK=$(bd ready --claim --json | jq -r '.id')`
124
+ - This sets `assignee=$BEADS_ACTOR` and `status=in_progress` in a single round-trip — eliminates the race window where two agents both see the same "ready" task.
125
+ - If `bd ready --claim` returns no task, you're done — exit the loop.
124
126
  - Implement following the TDD workflow below
125
- - After PR is merged: `bd close <id> && bd sync`
126
- - Repeat with `bd ready` until no tasks remain
127
+ - After PR is merged: `bd close <id>`
128
+ - Repeat (`bd ready --claim --json`) until no tasks remain
127
129
 
128
130
  **Without Beads:**
129
131
  - Branch naming: `<type>/<desc>` (e.g., `feat/add-auth`)
@@ -174,8 +176,28 @@ For each task:
174
176
  - Fix any findings at or above `fix_threshold` before proceeding
175
177
 
176
178
  7. **Create PR**
179
+ - If Beads is configured, run the PR-readiness checklist first:
180
+ ```bash
181
+ if [ -d .beads ]; then
182
+ bd preflight
183
+ fi
184
+ ```
185
+ Fix any issues `bd preflight` flags before proceeding.
186
+ - **For 3+ parallel agents**, acquire the project's merge slot to serialize merge-time conflicts:
187
+ ```bash
188
+ if [ -d .beads ]; then
189
+ bd merge-slot acquire --wait # blocks if held; queues you in priority order
190
+ fi
191
+ ```
192
+ There is one merge slot per project; `--wait` blocks until you have it. Skip for single-agent or two-agent runs. See `content/knowledge/execution/multi-agent-coordination.md`.
177
193
  - Push the branch: `git push -u origin HEAD`
178
194
  - Create a pull request: `gh pr create`
195
+ - After the PR merges (or if you abandon the work), release the slot:
196
+ ```bash
197
+ if [ -d .beads ]; then
198
+ bd merge-slot release # holder verified via $BEADS_ACTOR
199
+ fi
200
+ ```
179
201
  - Include in the PR description: what was implemented, key decisions, files changed, agent name
180
202
  - Follow the PR workflow from `docs/git-workflow.md` or CLAUDE.md
181
203
 
@@ -210,10 +232,16 @@ For each task:
210
232
  - Resolve conflicts, re-run tests, force-push the branch
211
233
 
212
234
  **Another agent claimed the same task:**
213
- - If Beads: `bd sync` will reveal the conflict pick a different task
235
+ - If Beads: A `git pull` (and `bd dolt pull` if a Dolt remote is configured) brings the local DB current; run `bd doctor --fix` if anything looks stale.
214
236
  - Without Beads: check open PRs (`gh pr list`) for overlapping work
215
237
  - Move to the next available unblocked task
216
238
 
239
+ **A downstream task is blocked on a specific async condition (PR merge, workflow run, timer, human decision):**
240
+ - If Beads: create a gate that blocks the downstream task. The gate has an auto-generated ID. For a PR-merge blocker: `bd gate create --type=gh:pr --blocks <task-id> --await-id=<pr-number> --reason "..."`. For a human-resolved blocker: `bd gate create --blocks <task-id> --reason "..."` (defaults to `--type=human`). Capture the gate ID via `--json | jq -r '.id'` if you need to resolve manually later.
241
+ - The gated task disappears from `bd ready` until the gate resolves. `gh:pr` / `gh:run` / `timer` gates auto-resolve via watchers; `human` gates resolve via `bd gate resolve <gate-id>`.
242
+ - If multiple downstream tasks share one underlying blocker, create one gate per blocked task pointing at the same `--await-id`. For dependency-style blocking ("this task can't start until that task finishes"), use `bd dep add --blocks` instead.
243
+ - See `content/knowledge/execution/multi-agent-coordination.md` for the full pattern.
244
+
217
245
  **Dependency install fails after cleanup:**
218
246
  - `git clean -fd` may have removed generated files — re-run the full install sequence
219
247
  - If persistent, check if another agent's merge changed the dependency file
@@ -263,9 +263,16 @@ For each user story (or logical grouping of small stories):
263
263
  **If Beads:**
264
264
  ```bash
265
265
  bd create "US-XXX: <imperative title>" -p <priority>
266
- # Priority: 0=blocking release, 1=must-have, 2=should-have, 3=nice-to-have
266
+ # Priority: 0=blocking release, 1=must-have, 2=should-have, 3=nice-to-have, 4=backlog
267
267
  ```
268
268
 
269
+ For architectural decisions (ADRs), use the built-in `decision` type:
270
+ ```bash
271
+ bd create "Use Postgres over MySQL for X" -t decision -p 1
272
+ ```
273
+
274
+ If your project enables custom types via `bd config set types.custom '["story","milestone","spike"]'`, you can also use `-t story` for user-story-sized work and `-t milestone` for releases.
275
+
269
276
  **Without Beads:** Document tasks as a structured list in `docs/implementation-plan.md` with title, priority, dependencies, and description.
270
277
 
271
278
  #### Task Titles and Descriptions
@@ -157,6 +157,7 @@ Assign priority using Beads conventions:
157
157
  - **P1** — Must-have for current milestone
158
158
  - **P2** — Should-have (default for most quick tasks)
159
159
  - **P3** — Nice-to-have, backlog
160
+ - **P4** — Backlog / future-consideration (lowest priority; effectively deferred)
160
161
 
161
162
  #### Acceptance Criteria
162
163
  Write 2-5 testable acceptance criteria in Given/When/Then format:
@@ -202,6 +203,14 @@ bd create "type(scope): description" -p <priority>
202
203
  # Example: bd create "fix(auth): prevent duplicate session creation on rapid re-login" -p 2
203
204
  ```
204
205
 
206
+ If this task was discovered while doing other work, link the lineage with `discovered-from`:
207
+ ```bash
208
+ bd create "fix(parser): handle empty input edge case" \
209
+ --type bug -p 2 \
210
+ --deps discovered-from:$CURRENT_TASK_ID
211
+ ```
212
+ The new task appears in `bd ready` normally; `discovered-from` is metadata for traceability and does NOT block readiness.
213
+
205
214
  **Without Beads:** Document the task inline and proceed directly to implementation.
206
215
 
207
216
  Then set the task description with the full context from Phase 2. Include all of: