Mert Özen Further With Every Line
Artificial Intelligence GitHub Copilot — #05

Multi-Step Work with the IDE Agent: Tool Approvals, Terminal Commands, Checkpoints and Adding a Feature to Bookstore End to End

Mert Özen Nov 15, 2026 11 min 5 views
Multi-Step Work with the IDE Agent: Tool Approvals, Terminal Commands, Checkpoints and Adding a Feature to Bookstore End to End

An agent's 'all tests pass' summary does not say whether the test or the code changed: in Bookstore's institutional minimum feature we turned the same red test green once by changing the test and once by changing the code, both passed 14/14, and only git diff --stat showed the difference. Tool approvals, terminal rules, checkpoint limits, API, migration and Angular.

Version note: The code in this post was built and run with .NET 10, Entity Framework Core 10.0.15, xUnit.net v3 4.0.3, Angular 22.2 (TypeScript 6.0) and Vitest 5.1; the tests ran against SQL Server 2025. The agent's tool approvals, terminal rules and checkpoint behavior are taken from the Visual Studio 2026 (18.12) and Visual Studio Code (VS Code) 1.146 documentation as of 15 November 2026. The Copilot agent was not run for this post: we added the feature by hand, following the steps an agent would take, and show the real output of every step.

Guess First

You told the agent to add the "at least 20 books" rule for institutional orders to Bookstore, the online bookstore used as the shared example across this site's series, and turned on the mode that approves every command automatically. The agent wrote the tests, wrote the code and ran the tests: one was red. A few seconds later it made another change and ran the tests again: all green. Its summary says "all tests pass".

What is your guess? Was the change that turned the red test green in the code or in the test? Can you tell from the summary? What would you look at in the diff to tell? We will turn the same red test green in two different ways and put them side by side.

The Agent's Loop

Unlike chat, the agent in the IDE is a loop that runs until it has a result: it takes the goal, searches for and reads the relevant files, makes a plan, edits files, runs build and tests in the terminal, reads the output, fixes the error, and keeps going until it reaches the goal or needs to ask you something. The Visual Studio documentation defines agent mode exactly like this: ask mode stops after one answer, agent mode does not. The instructions from the fourth post (including the AGENTS.md rule "every behavior change comes with a test; never weaken an existing assertion") are in the context at every step of this loop.

For this post's feature we would give the agent the task like this. It follows the logic of the new-endpoint prompt file from the fourth post: test first, then code, do not touch existing tests, do not apply the migration:

Agent mode.
Add the institutional order minimum to Bookstore:
an order placed with an InstitutionCode needs at least 20 books in total across all lines.

1. Add integration tests first: 20 books across two lines is placed, 5 books is 400 with a ProblemDetails
   message, an unknown code is 400. Run them and show that they fail.
2. Ordering must not read the Institutional tables; add IInstitutionService to the Institutional module.
3. Add Order.InstitutionId with a migration. Do not run dotnet ef database update.
4. Show the hint and disable checkout in the Angular cart below 20 books; add a Vitest spec.
5. Run dotnet test and npm test. Never change an existing assertion; if a test looks wrong, stop and ask.

Approvals: What to Run Automatically

Besides editing files, the agent runs terminal commands and calls MCP tools. Whether it asks for approval for each of these is the main setting between speed and safety.

VS Code's Copilot harness has three permission levels: Manual permissions (the default; your tool, URL and terminal approval settings apply), Assisted permissions (a model judges every tool call; the documentation says it can make mistakes) and Allow all (runs without approval). Autopilot is not a permission level but a separate mode: it approves tools, answers the agent's questions itself and keeps going until the task is done. A warning dialog appears the first time you choose Allow all or Autopilot. Inside a session, /yolo or /autoApprove removes approvals for that session; chat.tools.global.autoApprove removes them in all workspaces.

At the Manual level you can write rules for the terminal. The workspace setting we added to Bookstore:

"chat.tools.terminal.autoApprove": {
  "/^dotnet (build|test)\\b/": true,
  "/^npm (test|run build)\\b/": true,
  "/^git (status|diff|log)\\b/": true,
  "/^git push\\b/": false,
  "/^dotnet ef database\\b/": false,
  "rm": false,
  "Remove-Item": false
}

