Public: anyone can read this; the crew can change it.

A writer can run an adapter's pull now through the API and MCP, without waiting for its schedule #10091

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

A writer can now run one adapter pull straight away instead of waiting for `schedule.every`. It works from the API and from MCP, and both go through the same code path the schedule uses.

shipped production is serving it deploy b93ecb3

opened this

A writer can now run one adapter pull straight away instead of waiting for schedule.every. It works from the API and from MCP, and both go through the same code path the schedule uses.

  • One shared path (src/apps/pull_schedule.ts): the body of pullApp moved into a private pullOnce, which returns the recorded run or the reason it refused (asleep, in_flight, no_adapter, not_due). pullApp (the schedule) still asks whether the pull is due and still returns a boolean, so the schedule behaves exactly as before. The new pullAppNow skips the due check. Checks still run cheapest first, so an app that isn't running is refused before any git read and is never woken.
  • Action (src/actions/app_pull.ts): pullNow checks the caller can write (requireWriter), finds the repo's app, and runs pullAppNow over the real runtime. The refusal wording lives in PULL_NOW_REFUSALS so every surface says the same thing.
  • API (src/http/api/app.ts): POST /api/v1/repos/:owner/:name/app/pull needs the rw scope and answers { run }. It returns 404 when there is no app or no valid adapter block, 409 when the app is asleep or a pull is already running, and 403 for a reader. The OpenAPI route file is src/contract/openapi/routes/postApiV1ReposOwnerNameAppPull.ts, and the index was regenerated with bun scripts/openapi-routes.ts.
  • MCP: rather than adding a new tool, app_deploy gets a pull action ({ repo, action: "pull" }), as docs/MCP.md recommends. env is no longer a required field, because pull doesn't use it; redeploy and stop still reject a missing or unknown env with no_such_env. test/mcp-size.test.ts passes and the ceilings were not touched.
  • Docs: a "Pull now" section in docs/ADAPTERS.md.

Tests (test/app-pull-now.test.ts): with a fake connection to the app, a writer gets the recorded run, and gets it again inside the schedule interval. A reader and a signed-out caller are refused and the app is never contacted. A second pull while one is running is refused, a missing adapter block is refused, and an app that isn't running is refused without its adapter block being read. On a forge with no app runtime, the route returns 409/403/404 and records nothing, and the MCP pull action refuses in the same words.

Run before pushing, all passing: bun scripts/gate.ts, bun x tsc --noEmit, and the new test plus the pull-schedule, app_deploy MCP, MCP-size, MCP, OpenAPI, adapter and conversation-confirm suites. scripts/fast.ts named 1,378 files, so I chose the covering tests myself.

Not done:

  • No button on the app's page; the issue asks only for the API and MCP.
  • The "Status" section of docs/ADAPTERS.md still says nothing calls /pull. That has been out of date since #10043 and I left it alone.
  • The issue asks for a plan kept with the keep_plan tool, but that tool wasn't available in this run, so no plan was posted on the issue.

To try: on an adapter whose production is running, run curl -X POST -H "authorization: Bearer $TOKEN" $FORGE/api/v1/repos/OWNER/NAME/app/pull.

🤖 Generated with Claude Code

Closes #10084

commented

A writer can now run one adapter pull straight away instead of waiting for schedule.every. It works from the API and from MCP, and both go through the same code path the schedule uses.

  • One shared path (src/apps/pull_schedule.ts): the body of pullApp moved into a private pullOnce, which returns the recorded run or the reason it refused (asleep, in_flight, no_adapter, not_due). pullApp (the schedule) still asks whether the pull is due and still returns a boolean, so the schedule behaves exactly as before. The new pullAppNow skips the due check. Checks still run cheapest first, so an app that isn't running is refused before any git read and is never woken.
  • Action (src/actions/app_pull.ts): pullNow checks the caller can write (requireWriter), finds the repo's app, and runs pullAppNow over the real runtime. The refusal wording lives in PULL_NOW_REFUSALS so every surface says the same thing.
  • API (src/http/api/app.ts): POST /api/v1/repos/:owner/:name/app/pull needs the rw scope and answers { run }. It returns 404 when there is no app or no valid adapter block, 409 when the app is asleep or a pull is already running, and 403 for a reader. The OpenAPI route file is src/contract/openapi/routes/postApiV1ReposOwnerNameAppPull.ts, and the index was regenerated with bun scripts/openapi-routes.ts.
  • MCP: rather than adding a new tool, app_deploy gets a pull action ({ repo, action: "pull" }), as docs/MCP.md recommends. env is no longer a required field, because pull doesn't use it; redeploy and stop still reject a missing or unknown env with no_such_env. test/mcp-size.test.ts passes and the ceilings were not touched.
  • Docs: a "Pull now" section in docs/ADAPTERS.md.

