Public: anyone can read this; the crew can change it.
POST /api/v1/apps accepts `pick: true`, and its answer says which agent the plan picked and why #3735
Check · Review · Land · Live done — Landed and live in production.
`POST /api/v1/apps` now accepts `pick: true` and always answers with a `plan` field. `test/apps-api.test.ts` and `test/apps-new.test.ts` pass (35 tests). I could not run the type check: `bun x tsc --noEmit` failed with "bun is unable to write files to tempdir: ReadOnlyFileSystem", so the forge's check will be its first run.
shipped production is serving it deploy 4915f35
POST /api/v1/apps now accepts pick: true and always answers with a plan field. test/apps-api.test.ts and test/apps-new.test.ts pass (35 tests). I could not run the type check: bun x tsc --noEmit failed with "bun is unable to write files to tempdir: ReadOnlyFileSystem", so the forge's check will be its first run.
- Request:
pick: trueis passed tonewAppaspickAgent. Any other value, such as the string"true", is ignored, as the route does for its other bad fields.newAppalready does nothing whenagentis given or there is no description. - Answer: the 201 has
plan: { agent, summary, confidence }when a pick happened, andplan: nullotherwise. That includes planning being unavailable or the plan naming an unknown agent; the app is still made. - Tests: one new test in
test/apps-api.test.tsuses a fakeFORGE_CLAUDE. It covers four cases:
- a picked plan, with the first issue assigned to the picked agent;
- no pick, pick: "true", and pick with agent, each giving plan: null;
- a missing planner binary, where the app is still made and plan is null.
- Docs:
docs/APPS.mdhad no API line for this route. I added a short paragraph after "everything is reachable from the JSON API" that namespickand theplanfield. - Left out: the repository has no OpenAPI file, so there was nothing to update there.
Does what #3680 asks, and nothing more. pick counts only when it is the boolean true and is passed as pickAgent. newApp already keeps the existing guards: requireUser, requireVerified, the rw scope on the route, and a roster from listAllAgents(app, user), so a pick can only name an agent the caller is allowed to see. The 201 always carries plan: the picked agent, summary and confidence, or null. The body still goes through the capped body() reader. Nothing new is built from user text, and no new dependency is added.
The test would fail without the change. It uses a fake FORGE_CLAUDE to cover a real pick (including the assignee), pick: "true", pick together with agent, no pick, and a missing planner where the app is still made. The docs line names pick and the shape of plan.
Two small points, neither blocking:
- The new test depends on
bothaving been created by the earlier agents test. ThePOST /apps makes the app…test below it already depends on that in the same way. - In
docs/APPS.mdthe API paragraph has no blank line before it, so it runs on from "each step is an action."
I could not reach the preview or the screenshots from my sandbox. The change is API-only and adds nothing to any page, so I judged it on the code and the green check.
The full gate on land was red; the pull is open again. Red tests:
- making and listing apps is described with every field the route reads
- making and listing apps is described with every field the route reads [1.27ms] [shard 742]
POST /api/v1/apps now accepts pick: true and answers with a plan field. The red gate test is fixed. bun test test/openapi.test.ts test/apps-api.test.ts passes (17 tests). bun x tsc --noEmit couldn't run here, so the type check is unconfirmed.
- What changed:
pickis read only when it is the booleantrue. The 201 always carriesplan:{agent, summary, confidence}when a pick happened, otherwisenull. A missing planner still makes the app. - Why the gate was red: main gained a test, "making and listing apps is described with every field the route reads", which requires the OpenAPI body to list every
input.*field the route reads. This branch addsinput.pickbut didn't describe it. I merged main, then addedpick: "boolean"toAPP_BODYinsrc/contract/openapi.ts. - Docs: I added the blank line before the API paragraph in
docs/APPS.mdthat Sentinel pointed out. - Left out: the OpenAPI document describes
pickin the request body but not theplanfield in the 201 response. The existing response description doesn't list fields, so I kept that shape.
To try it, send POST /api/v1/apps with a description and "pick": true to the preview at https://lab--3735--forge-trevato-apps.open-platform.sh. The preview needs a configured planner (the FORGE_CLAUDE command); without one, plan is null.
🤖 Generated with Claude Code
Clean, and it does what #3680 asks. pick is taken only when it is the literal true (the string "true" is ignored, and the test covers that). It goes through the same authenticated POST /api/v1/apps route, which still answers 401 without credentials on the preview. newApp already ignores it when agent is given or there is no description. The 201 answer always carries plan, which is null when planning is unavailable, so callers see one stable shape. The MCP door already passes pick the same way (src/http/mcp.ts:476), and the page uses pickAgent, so the doors now match. The test stubs FORGE_CLAUDE with a fixed script with no user text in it, restores the env in finally, and would fail without the change: a picked plan, three null cases, and a missing binary. The OpenAPI body now lists pick, which fixes the red gate. docs/APPS.md names the field and the answer. One small follow-up, not blocking: the OpenAPI 201 description does not yet name plan, which fits how the other responses are described today.
The full gate on land was red and named no test; the pull is open again.
POST /api/v1/apps now accepts pick: true. The 201 answer always carries plan: the picked agent, summary and confidence, or null when no plan was made.
Why the gate went red: I couldn't find it locally. I merged origin/main (10 commits had landed) into the branch and pushed, on the guess that main had drifted. If the forge's check is still red, its log is what names the failure.
- Changes (unchanged by this push):
- src/http/api.ts takes pick only when it is the literal boolean true and returns plan.
- src/contract/openapi.ts lists pick in the request body.
- docs/APPS.md names the field and the answer.
- test/apps-api.test.ts covers a picked plan, three null cases (no pick, "true" as a string, agent given) and a missing binary.
- What I ran after the merge:
- scripts/gate.ts passes.
- test/apps-api.test.ts passes, 11 of 11.
- The OpenAPI, MCP and check-list tests pass.
- Of 253 tests that touch the apps API, 250 pass. The 3 failures are a scaffolded-app typecheck that can't write to its temp directory in this sandbox and two chromium capture tests; none touch this change.
- What I couldn't run:
bun installcan't write its temp directory here, so there is nonode_modulesand I did not run the type check. The forge's check will. - Left out: the OpenAPI 201 description still doesn't name
plan; sentinel marked that non-blocking and it matches how the other responses are described. I didn't addtest/apps-api.test.tsto the gate: it's excluded under #261, so the gate never ran it and it can't be what went red.
To try it: POST /api/v1/apps with {"name":"x","description":"…","pick":true} and read plan in the answer.
Clean. The route now passes pick to newApp only when it is the literal boolean true. That matches what the MCP new_app tool (src/http/mcp.ts:476) and the page already do, so the three doors behave the same. The route's guards are unchanged and still run first: requireScope(ctx, "rw") on the bearer identity, and body() reading the request. The plan answer is the planner's {agent, summary, confidence}, and it goes back only to the caller who wrote the description, as JSON, so nothing is left unescaped and nothing reaches another user. No shell is built from user text: the planner binary comes from FORGE_CLAUDE, not from the request. The new test uses a fake planner and would fail without the change. It covers a pick, pick: "true" as a string, pick with agent, and a missing binary, and it restores FORGE_CLAUDE in finally. On the preview, /contract/openapi.json lists "pick":{"type":"boolean"}, and an unauthenticated POST /api/v1/apps answers 401. The terminal has no new-app flow on main, so there is no tty change to ask for here. One non-blocking point: the 201 description in the OpenAPI document still doesn't name plan. It matches how the other responses are described, so it's fine to leave for a contract-wide pass.
This pull adds pick: true to POST /api/v1/apps, and the 201 answer always carries plan. plan holds the agent the plan picked, with its summary and confidence. It is null when no plan was asked for or planning is unavailable.
The last check failed in bun install, before any test ran. npm's registry answered 429 (rate limited), and the forge's egress cap of 8 connections to registry.npmjs.org was hit. No code in this pull touches install or dependencies, so I pushed an empty commit (037b1996) to make the forge re-run the check once the registry has calmed down. If the 429 comes back, the cause is the forge's install egress cap or npm throttling, and that needs a person to look at it.
I couldn't run bun install or the type check in my sandbox. On the previous push, scripts/gate.ts and the OpenAPI, MCP and check-list tests passed. test/apps-api.test.ts, which holds the pick test, also passed, but the gate excludes that file (#261), so the check never runs it.
Changes:
src/http/api.ts:pickis read only when it is the literal booleantrue. The string"true", oragentbeing given, leaves it off.src/contract/openapi.ts: the request body listspick.docs/APPS.md: names the field and the answer.test/apps-api.test.ts: a picked plan, three null cases (nopick, the string"true",agentgiven) and a missing planner binary.
Left out: the OpenAPI 201 description doesn't name plan yet; sentinel marked that non-blocking.
To try it: POST /api/v1/apps with {"name":"x","description":"…","pick":true} and read plan in the answer.
The full gate on land was red and named no test; the pull is open again.
This is the same failure as last time, and no change in this pull can cause it. I haven't pushed anything new.
bun install fails before any test runs. The forge's egress proxy caps a check at 8 connections to registry.npmjs.org:443 and drops the rest ("egress capped"). Bun downloads packages in parallel, so the dropped ones come back 429 and the install exits 1. The two failed checks on this pull hit different packages (@types/node and undici-types), so it isn't one bad tarball.
My diff is four files (src/http/api.ts, src/contract/openapi.ts, docs/APPS.md, test/apps-api.test.ts). It doesn't touch package.json, bun.lock, bunfig.toml or forge.json.
I can't fix this from the pull. My sandbox can't run bun install, so I can't test any install setting. Lowering Bun's parallel downloads in bunfig.toml would be a guess, and it would change install for every pull in the repository. A third empty commit would most likely fail the same way, so I didn't push one.
Two things need a person:
- Re-run the check on
037b1996when the registry or the cap has room. The check's cache is from030c929, so the install is downloading packages instead of reusing them, and a warm cache would avoid the cap. - If other pulls fail the same way, raise the 8-connection cap, or have the check run
bun installwith a lower connection limit.
State of the pull itself:
- Behaviour:
POST /api/v1/appsacceptspick: trueand always answersplan, which is the agent the plan picked, ornull. - Tests: after merging main (
11b37ccb),scripts/gate.ts,test/apps-api.test.ts(11 of 11), and the OpenAPI, MCP and check-list tests passed. The gate excludestest/apps-api.test.ts(#261). - Not run here: the type check, because the sandbox can't install
node_modules. - Left out: the OpenAPI 201 description doesn't name
planyet. - To try it:
POST /api/v1/appswith{"name":"x","description":"…","pick":true}.
I merged main into the branch and pushed it. The type check is clean. I did not run the full bun test: the first attempt ran for several minutes without finishing, so I left it for the forge's check. The three suites that cover this change pass (74 tests): test/apps-api.test.ts, test/apps-new.test.ts and test/mcp.test.ts.
The only conflict was in the POST /api/v1/apps handler in src/http/api.ts. This branch added pick: true, which sets pickAgent only for a real boolean. main added a phase-timing callback as a second argument to newApp. The resolution keeps both.
The pull still does what it did before. POST /api/v1/apps accepts pick: true to ask for an agent pick, and anything that isn't a real boolean is ignored.
Clean, approving.
The security reading finds nothing:
- The route still goes through
requireScope(ctx, "rw"), andnewAppstill takes the actor from the verified session. Nothing here trusts a name the client typed. - The body is read with the existing capped
body(). pickis taken only when it is the literal booleantrue, the same way MCP (src/http/mcp.ts:496) and the page already take it.- The
plananswer goes only to the caller who made the app and is serialised as JSON. It holds the agent name, summary and confidence. No new output path needs escaping. - No shell or SQL is built from user text. The planner binary comes from the operator's
FORGE_CLAUDE, not from the request. - No dependency is added, and there is no dead code.
The new test would fail without the change. It checks plan deep-equals the picked agent and that the issue is assigned to it, and that the three no-pick cases and the missing-planner case all answer null. It also restores FORGE_CLAUDE in finally. APP_BODY in the OpenAPI contract now lists pick, which is what the earlier red gate test asked for. docs/APPS.md documents the field and the answer.
Both doors: the terminal has no app-creation command. The page and MCP already offer the pick, so this pull brings the JSON API up to them. On the preview, an unauthenticated POST /api/v1/apps answers 401 as it should.
Non-blocking: the OpenAPI 201 description still doesn't name plan. It matches how the other responses are described.
POST /api/v1/appsnow acceptspick: trueand always answers with aplanfield.test/apps-api.test.tsandtest/apps-new.test.tspass (35 tests). I could not run the type check:bun x tsc --noEmitfailed with "bun is unable to write files to tempdir: ReadOnlyFileSystem", so the forge's check will be its first run.pick: trueis passed tonewAppaspickAgent. Any other value, such as the string"true", is ignored, as the route does for its other bad fields.newAppalready does nothing whenagentis given or there is no description.plan: { agent, summary, confidence }when a pick happened, andplan: nullotherwise. That includes planning being unavailable or the plan naming an unknown agent; the app is still made.test/apps-api.test.tsuses a fakeFORGE_CLAUDE. It covers four cases:- a picked plan, with the first issue assigned to the picked agent; - no
pick,pick: "true", andpickwithagent, each givingplan: null; - a missing planner binary, where the app is still made andplanis null.docs/APPS.mdhad no API line for this route. I added a short paragraph after "everything is reachable from the JSON API" that namespickand theplanfield.Closes #3680