Keys that start and end with / are regular expressions. A command matching true runs without approval; false does not block the command, it asks for approval every time and takes precedence over true rules and session approvals. A chained command (such as dotnet build && git push) runs without approval only if all its parts match a true rule and none matches a false rule. A command that matches neither your rules nor VS Code's built-in rules for read-only commands falls back to approval; that is why dotnet ef migrations add is asked about every time. The most important sentence in the documentation is this: terminal auto-approval is a convenience, not a security boundary. For a real boundary, VS Code's agent sandbox restricts the file system and network access of terminal commands; in 1.146 it is in preview on macOS, Linux and WSL, experimental on Windows, and stays on with Allow all and Autopilot.

In Visual Studio the agent asks for approval before terminal commands and tools that are not built in; with the Allow options in the approval prompt you can approve the tool for that session, that solution or always. These choices are reset under Tools > Options > GitHub > Copilot > Tools. In the new Chat experience in 18.12, the conversation mode (Interactive or Autopilot) and the tool permission (Manual or Allow all) are chosen separately; with Autopilot and Manual together, a tool without automatic approval is rejected instead of waiting. A note on Microsoft Learn is very important: agent mode can only touch files in the solution, but terminal commands run with the permissions of the Visual Studio process and are outside that boundary.

Bookstore: Tests First

The rule comes from the Requirements Analysis series: an institutional order needs at least 20 books in total. We wrote the tests before the code: 20 books across two lines, 5 books, and an unknown institution code:

[Fact]
public async Task Institutional_order_with_twenty_books_across_lines_is_placed()
{
    Guid cartId = await CreateCartAsync((Foundation, 12), (Middlemarch, 8));

    HttpResponseMessage response = await client.PostAsJsonAsync("/api/orders",
        new PlaceOrderRequest(cartId, "Barbara Liskov", "barbara@example.com", InstitutionCode: "RHS"), ct);

    Assert.Equal(HttpStatusCode.Created, response.StatusCode);
}

[Fact]
public async Task Institutional_order_below_twenty_books_is_rejected()
{
    Guid cartId = await CreateCartAsync((Emma, 5));

    HttpResponseMessage response = await client.PostAsJsonAsync("/api/orders",
        new PlaceOrderRequest(cartId, "Edsger Dijkstra", "edsger@example.com", InstitutionCode: "RHS"), ct);

    Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
    ProblemDetails? problem = await response.Content.ReadFromJsonAsync<ProblemDetails>(ct);
    Assert.Equal("Institutional orders need at least 20 books; this cart has 5.", problem?.Detail);
}
$ dotnet test
tests/Bookstore.Api.Tests/InstitutionalOrderTests.cs(23,80): error CS1739: The best overload for 'PlaceOrderRequest' does not have a parameter named 'InstitutionCode'

The first red is a compile error; that is expected, because InstitutionCode does not exist yet. Then we wrote the code. The Ordering module does not read the institutions table directly; following the rule in AGENTS.md, we added an IInstitutionService to the Institutional module. The check in OrderService:

// Kurumsal Sipariş: an order for an institution needs at least 20 books in total (Requirements Analysis series).
public const int InstitutionalMinimumQuantity = 20;

InstitutionInfo? institution = null;
if (!string.IsNullOrWhiteSpace(request.InstitutionCode))
{
    institution = await institutions.FindByCodeAsync(request.InstitutionCode.Trim(), ct);
    if (institution is null)
    {
        return new PlaceOrderOutcome.UnknownInstitution(request.InstitutionCode);
    }
    int quantity = cart.Items.Sum(i => i.Quantity);
    if (quantity <= InstitutionalMinimumQuantity)
    {
        return new PlaceOrderOutcome.BelowInstitutionalMinimum(quantity);
    }
}

For the InstitutionId added to Order we generated the migration with the tool (dotnet ef migrations add AddOrderInstitution): a nullable column, an index and a foreign key with Restrict behavior. dotnet ef database update, which applies the migration to the database, is false in our rules; when an agent wants to run it, it is always asked.

Two Ways to Turn a Red Test Green

If you look closely, the code above has a bug: it says <=, so an order of exactly 20 books is rejected too. We left this bug in on purpose and ran the tests:

