---
title: "Can Operations Staff Ask AI to Modify a Todo App? Provider Switching and Safe Stops"
description: "I built a dedicated web interface for requesting Todo app changes, normalized responses from Sakura AI Engine, Cloudflare Workers AI, and Google AI Studio, and tested whether a seven-file code proposal could stop after failing three of nine fixed tests without changing GitHub or production D1."
lang: "en"
canonical: "https://llm-lab.dev/en/posts/todo-cms-safe-local-code-agent/"
source: "https://llm-lab.dev/en/posts/todo-cms-safe-local-code-agent.md"
publishedAt: "2026-07-19"
updatedAt: "2026-07-19"
category: "AIDD"
tags:
  - "aidd"
  - "provider-adapter"
  - "workers-ai"
  - "sakura-ai-engine"
  - "non-engineer"
---

# Can Operations Staff Ask AI to Modify a Todo App? Provider Switching and Safe Stops

> [!NOTE]
> What this article confirms
>
> I requested a Todo app change from a dedicated web interface and made Sakura AI Engine, Cloudflare Workers AI, and Google AI Studio selectable for both requirements planning and code modification.
>
> All three live APIs produced requirements JSON that could be normalized into the same format. Sakura's seven-file code proposal, however, failed three of nine fixed tests. The runner returned `STOPPED` and wrote nothing to the GitHub repository or production D1.

## Connecting a Chat Request to a Draft PR in Stages

This is the first article in a series testing whether operations staff who do not work directly with Git, terminals, or AI development tools can request Todo app changes from a dedicated web interface.

The series goal is to let a user request a change in chat, answer AI clarification questions, edit and confirm structured requirements, then run code modification and tests in an isolated environment. Only changes that pass should become GitHub Draft PRs. At the end, I plan to run ten types of changes under the same conditions and compare the workflow with manually asking an existing coding agent to perform each task.

This first verification builds the entry point and the safety boundary. From the dedicated web interface, I switched among three AI providers, generated a code proposal from a natural-language request, and ran fixed tests in an isolated workspace. The flow does not yet include answering clarification questions in chat, confirming requirements, automatically repairing a failed change, or creating a GitHub Draft PR.

The experimental interface looks as though the difficult work is already solved.

Enter "I want priorities on my Todos. Make them easier to read," select Cloudflare Workers AI for requirements planning and Sakura AI Engine for code modification, then press a button. The interface seems ready to take care of both the GitHub code and D1 data.

The result panel did not show `READY`. It showed `STOPPED`.

![Todo app change interface for non-engineers with Sakura, Google, and Cloudflare provider selectors and a STOPPED result after fixed tests failed](/images/posts/todo-cms-safe-local-code-agent/01-provider-switch-ui.webp)

That `STOPPED` result is more trustworthy than marking a change `READY` after its tests fail. In this first article, the ability to stop a suspicious change before it leaves the isolated environment matters as much as the ability to generate code.

## Separating GitHub and D1 Write Permissions from the Web Interface

The user interacts only with the Todo app change interface. Behind it, the server handles AI authentication, repository snapshots, proposal validation, Git operations, and D1 operations.

```text
Natural-language request from a non-engineer
  ↓
Requirements-planning AI
  ↓
Common requirements JSON
  ↓
Human review
  ↓
  ├─ Code change -> isolated Git workspace -> fixed tests -> Draft PR
  └─ Data change -> typed operation -> preview -> D1
```

The interface sends the request text and selected AI provider IDs to the server. API keys remain on the server. The code-modification AI receives only the necessary source files; GitHub tokens and D1 credentials are excluded from the model input. Only a runner with an allowlist of paths and validation steps can write the returned proposal to real files.

## Normalizing Three Provider APIs into One Requirements JSON Format

The interface has separate selectors for the requirements-planning AI and the code-modification AI. Both selectors offer Sakura, Google, and Cloudflare.

