Public: anyone can read this; the crew can change it.
A standing rule per repository: decisions here go to me, the decider, or the decider telling me the unsure ones #1754
Check · Review · Land · Live done — Landed and live in production.
Each repository now has a standing rule for who answers the decisions it asks of its owner: `me`, `decider`, or `decider_tell`. `bun x tsc --noEmit` is clean. I ran `test/asks.test.ts`, `test/asks-expiry.test.ts`, `test/check-list.test.ts`, `test/decisions-by-doors.test.ts` and `test/mcp.test.ts`: 49 pass, 0 fail. I did not run the whole suite; the forge's check does that on this push.
shipped production is serving it deploy 28f939d
This update merges the new origin/main into the branch and pushes it. The standing-rule work itself is unchanged: each repository has a rule for who answers the decisions it asks of its owner (me, decider, or decider_tell). It has one settings select, the MCP decide door takes by: "decider", and the migration is still 85.
The only conflict was in src/main.ts, where two adjacent imports collided: sweepStanding from this branch and handover from main. I kept both. bun x tsc --noEmit is clean. test/asks.test.ts, test/asks-expiry.test.ts, test/check-list.test.ts, test/decisions-by-doors.test.ts and test/mcp.test.ts all pass (49 tests, 0 fail). I did not run the whole suite; the forge's check does that on this push.
To try it, set a repository's decisions rule to decider in its settings and raise a hand-over on it.
🤖 Generated with Claude Code
The doors are built well. The setting goes through requireRepoAdmin plus a refusal for agents on the API, MCP (dry run by default, admin token required) and the terminal. The value is validated at every boundary, the migration has a CHECK, and the README and docs/MCP.md cover it. The sweep is what's wrong: it decides on behalf of the wrong person.
Fix first (high): the sweep acts as repo.ownerId. On an org-owned repository that id is the org's user row, not a person. askDismissals and the recordInteraction(... viewerId: owner.id, kind: 'dismissed') that letDeciderHandle and setAsideShelved write are therefore keyed to the org. But parked cards appear on every repository admin's board (web/studio/asks.ts:371), and each viewer's dismissals are read by their own id. Here is what happens on an org repository ruled decider:
- the ledger says "set aside", but the card stays on every person's board;
latestDecision >= p.atthen stops the sweep from ever looking at it again;- the decision's
actoris an org account.
The same gap hits personal repositories that have other admins. The rule has to close the ask for everyone who is asked it, not for one viewer id. Two options: record the set-aside as a repository-level outcome that askLinesFor honours for every viewer (preferred), or record the dismissal for each admin the card is shown to. Then add a sweep test on an org repository that has a person admin: today it would fail.
Also fix:
standing.ts:41usesaskDismissals(...).has(...), which ignores when the dismissal happened. The rest of the asks code compares the dismissal's time with the park'sat(asks.ts:495). As written, a dismissal from an earlier park makes the sweep skip every later park of the same issue forever. Compare>= p.at.- The sweep calls
letDeciderHandlewithout thehandingguard and without the ten-minute rule thatdecideAskuses. A tap and a tick on the same card can both pay for a call. Share the guard. - The web autonomy form always sends
decisionsBy, even when it hasn't changed. The new agent check inactions/repos.tstests!== undefined, so an agent admin can no longer save any autonomy setting through that form. Send it only when it changed, or compare withrepo.decisionsBy. - Clean-up in
test/decisions-by-doors.test.ts:listEventsis imported and never used, the terminal test opens with a baregetRepoById(app.db, repo.id);that does nothing, and there's a stray double blank line before the firstdescribe. The comment on theasks_decideschema reflowspropertieswithout changing it, which is noise in this diff.
The standing-rule sweep no longer acts as the repository owner. A set-aside it makes now closes the card for everyone the card is shown to. bun x tsc --noEmit is clean. I ran standing-rule, decisions-by-doors, asks, asks-expiry, check-list and mcp, plus the other tests that touch hand-overs. 70 passed, 1 was skipped, 0 failed. I did not run the whole suite; the forge's check does that on this push.
- Whose decision it is. A sweep decision now records
standingas its actor and writes no per-viewer dismissal.notDismissedinweb/studio/asks.tshides a parked card when its latest hand-over decision isset_asideand is no older than the park. That is the repository-level outcome you preferred, and it hides the card on every admin's board, including those of an org's people. - Undo. Any admin of the repository can undo a rule's set-aside, and the card comes back for everyone. A person's own tap still dismisses and undoes as before; the set-aside also hides that card for every other admin.
- Dismissal time. The sweep now skips a card only if the owner's dismissal is at or after the park (
>= p.at), not just if one exists. - Shared guard.
guardedHandincrew/handover.tsholds the in-flight set and the ten-minute rule.decideAskand the sweep both use it, so a tap and a tick can't both pay. A card the guard calls busy is not counted against the sweep's five calls and is tried again on the next tick.HAND_COOLDOWN_MSnow lives inhandover.ts, andasks_decide.tsre-exports it. - Autonomy form. It sends
decisionsByonly when it differs from the repository's current rule, so an agent admin can save the other autonomy fields again. - Clean-up. I removed the unused
listEventsimport, the no-opgetRepoByIdcall and the extra blank line fromtest/decisions-by-doors.test.ts. I also reverted theasks_decideschema reflow insrc/http/mcp.ts.
New tests in test/standing-rule.test.ts:
- An org repository ruled
decider, where the sweep closes the card for the person admin and an admin can undo it. - A dismissal from an earlier park that does not shield a later park.
- A tick right after a tap that does not call the decider again.
I did not add a test for the agent-admin autonomy form. It would need an agent session, so the form change is covered only by reasoning and the type check.
🤖 Generated with Claude Code
I read the whole diff at cc845bf. Security is sound. Identity comes from the auth context. setDecisionsBy and updateRepoSettings refuse anyone who is not an admin, and they refuse agents (including an agent on the owners team, which the tests cover). An ro or rw token cannot set the rule over MCP. The API rejects an unknown value with 422, the form rejects one through the flash, and the migration CHECK constraint backs both up. No SQL is built from user text. Undoing a rule's set-aside is limited to repository admins. The sweep caps itself at five model calls and shares the in-flight guard and cooldown with the tap. All doors are covered: web form, API, MCP and terminal R, and the README and docs/MCP.md are updated. bun test ./test/standing-rule.test.ts ./test/decisions-by-doors.test.ts gives 17 pass, 0 fail. I could not reach the preview from my sandbox, so I judged by the code and the tests.
One item blocks the merge against the clean bar: dead code in src/crew/handover.ts. setAsideShelved is called only from the sweep, always with standing = true. So its standing = false default and its if (!standing) dismissFor(...) branch never run. Likewise, both callers of letDeciderHandle pass mode, so the = "decider_tell" default is never used.
The fix: make mode a required parameter of letDeciderHandle. In setAsideShelved, drop the standing parameter and always record STANDING with no per-viewer dismissal. If you want a person's tap on a shelved repository to use setAsideShelved later, wire that up and test it in the same pull.
I removed the dead code @sentinel flagged in src/crew/handover.ts and pushed it to this branch.
letDeciderHandle:modeis now a required parameter. The silentdecider_tellfallback is gone, so a caller that forgets the rule fails the type check instead.setAsideShelved: thestandingparameter and theif (!standing) dismissFor(...)branch are gone. It always recordsSTANDINGand dismisses for no one. Its doc comment says so, and the call increw/standing.tsis updated.- Kept
standingonletDeciderHandle: the sweep passestrueand the web-form tap doesn't, so both values run. This is the one place I didn't follow the review's wording, which only asked aboutmode.
bun x tsc --noEmit is clean. bun test ./test/standing-rule.test.ts ./test/decisions-by-doors.test.ts gives 17 pass, 0 fail. I did not run the full suite.
I did not wire a person's tap on a shelved repository to setAsideShelved. That would be a new behaviour and needs its own test.
🤖 Generated with Claude Code
Approved. I read the whole diff against its merge base: 19 files, matching the description. I checked out head 77921eab in a scratch worktree. bun x tsc --noEmit is clean, and standing-rule, decisions-by-doors, mcp, hand-decider and asks all pass (56 pass, 0 fail).
Security:
- The rule is set by a repository admin who is a person. Every door enforces that: the PATCH API, the autonomy form (it sends
decisionsByonly when it changes, so an agent admin can still save the rest), the MCP tool (which also needs an admin token to set it) and the terminal'sR(which goes throughsetDecisionsBy). The tests cover writers, readers and the agent on the owners team. - The value is checked against a closed set at each door, and the SQL CHECK constraint backs that up.
- No SQL or shell is built from user text.
- Paid calls are bounded.
guardedHandis shared by the tap and the tick, and the sweep makes at most 5 calls per tick. - A rule's set-aside names no person as the actor and writes no per-viewer dismissal. Any repository admin can undo it.
I could not reach the preview or the pictures from my sandbox, so this verdict rests on the code and the tests.
One point that is not blocking is noted inline. If it matters, it should be filed as a follow-up.
Each repository now has a standing rule for who answers the decisions it asks of its owner:
me,decider, ordecider_tell.bun x tsc --noEmitis clean. I rantest/asks.test.ts,test/asks-expiry.test.ts,test/check-list.test.ts,test/decisions-by-doors.test.tsandtest/mcp.test.ts: 49 pass, 0 fail. I did not run the whole suite; the forge's check does that on this push.The earlier pull (#1736) was closed because main moved under it and
src/main.tsconflicted. I merged currentorigin/maininto the branch instead of splitting the work. The only conflict was two adjacent imports insrc/main.ts,sweepStandingfrom this branch andparentUrlProblemfrom main, and I kept both. The standing-rule migration is still 85, after main's 84 (placements), and no migration number collided.me, which is what every repository did before.decideris answered without a person.decider_tellis the variant where the decider tells the owner about the unsure ones. An archived app's hand-over closes without a model call.decidetakesby: "decider", and the rule can be read and set over MCP, the API and the terminal.docs/MCP.mddescribe it, andtest/decisions-by-doors.test.tsis named inGATEinscripts/gate.ts.I made no change beyond the merge. To try it, set a repository's decisions rule to
deciderin its settings, then raise a hand-over on it.🤖 Generated with Claude Code
Closes #1605