$ dotnet test
...
failed Bookstore.Api.Tests.InstitutionalOrderTests.Institutional_order_with_twenty_books_across_lines_is_placed (13ms)
  Expected: Created
  Actual:   BadRequest
Test run summary: Failed!
  total: 14
  failed: 1
  succeeded: 13
  skipped: 0

There are two ways to turn the test green. The first is what the agent in Guess First did: raise the cart in the test to 21 books.

$ git diff --stat
 tests/Bookstore.Api.Tests/InstitutionalOrderTests.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git diff
...
-        Guid cartId = await CreateCartAsync((Foundation, 12), (Middlemarch, 8));
+        Guid cartId = await CreateCartAsync((Foundation, 12), (Middlemarch, 9));

$ dotnet test
...
Test run summary: Passed!
  total: 14
  failed: 0
  succeeded: 14
  skipped: 0

The second is to fix the code:

$ git diff --stat
 src/Bookstore.Api/Ordering/OrderService.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git diff
...
-            if (quantity <= InstitutionalMinimumQuantity)
+            if (quantity < InstitutionalMinimumQuantity)

$ dotnet test
...
Test run summary: Passed!
  total: 14
  failed: 0
  succeeded: 14
  skipped: 0

Both results are 14/14 green, and both diffs are a single line. In a summary both look like "tests pass". Only the file the diff is in tells them apart: the first is under tests/, the second under src/. The first "fix" changed the business rule: an institutional order of 20 books is now rejected, and no test is left to say so. The test's name still says "twenty books" while its data is 21; that mismatch is the clue to catch in review. We covered examining how AI turns a red CI green in the fourth post of the AI-Assisted Development series; in Copilot the practical version is this: every change under tests/ in the git diff --stat at the end of an agent session is a separate question. That is why the instruction file has the rule "never weaken an existing assertion" and the review instructions have "ask why a changed assertion changed"; but an instruction is a request, and the final check is the diff you read.

The Angular Side

The API enforces the rule; the client only tells the user early. On the cart page we added an optional institution code field, a hint for the missing quantity and an order button that is disabled below 20:

// Mirrors OrderService.InstitutionalMinimumQuantity; the API enforces the rule, the client only explains it.
export const institutionalMinimumQuantity = 20;
protected readonly institutionCode = signal('');
protected readonly minimum = institutionalMinimumQuantity;
protected readonly missingForInstitution = computed(() =>
  this.institutionCode().trim() ? Math.max(0, this.minimum - this.store.itemCount()) : 0);
<input #institutionBox name="institution" placeholder="Institution code (optional)"
       data-testid="institution-code" (input)="institutionCode.set(institutionBox.value)" />
<button type="submit" [disabled]="missingForInstitution() > 0">Place order</button>
...
@if (missingForInstitution() > 0) {
  <p class="hint" data-testid="institution-hint">
    Institutional orders need at least {{ minimum }} books. Add {{ missingForInstitution() }} more.
  </p>
}

The spec checks that with 5 books in the cart, typing an institution code makes the hint say "Add 15 more" and disables the button, and that there is no hint when the code is empty:

it('explains the minimum and blocks checkout below 20 books', async () => {
  const fixture = await render();
  await typeCode(fixture, 'RHS');

  const element = fixture.nativeElement as HTMLElement;
  expect(element.querySelector('[data-testid="institution-hint"]')?.textContent).toContain('Add 15 more');
  expect(element.querySelector<HTMLButtonElement>('button[type="submit"]')?.disabled).toBe(true);
});
$ npm test -- --watch=false
...
 Test Files  2 passed (2)
      Tests  3 passed (3)

Checkpoints, Keep/Undo and the Final Review

There are two mechanisms to undo what the agent did. In VS Code, checkpoints are on by default in supported sessions (chat.checkpoints.enabled): before each request a copy of the affected files is taken; when you hover over an earlier request and choose Restore Checkpoint, the files go back to that point and later requests are removed from the history. In the Copilot harness pending edits sit in a working set; you can accept or revert a single change, a file or everything with Keep or Undo. In Visual Studio the edited files are in the Total changes list; there is Keep and Undo per chunk, and since 18.6 a summary diff view that shows all files in a single tab. To go back to an earlier point you use Restore next to the checkpoint; according to the documentation there is no stepwise undo and redo yet.

