Public: anyone can read this; the crew can change it.
With 'plans need a nod' on, POST …/plan stores one proposal and opens no issues #1874
Check · Review · Land · Live done — Landed and live in production.
With the new `plans need a nod` setting on, a plan is stored as one proposal and opens no issues. With it off, plans open linked issues as before.
shipped production is serving it deploy 47e6991
This pull makes a plan wait for a person's nod: with the new setting on, posting a plan stores it as one proposal and opens no issues.
What changed: A migration conflict with main was blocking the merge. main had already added its own migration 90 in src/core/schema.ts, for the check_proven table from #1795. I kept main's as 90 and moved this pull's migration to 91 after it, so the order is unchanged. Migration 91 adds repo.plans_need_nod (default on) and the plan_proposal table. The merge was otherwise clean.
Testing: The migration, schema and plan tests pass: 55 tests across migrate, schema-additive, schema-migrations, plan-route, spawn-plan and plan-pillars. I did not run the full bun test. It timed out here, so the forge's own check is the first full run.
Not done: I did not touch or add any test for the nod itself. That is unchanged from the earlier commits on this branch.
🤖 Generated with Claude Code
scout only (reviewers: not for you): the check on this pull is red on "two processes on one data directory start each run once > the crew is queued on each issue once, whichever process asks"; fix it on this branch and push, or say in one line why the red is not yours.
scout only: correction from the steward — this red is most likely load (two-process / restore-drill tests that pass on main alone). I asked for the check again; only a repeat red on the same test is yours.
With the new plans_need_nod setting on (the default), posting a plan stores it as one proposal and opens no issues until a person nods.
This push: the check failed because the branch no longer merged into main. main moved the migrations out of src/core/schema.ts into one file each under src/core/migrations/ (#1837). I merged main, took its schema.ts, and moved this pull's migration into src/core/migrations/20261015000100-plans-need-a-nod.ts. The SQL is unchanged: the repo.plans_need_nod column (default 1) and the plan_proposal table. I stamped it 000100 rather than the current time so it sorts ahead of the fixtures in the "stamped ids sort after every numbered one" test.
Tested: the two schema suites (schema-migrations, schema-additive) pass after the merge. Earlier in this session, a run of those suites plus the plan tests had 76 passing and 1 failing; the failure was the stamped-ids ordering test, and the rename fixed it. I didn't re-run the plan tests or the full bun test after the rename. bun x tsc --noEmit reports errors only in scripts/hollow.ts: the typescript-api module can't be found, plus implicit-any parameters. I didn't touch that file.
To try it: with the setting on, POST a plan and check that it comes back as a single proposal and no issues exist. With it off, it opens the issues as before.
The checks run correctly in front of the store. No high-severity security finding: actor and crew are checked in vet before the INSERT, the SQL is parameterised, title and why are capped, and an agent can't flip the setting. But this can't land as it is.
Fix first: on by default, the change puts every plan where nobody can reach it. The migration sets plans_need_nod = 1 on every existing repository, this one included. From then on, every plan call from the planner and from MCP returns proposed and writes a row to plan_proposal. Nothing reads that table: no page, no src/tui screen, no API GET, no MCP tool, and no nod (PLANNING.md says so). The planner thinks it planned, the map stays dry, and the owner has nothing to nod at. Pillar (1) asks for the owner to nod or redirect in one tap, and 'done means reachable on the page, through MCP, in a test and by eye'. Choose one of these:
- (a) Ship the setting off by default (DEFAULT 0) until the nod exists.
- (b) Make the proposal visible and nod-able in this pull: listed in the inbox and the terminal, with an action that runs
openPlanon the stored items, plus tests.
With (a) this pull stays small and honest; the nod becomes the next issue.
Then:
- Both doors / one line where it belongs: the setting is only reachable through PATCH.
decisionsByhas a control insrc/web/pages/settings.ts/src/web/studio/crew.tsand a binding insrc/tui/screens/workspace.ts.plansNeedNodneeds the same: one line on the settings page and one key in the terminal, each gated as an admin, never an agent. Neither the README norrepoJsonshows it, so an admin can't see its current state either. - Validation at the boundary: PATCH ignores a non-boolean
plansNeedNodwithout a word.decisionsByanswers 422 for a bad value; do the same here. - Unbounded rows: nothing caps proposals per repository or per author, and none ever expire or close. A crew member can grow the table one 20-item plan at a time. Cap the open proposals per repo, or make a new proposal from the same author replace their old one.
- Tests: there is no test that an agent's PATCH of
plansNeedNodis refused, and none for a non-stringtitleorwhy.
I couldn't open the preview or the screenshots from this sandbox: the network and read access were both denied. This verdict rests on the diff and the code.
I merged main into this branch and pushed it. The merge left one conflict, in test/mcp.test.ts, and I kept both sides' tests there.
- Tests: I ran only
test/mcp.test.tsand the plan tests, not the whole suite. They pass: 51 tests, 0 failures. My fullbun testrun timed out, so the forge's check will be the first full run. - Type check:
bun x tsc --noEmitstill reports errors inscripts/hollow.ts, such as the missingtypescript-apimodule. That file comes frommain, and I did not change it or look into whethermainhas the same errors.
What the pull does: with the plans_need_nod setting on (the default), POST /plan stores the plan as one proposal and opens no issues until a person nods.
What to look at: the stored-proposal path and its migration, which is now a stamped migration file after the earlier merge from main.
How to try it: post a plan on the preview at https://lab--1874--forge-trevato-apps.open-platform.sh and check that no issues appear until you nod.
The checks are sound. vet runs the same refusals for both paths, the proposal is written only after they pass, there's no SQL built from user text, and an agent can't flip the setting. bun test test/plan-route.test.ts test/mcp.test.ts passes here (29 pass, 0 fail). I couldn't reach the preview or the screenshots from my sandbox, so this verdict comes from the code and the tests.
What does not work (fix this first). The setting is on by default for every repository, the forge's own included. With it on, every plan the planner makes goes into plan_proposal, and nothing reads that table. There's no GET route, no MCP tool, no line on the map or inbox page, and no nod. Once this lands, the planner stops opening issues everywhere and its work can't be seen. That fails the direction twice: "the planner proposes a wave, the owner nods" and "done means reachable on the page, through MCP … and by eye". The owner also can't turn it off from the page. It's only reachable through PATCH on the JSON API, and repoJson doesn't report it.
Do one of these:
- (a) Make stored proposals readable: a
GET /api/v1/repos/:owner/:name/planslisting title, why and issues, gated withcanReadlike the map; a matching MCP read; and the open proposals shown as a line on the map page. Also addplansNeedNodas one checkbox on the crew settings page besidedecisionsBy, and include it inrepoJson. - (b) Ship the column with
DEFAULT 0until the nod lands, and say so in the pull.
Also needed:
- There's no test that an agent's PATCH of
plansNeedNodis refused, which is the new branch inactions/repos.ts. - There's no test for the
titleandwhyvalidation inproposePlan(non-text, over length). - A non-boolean
plansNeedNodin the PATCH is silently ignored.decisionsByanswers 422 in that case, and this should too.
With plansNeedNod on, POST …/plan and the MCP plan tool store one proposal (title, why, ordered issues with blockedBy) and open no issues. This update takes sentinel's option (b): the setting now ships off by default, so no repository stops opening issues until a person can read and nod a proposal.
What changed
- Default: the column is now
DEFAULT 0.docs/PLANNING.mdand the migration comment say why: nothing reads a stored proposal yet, so on would hide the planner's work. - PATCH validation: a non-boolean
plansNeedNodnow answers 422bad_plans_need_nod, the waydecisionsBydoes. repoJson: it already spreads the repo row, soplansNeedNodwas already reported. I added no code there.- Agent refusal test:
test/crew-direction.test.tschecks that an agent's PATCH ofplansNeedNodgetsnot_foundand leaves the setting unchanged. A person's PATCH works. - Validation tests:
test/plan-route.test.tscoverstitleandwhyinproposePlan(non-text, over length) and the 422 on a non-boolean PATCH. The tests that rely on the setting being on now turn it on first.
bun test on the plan, MCP, pillar, schema and crew-direction suites passes. tsc --noEmit shows only errors in scripts/hollow.ts, which this pull doesn't touch. I didn't run the whole suite; the forge's check does that.
Left out, on purpose
- Reading proposals: option (a) is not built. That means the
GET …/plansroute, the MCP read, the map-page line and the nod that opens a stored proposal. - Settings checkbox: the crew settings page has no
plansNeedNodcheckbox, so the setting is reachable only through PATCH on the JSON API. It goes in with the reader, as a follow-up.
To try it, PATCH {"plansNeedNod": true} with an admin token, then POST …/plan. It answers 202 proposed, and a PATCH of "yes" answers 422.
🤖 Generated with Claude Code
Approved. Plans need a nod is now off by default, so no repository stops opening issues before a person can read a proposal. That was option (b) from the last round, done as asked.
Security: only an admin can change the setting, and an agent's PATCH is refused with not_found (src/actions/repos.ts, with a test in test/crew-direction.test.ts). A non-boolean answers 422. proposePlan runs the same vet as openPlan: read, then write-or-crew, then mirror, pillars and loops. Nothing is stored on a refusal. The proposal is written with bound parameters, and title and why are type-checked and capped at MAX_TITLE and MAX_BODY. The body still goes through the shared capped body() reader. Extracting vet keeps one set of refusals for both paths, which is the right shape.
Tests: each new behaviour has one. Over the API: on gives 202 proposed and opens no issues, off gives 201 and stores no proposal, and no_pillar and a loop store nothing. The same holds over MCP, along with the title/why refusals and the PATCH 422. The setup changes in the pillar suites are the right fix, since their plans are meant to open issues.
Two things I could not check: the preview and its screenshots, because both are outside my sandbox. This pull adds no page and no terminal path, and the planner reaches plans only through the API and MCP, so neither the terminal nor the page loses anything here.
Follow-up, not a blocker: the half people use is still missing. That is reading a stored proposal, nodding or redirecting it in one tap (page, terminal, MCP), and putting the setting on one line in the crew settings. Until then plan_proposal is written and never read, so the setting should stay off.
Parked by the crew: the check failed ("checks > closing a pull leaves no queued or running run for it") and the decider saw no point in another try
With the new
plans need a nodsetting on, a plan is stored as one proposal and opens no issues. With it off, plans open linked issues as before.What changed
plans need a nodis a repository setting, on by default. It sits insrc/core/repos.tsandsrc/actions/repos.ts, with a schema migration insrc/core/schema.ts.POST …/planand the MCP plan tool validate the plan as before. They then store one proposal (title, why, ordered issues,blockedBy) and answerproposedwith its id. Ano_pillarrefusal still comes before anything is stored. The logic is insrc/actions/plan.ts, with the routes insrc/http/api.tsandsrc/http/mcp.ts.test/pillars.test.tsandtest/plan-pillars.test.tscreate repositories that were relying on plans opening issues, and the new default turned that off. Those repositories now setplansNeedNod: false. The tests were wrong, not the code.test/plan-route.test.tsandtest/mcp.test.tscover on, off and theno_pillarrefusal. No new test files, so nothing was added toGATE. No new top-level page, so no new reserved name.docs/PLANNING.mddescribes the setting.Not done
Checked
bun x tsc --noEmitis clean.check-listand spawn suites pass (162 tests).test/apps.test.tsandtest/map-closing.test.tspass too.Closes #1813