The three options look equivalent in the UI, but their APIs differ. Sakura uses an OpenAI-compatible Chat Completions API, Cloudflare Workers AI uses an account-scoped REST API, and Google Gemini uses `generateContent`.

If the interface handled those differences directly, switching providers would also force branching throughout the downstream workflow. I instead used server-side adapters to transform requests and responses, then passed only `requirement-plan-v1` to the later stages. This common format contains the change path, current state, expected state, acceptance criteria, test plan, constraints, and unresolved questions.

```js
const result = await runRequirementProvider({
  providerId,
  rawRequest,
  confirmedFacts,
  configuration,
});

validateRequirementPlan(result.plan);
```

I sent the same request once to each live API and ran every returned requirements JSON object through the same validator.

| AI provider | Model | Result | Elapsed time | Calls |
|---|---|---:|---:|---:|
| Sakura AI Engine | `preview/Kimi-K2.6` | Passed common-format validation | 28.8 seconds | 1 |
| Cloudflare Workers AI | `@cf/zai-org/glm-5.2` | Passed common-format validation | 14.5 seconds | 1 |
| Google AI Studio | `gemini-2.5-flash` | Passed common-format validation | 6.5 seconds | 1 |

![Successful live API responses from Sakura, Cloudflare, and Google normalized into the same requirements JSON format](/images/posts/todo-cms-safe-local-code-agent/02-provider-evidence.webp)

One call per provider with different models is not enough to compare speed or quality. This verification covers connectivity. Normalizing the three live responses separated provider selection from downstream path validation and fixed tests. The stop conditions remain the same when the provider changes.

