feat: governance proposal + objection lifecycle#45
Merged
Conversation
Adds a thin commitments-only audit layer on top of the existing executeGovernance path. New entry points let an agent or member create a Draft proposal, raise objections against it, and let an org admin adopt or discard it — with the structural change applied via the same RoleRegistry path that executeGovernance already used. Spec stance: on-chain = commitments, off-chain = coordination. The contract does NOT model IDM rounds, clarifying questions, or integration. It records who proposed what and when, who objected and when their objection was resolved, and who adopted or discarded the proposal. The event log is the auditable trail; the meeting room is where the actual decision-making happens. New on MeetingFactory: - Storage: ProposalRecord and ObjectionRecord mappings + counters, appended after _initialized. Safe in clones because each clone gets a fresh layout at deploy time. uint64 timestamps pack with address to save a slot per record. - createProposal — any org member. Emits ProposalCreated with the full change payload as unindexed bytes so the indexer can persist the row from the event alone, no contract reads needed. - adoptProposal — org admin only. Calls the shared _applyChange helper, sets status=Adopted, emits ProposalAdopted. - discardProposal — org admin only. Sets status=Discarded. - raiseObjection — any org member. Only allowed on Draft proposals. - resolveObjection — original objector OR org admin. The resolvedBy address in the event lets indexers/UI distinguish withdrawal from integration. - getProposal / getObjection / proposalCount / objectionCount views. Refactor: executeGovernance extracts its decoder body into a private _applyChange(ChangeType, bytes memory) helper so both the legacy and proposal paths run the same code. executeGovernance is marked @deprecated in IMeetingFactory with a TODO to internally rewrite it as _create + _adopt once all callers migrate. Calldata→memory copy on the legacy path costs ~few hundred gas, acceptable for a deprecated entry. State machine: enforced cheaply (one SLOAD per call) without modeling the full Holacracy IDM: - Draft → {Adopted, Discarded}, no double-transition - Raised → Resolved, no double-resolve - raiseObjection only on Draft proposals - adoptProposal does NOT check for unresolved objections — the coordinator's job, documented in test_AdoptWithUnresolvedObjections Tests: new test/unit/MeetingFactoryProposals.t.sol with 21 cases — happy paths, auth failures (non-member create/objection, non-admin adopt/discard, non-objector-non-admin resolve), state machine edges (double-adopt, adopt-after-discard, discard-after-adopt, objection-on-non-draft, double-resolve, nonexistent proposal/objection), legacy-path regression (executeGovernance unchanged after refactor), and AmendRole through the proposal path to prove the shared helper works for all change types. Full forge suite: 94 passing (was 73). Monorepo build green — regenerated meetingFactoryAbi flows to agent-sdk and hola-modern via @hollab-io/contracts/actions but no downstream caller has migrated yet. Indexer ABI hand-copy + handlers land in the next commit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nifest Wires the new MeetingFactory events (commit 73bbcfd) into the Ponder indexer so the proposal and objection tables actually populate, and plumbs open proposals into the agent manifest. Schema additions (append-only, safe to drop local .ponder on dev restart): - proposal: new orgId (direct lookups without joining through meetingContractIndex), tensionHash (hex, content-address of off-chain text), changeType (uint8, mirrors HolacracyTypes.ChangeType), changeData (hex, abi-encoded payload), changeResultId (nullable, populated on adopt), resolvedBy (nullable). Kept legacy tension:text as empty-string for backward compat. - objection: new concernHash (hex), resolvedBy (nullable — objector on withdrawal or admin on integration, lets UI distinguish the two). ABI copy: apps/hollab-indexing/abis/MeetingFactoryAbi.ts gets the 5 new events (ProposalCreated / Adopted / Discarded, ObjectionRaised / Resolved). This file is hand-maintained and NOT imported from @hollab-io/contracts — documented as a recurring papercut in the sprint notes; every MeetingFactory ABI change needs a paired update here. Handlers: new apps/hollab-indexing/src/proposals.ts with 5 ponder.on subscribers. ProposalCreated carries the full change payload as unindexed bytes, so every handler persists purely from event.args — zero contract reads on the hot path. Matches the existing conventions in meetings.ts and actionVoting.ts: composite ids shaped as `${contract.toLowerCase()}-${id}`, txHash from event.transaction.hash. Manifest: - assembleOrgManifest now accepts openProposals via a backward-compat overload — old 6-arg callers still work (the 39 existing test cases did not need to change), new callers pass a ProposalRow[] before opts. Route handler in src/api/index.ts queries schema.proposal WHERE orgId=? AND status=0 (Draft) ordered by submittedAt asc and passes the rows through. - Manifest entries include a path-based permalink (/o/:orgId/p/:id) so agents can follow the reference without ad-hoc URL construction. - openProposals type on OrgManifest replaced the historical `never[]` with a concrete shape. `/agents/:orgId.json` now returns real proposal rows instead of a hardcoded empty array. Tests: 5 new cases in test/manifest.spec.ts covering the default-empty path (backward-compat overload), row → manifest mapping, permalink shape, caller-ordering preservation, and bigint serialization inside proposal rows. hollab-indexing vitest 39 → 44. Lint + typecheck + ponder codegen all clean. Live smoke test still requires a .ponder reset + DeployLocal re-run (the seed orgs from the merged WS3 PR were cloned against the pre-refactor MeetingFactory bytecode and cannot call the new functions) — scheduled for commit 5 with the seed script update. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ite methods Wires the new MeetingFactory entry points (commit 73bbcfd) and the indexer columns (commit 24c988d) through to the typed GraphQL client and the agent SDK. indexing-client: - PROPOSAL_FIELDS gains orgId, tensionHash, changeType, changeData, changeResultId, resolvedBy. OBJECTION_FIELDS gains concernHash and resolvedBy. Proposal and Objection types updated to match — new fields documented inline. - LIST_PROPOSALS_BY_ORG (all proposals in an org, submittedAt desc) and LIST_OPEN_PROPOSALS_BY_ORG (status=0 only, submittedAt asc) added alongside the existing LIST_PROPOSALS_BY_CIRCLE. - BigInt regression fixes: LIST_PROPOSALS_BY_CIRCLE's circleId and LIST_OBJECTIONS_BY_PROPOSAL's proposalId were declared as String against bigint columns — same Day 1 episode as GET_ORGANIZATION and LIST_ROLES_BY_ORG. Both now BigInt. The new queries start life correctly typed. - client.listProposalsByOrg / listOpenProposalsByOrg methods added. - MOCK_PROPOSAL and MOCK_OBJECTION fixtures in client.spec.ts updated to include the new fields so the 42 existing client tests still typecheck against the new shape. - queries.spec.ts regression set grows to 14 asserts (was 4): all five new-or-touched BigInt params plus a PROPOSAL_FIELDS column-coverage table test so dropping a new field from the selection gets caught the next time someone touches the schema. agent-sdk GovernanceModule: - createProposal(meetingFactory, input) — hashes tensionText with keccak256 by default, or accepts a pre-computed tensionHash (for content stored on IPFS/0G). Returns the extracted proposalId from the ProposalCreated event log via decodeEventLog. - adoptProposal / discardProposal / raiseObjection / resolveObjection wrappers, all going through walletClient.writeContract with the regenerated meetingFactoryAbi. - listOpenProposalsByOrg / getProposal read helpers delegate to the indexing-client (matches the existing listProposals pattern). - execute() marked deprecated pointing to the new path. - Receipt parsing uses viem decodeEventLog so it survives future ABI event reordering (the old executeGovernance path assumed topic[3] was the resultId, which is fine there but not reusable). Build + test: - @hollab-io/indexing-client: 46 → 56 passing. - @hollab-io/agent-sdk: 4 passing (encoding tests unchanged — the new write methods are thin viem wrappers best verified by a live smoke test against anvil, which commit 5 covers). - Full monorepo build + test green across 14 turbo tasks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Unblocks the #/o/:orgId/p/:proposalId route that the Day 3 PR carried over as blocked at the contract layer. Now that the proposal lifecycle exists (commits 73bbcfd + 24c988d + acd2244), the public surface can render what agents propose and how the adoption trail plays out — entirely wallet-less. New hook file apps/hola-modern/src/hooks/useProposalsFromIndexer.ts: - useOpenProposalsByOrg — status=0 (Draft), submittedAt asc, limit 50 - useProposalFromIndexer — single fetch by composite "<processAddress>-<proposalId>" id - useObjectionsByProposal — raisedAt asc, scoped by process + proposal All three go through the typed indexing-client — no wagmi, no RPC. New view PublicProposalView: - Status chip (Draft / Adopted / Discarded) with tone colors - Change type label derived from the uint8 enum - Tension hash rendered as the content-address (full text resolution is a follow-up tied to the IPFS/0G backend decision) - Proposer chip with the existing 🤖 agent allowlist treatment - Objections list with per-row raised/resolved state and objector chip - Raw change payload pre-block (developer surface; a decoded render needs the ABI + change-type case analysis, deferred) - Adopted proposals show their resultId so readers can jump back to the role/policy the proposal created Resolving composite id from (orgId, proposalId): The route carries orgId + proposalId, but the indexer keys proposals by `${processAddress}-${proposalId}`. Rather than plumb processAddress through the URL, the view first reads open proposals for the org (the org has at most one MeetingFactory clone) to discover the address, then falls back to a direct getProposal call with the composite id. Cost: one extra GraphQL request on a cache miss, acceptable for a public permalink. Cleaner than polluting the URL with an address. Router: - useHashRouter gains `{ page: "publicProposal"; orgId; proposalId }`. - Parser handles `#/o/:orgId/p/:proposalId` with the same decodeURIComponent + malformed-fallback pattern as publicRole. - App.tsx dispatches the new route inside the existing public branch (same minimal shell as the other wallet-less views, still above the auth gate). - 3 new tests: parse happy path, malformed fallback to public, and round-trip. useHashRouter suite 16 → 19; hola-modern total 23 → 26. PublicOrgView: - New "Open proposals" section surfaces between Members and Roles when openProposals.length > 0. Each row links to the permalink via a new onOpenProposal callback, same click affordance as the role cards. Shows proposal id + change-type shorthand + proposer chip (🤖 when in allowlist) + tension hash. - The old TODOs about blocked proposal list + progressive-connect CTA are removed — the first is done, the second shipped in Day 3. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…opose-tension via lifecycle
Closes out the feat/governance-proposal-lifecycle PR with the data,
examples, and docs the previous four commits were building toward.
SeedDemoOrgs:
- Renames the first demo org from paperclip to lantern. Paperclip
remains the UX inspiration the sprint doc references (paperclip.ing
is a real web2 product we're borrowing posture from), but the demo
org should not literally share the name. Lantern: illuminates,
warm, shareable, matches the "make orgs legible from the outside"
story, no brand collision in org-tooling space.
- Appends _seedProposals(lanternId, meetingFactory, agent) that
creates one proposal in each terminal state on the lantern org:
- Adopted: "QA Inspector" role — deployer creates + adopts, which
flows through the shared _applyChange helper and appends role #4
to lantern's RoleRegistry
- Draft + unresolved objection: "Storyteller" role + one raised
objection with a keccak256 concernHash
- Discarded: "Party Planner" role — created then discarded without
application
The three branches give PublicProposalView real data to render from
a clean anvil stack, including the objection trail for the Draft
path.
- Resolves the MeetingFactory clone address for the lantern org by
reading RoleRegistry.governanceProcess() rather than parsing the
MeetingComponentsDeployed event log — cleaner and doesn't require
receipt parsing in Solidity.
propose-tension.ts:
- Swaps from the deprecated executeGovernance(CreateRole) shortcut
to the full createProposal + adoptProposal lifecycle. Agent signs
createProposal (matching the "agent can propose but not adopt"
access model); admin (anvil account #0) signs adoptProposal in a
second wallet client, mirroring the SeedDemoOrgs flow.
- New --draft-only flag stops after createProposal for agents that
want to demonstrate the propose-without-adopt path. Cheap to wire,
useful for demos where the adopt step is shown in the UI instead.
- Tension text is hashed to tensionHash via the SDK's default
keccak256 path; the raw text stays off-chain in the script log.
- Polls the manifest endpoint until the new role appears in
`roles[]`, then prints both the proposal permalink
(#/o/:orgId/p/:proposalId) and the org page URL.
Spec doc — specs/05-governance-process.md:
- New "On-chain Commitments Surface" section that documents the
commitments-vs-coordination stance, maps each of the 5 new events
to its IDM step in §5.4.5, explains the bytes32 content-address
discipline for tensionHash / concernHash, lays out the
authorization table, and states the non-enforcement of the
"no-adopt-with-unresolved-objections" rule with the explicit
reasoning (validity is judgment-based, contract can't reliably
determine it, event log preserves auditability). Lists what is
deliberately out of scope (process breakdown, integrative election
internals, objection validity tests, Withdrawn status). Notes the
executeGovernance deprecation.
Sprint doc — docs/sprint-agent-native-mvp.md:
- Unblocks the #/o/:orgId/p/:proposalId carry-over bullet with a
pointer to this branch and a summary of what shipped: the
MeetingFactory surface, the indexer columns, the PublicProposalView,
the three-status seed, and the spec doc section. executeGovernance
stays deprecated-but-working for back-compat so the authed
GovernanceView + SeedDemoOrgs legacy path keep working.
Live smoke test: DeployLocal + SeedDemoOrgs run end-to-end against a
fresh anvil. roleCount goes 3 → 4 (proof the Adopted path flows
through _applyChange), proposalCount 3, objectionCount 1. Role #4 on
lantern is "QA Inspector" with the exact fields from the seed.
Full-monorepo verification:
- forge test: 94 passing
- hollab-indexing vitest: 44 passing
- @hollab-io/indexing-client vitest: 56 passing
- @hollab-io/viem-extension vitest: 29 passing
- @hollab-io/agent-sdk vitest: 4 passing
- hola-modern vitest: 26 passing
- pnpm check-types: clean
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Nothing is deployed anywhere yet, so the back-compat layer added in the previous five commits was pure overhead. Removing it: Contracts: - Delete executeGovernance() entry point from MeetingFactory.sol - Delete GovernanceExecuted event from IMeetingFactory.sol - Delete executeGovernance declaration from IMeetingFactory.sol - Delete test_ExecuteGovernance_StillWorksAfterRefactor (was only there to prove the refactor didn't break the legacy path — no legacy path now). 20 passing MeetingFactoryProposals tests remain. - Migrate SeedDemoOrgs._seedOrg baseline role creation and agent election to createProposal + adoptProposal. Same signer (deployer = member + admin) so both calls land in the same broadcast. On-chain result is identical to the previous path but now every role mutation leaves a ProposalRecord on-chain. Indexer: - Drop the legacy `tension: text` column from ponder.schema.ts (was kept empty-string for "back-compat with historical readers" — no historical readers exist). - Remove `tension: ""` assignment from the ProposalCreated handler. - Simplify assembleOrgManifest signature — remove the ProposalRow[] | ManifestOpts overload that was preserving the old 6-arg call shape. openProposals is now a required 6th positional param. - Update all 28 assembleOrgManifest call sites in test/manifest.spec.ts to pass the new [] argument before OPTS. 44 passing. indexing-client: - Drop `tension: string` field from the Proposal type. - Drop `tension` from PROPOSAL_FIELDS GraphQL selection. - Drop `tension` from the MOCK_PROPOSAL test fixture. - All 56 tests still pass; regression set untouched. agent-sdk: - Delete execute(), createRole(), amendRole(), removeRole(), electLead() methods from GovernanceModule — all of them were thin wrappers around the deleted executeGovernance. createProposal is the primitive now; callers that want ergonomic role creation call createProposal with encodeCreateRole from `encoding.ts` (the propose-tension example already shows the pattern). - Delete ExecuteGovernanceResult type from types.ts and the re-export from index.ts. - Clean up unused imports in governance.ts. Frontend: - Rewrite GovernanceMeetingRoom's buildGovernanceCall as buildGovernanceCalls — returns a [createCall, adoptCall] pair per action instead of a single executeGovernance call. Caller reads proposalCount() once via usePublicClient before iterating and predicts sequential ids (startingCount + supportedIndex + 1). Safe inside the authed meeting room because the signer is a single wallet submitting one batch — no interleaving writers. wallet_sendCalls batches both txs atomically; sequential fallback signs them in order. Result: the authed meeting room now leaves a ProposalRecord on-chain for every governance change it enacts. - Delete the useExecuteGovernance hook function. Keep encoders (encodeCreateRole / encodeAmendRole / encodeRemoveRole / encodeElection / *WithRefs) and the ChangeType enum mirror — GovernanceMeetingRoom still imports them. File kept at its current path to avoid churn on the import paths; header updated. - Drop the dead `if (proposal.tension)` fallback from PublicProposalView — the column no longer exists. Docs: - specs/05-governance-process.md "Legacy entry point" section replaced with "Only entry point" — states cleanly that createProposal + adoptProposal is the only path to structural change. - docs/sprint-agent-native-mvp.md note updated: executeGovernance is deleted entirely, not deprecated. Verification: - forge test: 93 passing (was 94; -1 for the deleted legacy regression test) - hollab-indexing vitest: 44 passing - @hollab-io/indexing-client vitest: 56 passing - @hollab-io/viem-extension vitest: 29 passing - @hollab-io/agent-sdk vitest: 4 passing - hola-modern vitest: 26 passing - pnpm check-types: clean - Live smoke test: DeployLocal + SeedDemoOrgs run end-to-end on fresh anvil. Verified roleCount=4 (baseline 3 + adopted "QA Inspector"), proposalCount=7 (3 baseline creates + 1 election + 3 state-demo proposals — every role mutation now leaves a ProposalRecord), objectionCount=1, role #4 body matches the seed exactly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update top-level docs to match the shipped proposal lifecycle, add CLAUDE.md agent context, and queue two PRDs (public/private tensions, public objection flow) under docs/prds/. Flag Aztec privacy posture as v2 in specs/07 and prepend a current-state block to the sprint log. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a commitments-only proposal + objection lifecycle to
MeetingFactory, then plumbs it all the way through — indexer, typed GraphQL client, agent SDK, frontend hooks, public permalink view, seed data, example script, spec doc — in a single PR so no intermediate state is left half-wired.This closes the Day 2 sprint finding that blocked the
#/o/:orgId/p/:proposalIdpermalink at the contract layer (no proposal events, no lifecycle). It also unlocks the agent-native story's final beat: an agent can propose a structural change and a human admin can adopt it, and every step is a public, cold-openable link.Design stance (documented in
specs/05-governance-process.md"On-chain Commitments Surface"): IDM rounds, clarifying questions, and integration discussion are coordination — they stay off-chain. What goes on-chain is the audit trail of commitments: who proposed what, who objected when, who adopted or discarded it.The 5 commits
73bbcfd24c988dacd2244governance.createProposaletc.10aa7acPublicProposalView,#/o/:orgId/p/:proposalIdroute, open proposals paneld12222epaperclip→lantern), 3-state proposal seed,propose-tension.tsswap, spec doc section38ea727executeGovernance+ back-compat layer entirely, migrateSeedDemoOrgs._seedOrgandGovernanceMeetingRoom.buildGovernanceCallto create+adopt, drop legacytension:textcolumn andassembleOrgManifestoverload (-417/+205 lines)Each commit compiles + tests green in isolation; the branch point between "contract + indexer + SDK" (1–3) and "frontend + seed + docs" (4–5) is clean if a future split is ever wanted.
Contract surface
New on
MeetingFactory(all additive, no ABI break on existing methods):Design calls
Auth:
createProposal/raiseObjection= any org member.adoptProposal/discardProposal= org admin only.resolveObjection= original objector (withdrawal) OR org admin (integration). TheresolvedByin the event lets UIs distinguish the two paths without a separate status.No adopt-with-unresolved-objections enforcement on-chain. The constitution's objection validity criteria (§5.3.4) are judgment-based; the contract cannot reliably determine them without duplicating the facilitator's role. The event log preserves full auditability. Cheap state-machine invariants are enforced:
Draft → {Adopted, Discarded},Raised → Resolved, no objections on non-Draft proposals, no double-resolve.changeDatastored on-chain in theProposalRecord, not hash-then-resupply. This matches the agent-native UX ("agent proposes now, admin adopts tomorrow from a different wallet without reconstructing the payload"). TypicalCreateRolepayload is ~300–800 bytes, ~5–15k gas of SSTORE — acceptable for governance frequency.bytes32content-addresses fortensionHashandconcernHash. The contract is indifferent to scheme —keccak256(utf8(rawText))for inline text, CIDv1 sha256-truncated for IPFS, 0G Merkle root for 0G Storage. KeepsContentRefpolymorphic across backends.uint64timestamps packed with address in storage — saves a slot per record._applyChangehelper refactor:executeGovernanceextracted its decoder body into a private_applyChange(ChangeType, bytes memory)which both the legacy entry point andadoptProposalcall. LegacyexecuteGovernanceis unchanged on the ABI and all pre-existing tests still pass — 73 → 94 forge tests.No back-compat layer. Originally shipped with
executeGovernancemarked@deprecatedso the merged WS3 seed and demo flows kept working without edits. Simplified in commit38ea727— since nothing is deployed anywhere yet, the legacy path is dead weight.executeGovernanceis deleted entirely;createProposal+adoptProposalis the only path to structural change. Seed script and authed meeting room were migrated to the new path in the same commit.Indexer
apps/hollab-indexing/src/proposals.tsis new — 5 handlers, all persisting purely from event args (no contract reads on the hot path, becauseProposalCreatedcarriesbytes changeDataunindexed).ponder.schema.tsadditions (append-only; local.ponderneeds one reset in dev to pick them up):proposalgetsorgId,tensionHash,changeType,changeData,changeResultId,resolvedBy— and keeps the legacytension: textcolumn as empty-string for back-compatobjectiongetsconcernHash,resolvedByManifest (
apps/hollab-indexing/src/api/manifest.ts):assembleOrgManifestgains a 6thopenProposalsparam via a backward-compat overload so all 39 existing test call sites keep working unchanged. New route handler queriesschema.proposal WHERE orgId=? AND status=0ordered bysubmittedAtasc.permalink(/o/:orgId/p/:id) so agents can follow references without ad-hoc URL construction.test/manifest.spec.ts: backward-compat default-empty path, row → manifest mapping, permalink shape, ordering preservation, bigint serialization. Manifest suite 39 → 44.Hand-maintained
apps/hollab-indexing/abis/MeetingFactoryAbi.ts— the 5 new events copied in. This file is a recurring papercut (not imported from@hollab-io/contracts); every MeetingFactory ABI change needs a paired update here.indexing-client + agent-sdk
PROPOSAL_FIELDS/OBJECTION_FIELDSextended with the new columns.Proposal/Objectiontypes updated to match.LIST_PROPOSALS_BY_ORG,LIST_OPEN_PROPOSALS_BY_ORGadded.listProposalsByOrg/listOpenProposalsByOrgclient methods added.LIST_PROPOSALS_BY_CIRCLE.$circleIdandLIST_OBJECTIONS_BY_PROPOSAL.$proposalIdwere both declared asString!against bigint columns — same Day 1 episode asGET_ORGANIZATION/LIST_ROLES_BY_ORG. Both nowBigInt!. New queries start life correctly typed.queries.spec.tsregression set grows from 4 → 14 asserts, including a newPROPOSAL_FIELDScolumn-coverage table test so dropping a field from the selection is caught the next time someone touches the schema.MOCK_PROPOSAL/MOCK_OBJECTIONfixtures inclient.spec.tsupdated to the new shape so all 42 existing client tests still typecheck.GovernanceModulegains:createProposal,adoptProposal,discardProposal,raiseObjection,resolveObjection,listOpenProposalsByOrg,getProposal. Receipt parsing uses viemdecodeEventLogso it survives future ABI reordering (the oldexecuteGovernancepath assumedtopic[3]was the resultId — fine there but not reusable).executemethod marked@deprecatedwith a pointer to the new path.Frontend
New hook file
useProposalsFromIndexer.ts:useOpenProposalsByOrg(orgId)— Drafts only, submittedAt asc, limit 50useProposalFromIndexer(id)— single fetch by composite<processAddress>-<proposalId>useObjectionsByProposal(processAddress, proposalId)— raisedAt ascAll three go through the typed
indexing-client— zero wagmi, zero RPC. Safe to mount above the auth gate.New view
PublicProposalView:uint8enumchangeDatapre-block (developer surface; a decoded render needs the ABI + change-type case analysis, deferred)resultIdso readers can jump back to the role the proposal createdComposite id resolution: the route carries
orgId + proposalId, but the indexer keys proposals by${processAddress}-${proposalId}. Rather than plumbprocessAddressthrough the URL, the view first reads open proposals for the org (the org has at most oneMeetingFactoryclone) to discover the address, then falls back to a directgetProposalcall with the composite id. One extra GraphQL request on cache miss, cleaner than polluting the URL with an address.Router:
useHashRoutergains{ page: "publicProposal"; orgId; proposalId }. Parser handles#/o/:orgId/p/:proposalIdwith the samedecodeURIComponent+ malformed-fallback pattern aspublicRole.App.tsxdispatches the new route in the existing public branch. 3 new tests.useHashRoutersuite 16 → 19;hola-moderntotal 23 → 26.PublicOrgView: new "Open proposals" section between Members and Roles, rendered whenopenProposals.length > 0. Links to the permalink via a newonOpenProposalcallback, matches the role-card affordance. Shows proposal id + change-type shorthand + proposer chip (🤖 when in allowlist) + tension hash.Seed + example + docs
SeedDemoOrgs.s.sol:paperclip→lantern. Paperclip remains the UX inspiration the sprint doc references (paperclip.ing is a real web2 product we're borrowing posture from), but the demo org should not literally share the name. Lantern: illuminates, warm, shareable, no brand collision in org-tooling space._seedProposals(lanternId, meetingFactory, agent)appends one proposal in each terminal state on lantern:_applyChangeand appends role feat: governor by OZ + ENS sudbomain #4 to lantern'sRoleRegistrykeccak256concernHashMeetingFactoryclone address by readingRoleRegistry.governanceProcess()— cleaner than parsing theMeetingComponentsDeployedevent log in Solidity.propose-tension.ts:executeGovernance(CreateRole)to the fullcreateProposal+adoptProposallifecycle. Agent signscreateProposal(matches the access model: agent can propose, admin adopts). Admin (anvil account #0) signsadoptProposalvia a second wallet client.--draft-onlyflag stops aftercreateProposalfor demos that want to show the adopt step in the UI instead.keccak256path; raw text stays off-chain.roles[], then prints both the proposal permalink (#/o/:orgId/p/:proposalId) and the org page URL.specs/05-governance-process.md: new "On-chain Commitments Surface" section. Commitments-vs-coordination stance, event → IDM-step mapping table, content-address discipline, authorization table, non-enforcement reasoning, out-of-scope list (process breakdown, integrative election internals, objection validity tests,Withdrawnstatus), legacy entry point note.docs/sprint-agent-native-mvp.md: unblocks the/p/:proposalIdcarry-over bullet with a pointer to this branch.Live smoke test
Ran end-to-end against a local anvil:
Verified on-chain state directly via
cast call:id=2, subnamelantern✓proposalCount: 7✓ — 3 baseline role creations + 1 agent Election + 3 proposal-state-demo proposals. After commit38ea727, every role mutation in the seed flows throughcreateProposal+adoptProposal, so every structural change leaves a ProposalRecord on-chain.objectionCount: 1✓roleCount: 4(baseline 3 + the Adopted "QA Inspector") — proves the adopt path flows through_applyChangeand mutatesRoleRegistryTest plan
forge test— 93 passing (was 73; +20UnitMeetingFactoryProposalscases after commit38ea727dropped the legacy regression test)@hollab-io/contracts— forge build clean (only pre-existingerc20-unchecked-transferwarnings onE2EJourney.t.sol)hollab-indexingvitest — 44 passing (was 39; +5openProposalscases)@hollab-io/indexing-clientvitest — 56 passing (was 46; +10 regression asserts onBigInt!+PROPOSAL_FIELDScoverage)@hollab-io/viem-extensionvitest — 29 passing (unchanged)@hollab-io/agent-sdkvitest — 4 passing (unchanged — the new write methods are thin viem wrappers, covered by the live smoke test + thepropose-tension.tsswap)hola-modernvitest — 26 passing (was 23; +3useHashRoutercases for the proposal permalink)pnpm check-types— clean across all 4 typechecked packagespnpm lint— clean (4 pre-existing react-hooks warnings inhola-modern, 0 errors)pnpm build— full monorepo build green, hola-modern production bundle builds to IPFS-ready SPADeployLocal+SeedDemoOrgsbothONCHAIN EXECUTION COMPLETE & SUCCESSFUL, verifiedlantern.roleCount=4and "QA Inspector" role exists viacast call./scripts/dev-local.sh+cd packages/contracts && forge script script/SeedDemoOrgs.s.sol --tc SeedDemoOrgs --rpc-url http://127.0.0.1:8545 --broadcast, then:curl http://localhost:42069/agents/2.json | jq '.openProposals | length'→ should be 1 (the Draft)curl http://localhost:42069/agents/2.json | jq '.org.roleCount'→ should be 4 (original 3 + the Adopted "QA Inspector")http://localhost:5173/#/o/2in incognito → "Open proposals" panel shows 1 Draft with the 🤖 chip wiringPublicProposalViewrenders with the objection row in "raised" stateAGENT_PRIVATE_KEY=0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 pnpm --filter @hollab-io/agent-sdk tsx examples/propose-tension.ts 2→ agent proposes, admin adopts, permalink printsFollow-ups explicitly out of scope for this PR
PublicProposalViewis a one-line hook addition once the backend lands.changeDatarendering — the view currently shows the raw hex. Decoding needs a change-type switch and the role ABI; deferred to a dedicated polish pass.GovernanceViewstill callsexecuteGovernancedirectly. It keeps working (deprecated path), but migrating the authed flow tocreateProposal+adoptProposalis a separate frontend PR so this one stays reviewable.executeGovernanceremoval —was marked deprecated-but-working in commits 1-5, now deleted entirely in commit38ea727. No back-compat scaffolding remains.specs/05-governance-process.md"Out of scope (for now)".🤖 Generated with Claude Code