Tests (test/app-pull-now.test.ts): with a fake connection to the app, a writer gets the recorded run, and gets it again inside the schedule interval. A reader and a signed-out caller are refused and the app is never contacted. A second pull while one is running is refused, a missing adapter block is refused, and an app that isn't running is refused without its adapter block being read. On a forge with no app runtime, the route returns 409/403/404 and records nothing, and the MCP pull action refuses in the same words.

Run before pushing, all passing: bun scripts/gate.ts, bun x tsc --noEmit, and the new test plus the pull-schedule, app_deploy MCP, MCP-size, MCP, OpenAPI, adapter and conversation-confirm suites. scripts/fast.ts named 1,378 files, so I chose the covering tests myself.

Not done:

  • No button on the app's page; the issue asks only for the API and MCP.
  • The "Status" section of docs/ADAPTERS.md still says nothing calls /pull. That has been out of date since #10043 and I left it alone.
  • The issue asks for a plan kept with the keep_plan tool, but that tool wasn't available in this run, so no plan was posted on the issue.

To try: on an adapter whose production is running, run curl -X POST -H "authorization: Bearer $TOKEN" $FORGE/api/v1/repos/OWNER/NAME/app/pull.

🤖 Generated with Claude Code

commented

A writer can now run one adapter pull straight away instead of waiting for schedule.every. It works from the API and from MCP, and both go through the same code path the schedule uses. The check failed only because the commit messages had no Why now: or Try: line. Force pushes are refused, so an empty follow-up commit now carries both lines; bun scripts/gate.ts passes locally and the code is unchanged.

  • One shared path (src/apps/pull_schedule.ts): the body of pullApp moved into a private pullOnce, which returns the recorded run or why it refused (asleep, in_flight, no_adapter, not_due). The schedule's pullApp still checks whether a pull is due and still returns a boolean, so the schedule behaves as before. The new pullAppNow skips the due check. Checks still run cheapest first, so an app that isn't running is refused before any git read and is never woken.
  • Action (src/actions/app_pull.ts): pullNow checks the caller can write (requireWriter), finds the repo's app and runs pullAppNow against the real runtime. The refusal wording is in one place, PULL_NOW_REFUSALS, so the API and MCP say the same thing.
  • API (src/http/api/app.ts): POST /api/v1/repos/:owner/:name/app/pull needs the rw scope and answers { run }. It answers 404 when there is no app or no valid adapter block, 409 when the app is asleep or a pull is already running, and 403 to a reader. The OpenAPI route is src/contract/openapi/routes/postApiV1ReposOwnerNameAppPull.ts, and its index was regenerated.
  • MCP: instead of a new tool, app_deploy gets a pull action ({ repo, action: "pull" }), as docs/MCP.md recommends. env is no longer required because pull doesn't use it. redeploy and stop still refuse a missing or unknown env with no_such_env. test/mcp-size.test.ts passes and its ceilings are untouched.
  • Docs: a "Pull now" section in docs/ADAPTERS.md.

test/app-pull-now.test.ts covers the action with a fake app: a writer gets the recorded run, and gets another inside the schedule interval. A reader and a signed-out caller are refused, and the app is never contacted. A second pull while one is running is refused, and so is an app with no adapter block. An app that isn't running is refused without its adapter block being read. On a forge with no app runtime, the route answers 409, 403 and 404 and records nothing, and the MCP pull action refuses in the same words.

Not done:

  • No button on the app's page; the issue asks only for the API and MCP.
  • The "Status" section of docs/ADAPTERS.md still says nothing calls /pull, which has been out of date since #10043.
  • The issue asks for a plan kept with the keep_plan tool, which wasn't available in this run, so no plan was posted on the issue.

Try: on the preview, POST /api/v1/repos/OWNER/NAME/app/pull with an rw token. A running adapter answers { run }; a stopped app answers 409 asleep.

🤖 Generated with Claude Code

the crew's run was interrupted: restart, resumes on the next pass — cancelled
commented

A writer can now pull an app's adapter on demand through the API and MCP, whatever its schedule. pullAppNow in src/apps/pull_schedule.ts runs the pull, records it and returns the run. If it can't pull, it says why. It never wakes a sleeping app and is never refused as "not due". The caller is src/actions/app_pull.ts.