The [Sakura AI Engine product page](https://ai.sakura.ad.jp/sakura-ai/ai-engine/) describes OpenAI-compatible and Anthropic-compatible APIs and a free allowance of 3,000 Chat Completions per month. The [Cloudflare Workers AI pricing page](https://developers.cloudflare.com/workers-ai/platform/pricing/) describes a daily free allocation of 10,000 Neurons. Free-tier limits can change, so current limits should be checked against the official pages.

## Limiting Google AI Studio Usage and the Data Sent to It

The Google Gemini adapter already existed, yet the first version of the interface displayed it as `Not configured`. The API key was present. The code was looking for a configuration file and variable name that did not match their actual location.

After correcting the configuration lookup, I limited the `gemini-2.5-flash` input to a fictional request to add priorities to a Todo app and the information required to organize its requirements. The single call completed in 6.5 seconds, classified the change path as `github_change`, and returned requirements JSON that passed the common-format validator.

The Google AI Studio usage screen I checked showed limits of 5 RPM, 250K TPM, and 20 RPD for Gemini 2.5 Flash. To avoid exhausting a small free allowance during the experiment, the runner records call history and rejects requests that would exceed the limit before sending them to the API.

The [Gemini API Additional Terms](https://ai.google.dev/gemini-api/terms) state that content sent to unpaid services may be used to improve Google products and may be processed by human reviewers. Whether an API works on a free tier and whether private repository content should be sent to it are separate decisions. That is why this verification used only a fictional request.

## Tracing a Workers AI 401 from a Known Successful Path

Cloudflare authentication failed in two different places.

Calling the Workers AI REST API with a token from the existing `.env` returned HTTP 401 and error code 10000 three times. The same verification directory, however, contained evidence of an earlier successful call to GLM-5.2 through a Workers AI binding. That evidence shifted the investigation from the API and model toward the difference between the two call paths.

The comparison showed that the failed token had permissions for another purpose, while the successful path used Wrangler OAuth or a remote Workers AI binding. I switched to OAuth, but `wrangler auth token --json` then crashed before returning a token. Wrangler's shebang selected Homebrew Node.js 25, which attempted to load the missing `libsimdjson.29.dylib`.

I placed the Node.js 22 directory at the start of `PATH` and loaded the OAuth token only into process memory. On the next run, Cloudflare Workers AI succeeded on the first call in 14.5 seconds.

```js
env: {
  ...process.env,
  PATH: `${dirname(process.execPath)}:${process.env.PATH}`,
}
```

The 401 response alone did not expose the Node.js 25 problem. Comparing it with a previously successful path allowed token permissions and Node.js version selection to be investigated separately. Small successful experiments become useful evidence when a later call fails.

## Running Sakura's Seven-File Proposal Through Fixed Tests

Stopping after the requirements JSON would verify only requirements planning. To reach the Todo app code, I called Sakura AI Engine's `Qwen3-Coder-30B-A3B-Instruct` once and asked it to add `low`, `medium`, and `high` priorities.

After 31.8 seconds, the model returned a seven-file proposal covering a migration, repository layer, input validation, UI, and tests. The call used 6,885 input tokens and 5,341 output tokens, for a total of 12,226 tokens.

Every proposed path was inside the allowlist, and the build succeeded. An evaluator outside the model also confirmed that Todos created before the priority migration became `medium`.

So far, the proposal looked healthy. The fixed test run told a different story: six of nine tests passed and three failed.

1. A baseline test still expected the old schema without a priority column after all migrations had run.
2. A priority test tried to read a nonexistent row without first creating the Todo it intended to inspect.
3. An input-validation test still expected the old return value without the newly added default priority.

The priority implementation was present, but the generated tests did not match the post-change contract. The runner stopped at this point and created no local branch, commit, or Draft PR data. The `STOPPED` result at the beginning came from these three failures.

Marking the proposal `READY` with three of nine tests failing would defeat the purpose of the dedicated interface. This first verification does not automatically return the failures to the model for repair. It records where the initial proposal can be stopped.

## Removing a Hard-Coded Migration Name from the Evaluator

The validation side also contained a bug. An external evaluator separate from the fixed tests assumed that the added migration would be named `002_add_todo_priority.sql`. The model returned `002_add_priority_to_todos.sql`, so the evaluator failed with `file not found` before reading the file.

The behavior to verify is not the filename. It is whether existing Todos become `medium` after applying migration number 002. I changed the evaluator to assert that exactly one `002_*.sql` file exists and apply that file. The existing-data check then passed.

Strict validation can still reject a correct change if the assertion does not match the behavior it is meant to guarantee. The migration order and its result were the properties that needed to remain fixed.

## Separating GitHub Code Changes from D1 Data Changes

The request "change a Todo" contains two distinct types of operations.

Adding a priority field changes the database schema and migration as well as the API, UI, and tests, so it follows the GitHub code-change path. Marking an existing Todo as complete requires only a D1 data update.

For D1 updates, I allowed two operations: `todo.set_status` and `todo.set_title`. The server validates the argument types, returns a preview, and executes a prepared statement only after human confirmation. Contract tests reject raw SQL or arbitrary queries generated by the model.

This path connected only to local SQLite. It did not update production D1. On the GitHub side, it stopped after producing a local package following successful validation; no remote branch or Draft PR was created.

The interface displays "Draft PR" and "D1," but this verification stops one step before writing to either external system. The question was whether an incomplete AI proposal could remain isolated from GitHub and production D1.

## Evaluating Provider Switching and Safe Stops

The dedicated web interface successfully switched among live APIs from Sakura, Cloudflare, and Google and received requirements JSON in one common format. The path allowlist, fixed tests, and GitHub and D1 permission boundaries remained unchanged across providers.

For code modification, Sakura returned a seven-file proposal spanning the migration through the UI. It passed the build and the existing-data migration check, but three fixed tests still failed, so the interface displayed `STOPPED`. The GitHub repository and production D1 remained unchanged.

The decision for this first article is **Conditional Go**. The provider adapters and the boundary that blocks external writes after failed tests worked. The non-engineer experience of confirming requirements in chat and completing an actual application change remains unverified.

One problem remains before that workflow can proceed: who turns an ambiguous phrase such as "make it easier to read" into a confirmed requirement, and where does that decision happen? The second article adds clarification questions to the interface and starts a change job only after the user edits and confirms the requirements.