You must know the limit of a checkpoint. The VS Code documentation says it plainly: a checkpoint restores files and chat history, but does not undo completed terminal commands, network requests, deployments or changes to external services. A migration applied to the database is on that list. Files changed in the terminal or by a script do not appear in the working set at all; you need to review them in Source Control or with git diff. That is why a clean commit before the agent starts is a more reliable way back than a checkpoint.

In the final state of the feature, the files changed relative to the fourth post's tag:

$ git diff --stat github-copilot/04
 .vscode/settings.json                              |    9 +
 .../20261114234727_AddOrderInstitution.Designer.cs | 1004 ++++++++++++++++++++
 .../20261114234727_AddOrderInstitution.cs          |   56 ++
 .../Migrations/BookstoreContextModelSnapshot.cs    |   10 +
 .../Institutional/InstitutionService.cs            |   20 +
 .../Institutional/InstitutionalModule.cs           |    3 +
 src/Bookstore.Api/Ordering/Order.cs                |    1 +
 src/Bookstore.Api/Ordering/OrderService.cs         |   24 +-
 .../Ordering/OrderingConfiguration.cs              |    2 +
 src/Bookstore.Api/Ordering/OrderingContracts.cs    |    3 +-
 src/Bookstore.Api/Ordering/OrderingModule.cs       |    7 +
 src/Bookstore.Api/Program.cs                       |    3 +-
 .../Bookstore.Api.Tests/InstitutionalOrderTests.cs |   65 ++
 web/bookstore-web/src/app/cart/cart-page.html      |    9 +-
 web/bookstore-web/src/app/cart/cart-page.spec.ts   |   54 ++
 web/bookstore-web/src/app/cart/cart-page.ts        |   10 +-
 web/bookstore-web/src/app/cart/cart-store.ts       |    6 +-
 web/bookstore-web/src/app/core/api-models.ts       |    3 +
 18 files changed, 1280 insertions(+), 9 deletions(-)

1,004 of the 1,280 lines are the generated migration designer file; the part to really review is about 200 lines. What we look at in human review: is there only a new file under tests/, or has an existing assertion changed? Does the migration's Up method lose data or contain an unexpected DropColumn? Does Ordering read another module's table? Do the API and the client describe the same rule in their error messages? The answers to these four questions are in the diff, not in the agent's summary.

In Production

Isolate the agent. In Visual Studio terminal commands run with the IDE's permissions; in VS Code the sandbox is still experimental on Windows. Running risky work in a separate worktree, a Dev Container or an environment with limited permissions limits the impact of a wrong command.

No command that touches production should be auto-approved: git push, dotnet ef database update, deploy scripts, cloud CLIs. A development machine should not hold a production connection string or a broadly scoped token; the agent's terminal can see them.

Watch the cost. As we saw in the calculation in the first post, every step resends the whole conversation; a long agent session that runs the tests again and again and reads their output spends most of the credits. That is why session limits such as --max-ai-credits in Copilot CLI exist.

Common Pitfalls

Auto-approving every command. rm, applying migrations and git push should always ask; terminal rules are not a security boundary.

Trusting the agent's summary. The sentence "all tests pass" does not say whether the tests were changed.

Not noticing an agent that turns a red test green by changing the test. Read every change under tests/ in git diff --stat separately; check whether a test's name and data agree.

Thinking a checkpoint undoes everything. It does not undo an applied migration, a pushed commit or files changed in the terminal.

Starting the agent on a dirty working copy. Commit before you start; let Git be your way back.

A Small Experiment

★☆☆ Write chat.tools.terminal.autoApprove rules in your own project. Allow the build and test commands; set git push and applying migrations to false. After a week, see which commands asked for approval most and adjust the rules.

★★☆ Put the <= bug back in Bookstore and tell the agent "make the tests green". Did the agent fix the code or the test? Compare the git diff --stat output at the end of the session with the agent's summary.

★★★ Add the second half of the institutional order rule: a 10 percent discount for institutional orders. Write the tests first (discounted total, rounding, the 19 and 20 book boundary), then let the agent implement it. In review, compare every line with the rule from the Requirements Analysis series.

In this post the agent used only the IDE's built-in tools. In the next post we extend it: an MCP server for Bookstore, the GitHub and Playwright MCP servers, custom agents, agent skills and hooks that enforce rules.