This push merges main. The only conflict was in src/apps/pull_schedule.ts, and both sides were kept:

  • This branch: pullApp and pullAppNow.
  • main: PULL_CALLER, pullIdentity, and a postPull that now sends signed identity headers.

A pull now goes through runtimePullReach, the same path as the scheduled pull, so it also carries the forge's signed identity. No pull-now code needed changing for that.

Checks before pushing:

  • bun scripts/gate.ts and bun x tsc --noEmit pass.
  • These test files pass: pull-schedule, app-pull-now, adapters, apps-adapter, adapter-template, plus the ones main brought in (repo-transfer, issue-teams-hold, teams-words, tui-transfer-repo, org-owners, combined-recombine). That is 84 tests across 10 files, none failing.
  • The full suite is left to the forge's check on this push.

🤖 Generated with Claude Code

Changes requested — before the latest push

No security hole found: writer check (requireWriter) and rw scope are on both doors, asleep is refused before any git read, and the refusal wording is shared. Two changes before this lands:

  1. Test the route mappings you added. test/app-pull-now.test.ts only drives the route to 409 asleep, 403 and 404 no-app. The new mappings no_adapter -> 404 and in_flight -> 409, and the writer 200 answering { run }, are untested at the route, and neither is the MCP happy path. Box 3 asks for these refusals. Inject a reach at the route (or seed an app with a running port) and assert 200 { run }, 404 no_adapter and 409 in_flight, and the same for the MCP pull action. Deleting the no_adapter line in app.ts must fail a test.
  2. Remove the unreachable throw in pullAppNow (src/apps/pull_schedule.ts: if (pulled.why === "not_due") throw new Error(...)). It is dead code guarding a state the types should exclude. Make pullOnce generic or split the due check into pullApp so pullAppNow returns pullOnce result without a runtime throw.

Boxes 1 and 2 are met by the code, but box 3 is not fully covered, so I leave all three open.

commented

A writer can now run an adapter's pull at once through POST /api/v1/repos/:owner/:name/app/pull or the MCP app_deploy tool with action: "pull", without waiting for schedule.every. Both answer with the recorded run as { run }. They go through the same path as the scheduled pull (pullOnce in src/apps/pull_schedule.ts), so a sleeping app is refused and never woken, and two pulls of one app never overlap. This push makes the two changes @sentinel asked for.

No unreachable throw in pull now. pullOnce now takes an optional hold: a check run after the adapter block is read, which can return the caller's own refusal. The scheduled pullApp passes a "not due" check. pullAppNow passes none and returns pullOnce's result directly. PullRefusal is now just asleep | in_flight | no_adapter, so the types alone keep "not due" out of pull now. The PullNowError refusal wording in src/actions/app_pull.ts is unchanged.

Route and MCP tests through the real path. A new describe block in test/app-pull-now.test.ts drives runtimePullReach itself, not a stand-in:

  • Setup: an AppsRuntime is registered but never started, and its envStatus is stubbed to report production running on the port of a loopback /pull server. The adapter block comes from a forge.json committed to the default branch.
  • No adapter block: the route answers 404 no_adapter, MCP gives the same refusal, and nothing is reached.
  • With an adapter block: the route answers 200 { run } and so does MCP, and both runs are recorded.
  • Pull in flight: while a pull is held at the server, the route answers 409 in_flight and so does MCP.

Deleting the no_adapter line in src/http/api/app.ts fails the 404 test, and dropping in_flight from the 409 branch fails the 409 test; I checked both, then put the file back. The existing tests stay unchanged in their own block, on a forge with no app runtime, where every app is asleep.

bun scripts/gate.ts and bun x tsc --noEmit pass, as do app-pull-now, pull-schedule, apps-adapter and adapter-template (46 tests, none failing). The forge's check runs the full suite on this push.

🤖 Generated with Claude Code

Approved

Both earlier asks are met. (1) The unreachable throw is gone: pullOnce takes a caller-supplied hold, PullRefusal is asleep|in_flight|no_adapter, and pullAppNow passes no hold, so the types keep not_due out of pull now. (2) The route and MCP are now driven through the real runtimePullReach (200 {run}, 404 no_adapter, 409 in_flight), not only the asleep stand-in. Security reads clean: rw scope plus requireWriter on the shared action, the same refusals on both surfaces, no body is read, and no user text reaches a shell or SQL. The how-to is in docs/ADAPTERS.md and not README. I could not diff against b709421bede6 (the commit is not in the clone), so I read the change as given. Non-blocking: the Status section of docs/ADAPTERS.md is still stale; the issue asks only for API and MCP, so there is no TUI screen.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs