Run a Gated Express 5 Upgrade
Use Claude Code to upgrade Express 4 to 5 with gates, tests, and Git rollback.
Introduction
30 Second Summary
A risky software update can pass every quick check and still fail when the service starts. Without fixed decision rules, one green result can distract a team from the evidence that is still missing.
In this project, you will upgrade a small delivery service from Express 4.22.2 to 5.2.1 with Claude Code. You will use predeclared verification gates to control the work from investigation through the final release decision.
What You'll Build
You will run a staged upgrade whose real startup failure produces a reviewable evidence trail for the final release decision.
By the end of this project, you'll have:
- A reproducible upgrade trail you can review checkpoint by checkpoint in Git, including the baseline-express-4 rollback tag.
- A gated autonomous workflow that carries Claude Code from a read-only Plan Mode investigation to a deliberate stop at the first red gate.
- An evidence-backed release decision that cites the green checks, the startup repair, the missing contract evidence, and the rollback target.
- Secret Mission: Add a deterministic fallback-contract test that exposes Express 5's wildcard-array behavior before a bounded repair restores the original string response.
Are there any prerequisites?
You need a Mac with Node.js 20 or newer, Visual Studio Code, Git, and Claude Code installed. You also need an existing Claude subscription plus experience with Express and terminal workflows.
Before We Start
This project upgrades an Express 4 service to Express 5 under delivery gates declared before execution. You are defining the decision boundary now so Claude Code cannot silently choose repair, revert, or release without evidence or a safe rollback target.
Build the Express 4 Delivery Fixture
A risky Express upgrade only produces useful evidence when the starting service is deterministic and green. This fixture gives every later result a trustworthy comparison point.
You will verify Node.js, Git, Claude Code, and Visual Studio Code before creating the baseline service. You will use TypeScript to compile the fixture without changing its Express 4 behavior.
In this step, get ready to:
- Verify the existing Mac toolchain.
- Create the Express 4 delivery fixture.
- Install the dependencies and compile the baseline.
Verify the existing Mac toolchain
The fixture requires Node.js 20 or newer so its built-in test runner is stable. The other checks confirm that the tools needed for later planning and checkpoint work are available.
- Check the installed tool versions by running these commands:
node --version
git --version
claude --version
What do these checks prove?
- The Node.js result confirms whether the runtime meets the minimum version for this project.
- The Git result confirms that durable commits and tags are available.
- The Claude Code result confirms that the autonomous delivery tool is available.
Choose the tab that matches the Node.js result you see.
✔️ I see version 20 or higher
Your Node.js runtime meets the project requirement. You can continue with the installed version.
ⓧ I see an older version
Your current runtime is too old for this fixture. Install the current Node.js v24.20.0 LTS package before continuing.
- Visit the official Node.js download page.
- Download the macOS package for Node.js v24.20.0 LTS.
- Complete the macOS installation.
- Repeat the Node.js version check by running this command:
node --version
What should the version show?
The command prints the active Node.js version. Continue when the major version is 20 or higher.
ⓧ Command not found
Node.js is missing from your shell. Install the current Node.js v24.20.0 LTS package before creating the fixture.
- Visit the official Node.js download page.
- Download the macOS package for Node.js v24.20.0 LTS.
- Complete the macOS installation.
- Restart your terminal.
- Repeat the version check by running this command:
node --version
What should the version show?
The command now prints the installed Node.js version. Continue when the major version is 20 or higher.
Your Git result should contain a version number. Your Claude Code result should contain a version followed by (Claude Code).
Missing a tool result?
If either remaining command is unavailable, confirm that the corresponding application is installed on your Mac. Restart the terminal after changing an installation so the shell can refresh its command paths.
Help me diagnose my toolchain check.
Create the Express 4 delivery fixture
The fixture separates service behavior from the later framework upgrade. Its narrow unit test checks formatting logic while startup remains a distinct runtime gate.
- Create the project folders on your Desktop by running these commands:
cd ~/Desktop
mkdir claude-express-upgrade
cd claude-express-upgrade
mkdir src test
What do these commands create?
The commands create the claude-express-upgrade folder on your Desktop. The nested src and test folders separate application code from tests.
- Confirm the source folders exist by running:
ls
What should the folder check show?
The output should list src and test.
- Open the current claude-express-upgrade folder in Visual Studio Code by running:
code .
What does this command open?
The dot represents the current folder. Visual Studio Code opens claude-express-upgrade as the active workspace.
You should see src and test in the Explorer sidebar.
Visual Studio Code did not open?
The macOS command-line launcher may be missing from your shell path. In Visual Studio Code, press Cmd+Shift+P to open the Command Palette.
- Type shell command into the Command Palette.
- Select Shell Command: Install 'code' command in PATH.
- Restart the terminal.
- Retry the folder-opening command above.
Help me open my project folder in Visual Studio Code.
- Create .gitignore inside the claude-express-upgrade folder using the Explorer sidebar.
- Paste this content into .gitignore:
node_modules/
dist/
What does this file exclude?
The file keeps installed dependencies and compiled output out of future Git commits. Both directories can be recreated from the tracked project files.
- Save .gitignore.
- Check the Explorer sidebar for .gitignore.
You should see .gitignore beside the two source folders.
Cannot see .gitignore?
Confirm that you saved the file directly inside claude-express-upgrade. Check that the filename begins with a dot.
Help me locate my .gitignore file.
- Create package.json inside the claude-express-upgrade folder using the Explorer sidebar.
- Paste this baseline package configuration into package.json:
{
"name": "claude-express-upgrade",
"version": "1.0.0",
"private": true,
"type": "module",
"engines": {
"node": ">=20"
},
"scripts": {
"build": "tsc",
"test": "node --test",
"verify": "npm run build && npm test",
"start": "node dist/server.js"
},
"dependencies": {
"express": "4.22.2"
},
"devDependencies": {
"typescript": "7.0.2"
}
}
What does this configuration define?
- The engine requirement records Node.js 20 as the minimum runtime.
- The scripts separate compilation, testing, verification, and startup into repeatable commands.
- The exact Express 4.22.2 dependency creates the baseline for the later upgrade.
- The exact TypeScript 7.0.2 development dependency makes compilation reproducible.
- Save package.json.
- Check the Explorer sidebar for package.json.
You should see package.json at the same level as .gitignore.
Package file shows JSON errors?
Check every comma and double quote against the reference. A missing comma prevents npm from reading the package configuration.
Help me debug my package.json syntax.
- Create tsconfig.json inside the claude-express-upgrade folder using the Explorer sidebar.
- Paste this compiler configuration into tsconfig.json:
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "src",
"outDir": "dist",
"noEmitOnError": true
},
"include": ["src/**/*.js"]
}
How does compilation work?
The compiler reads JavaScript files from src and emits them into dist. The module settings preserve the project's ESM imports.
The noEmitOnError option prevents a broken compilation from producing misleading output.
- Save tsconfig.json.
- Check the Explorer sidebar for tsconfig.json.
You should see tsconfig.json beside package.json.
Compiler configuration shows errors?
Confirm that the file is valid JSON. Check that include sits outside compilerOptions.
Help me debug my tsconfig.json file.
- Create format.js inside the src folder using the Explorer sidebar.
- Paste this status formatter into src/format.js:
export function serviceStatus(name) {
return {
service: name,
status: "ready"
};
}
What does this function return?
The serviceStatus() function creates the service payload used by the health route. Its narrow output also gives the baseline unit test one deterministic behavior to check.
- Save src/format.js.
- Check the src folder for format.js.
You should see format.js inside src.
Formatter file is misplaced?
Confirm that format.js is nested inside src. A copy at the top level will not match the compiler input path.
Help me fix the format.js location.
- Create app.js inside the src folder using the Explorer sidebar.
- Paste this Express 4 application into src/app.js:
import express from "express";
import { serviceStatus } from "./format.js";
export function buildApp() {
const app = express();
app.get("/health", (_request, response) => {
response.json(serviceStatus("delivery-service"));
});
app.get("/*", (req, response) => {
response.json({ view: req.params[0] || "home" });
});
return app;
}
How does the baseline application behave?
- The buildApp() function creates the Express application without starting a network listener.
- The /health route returns the deterministic service status.
- The unnamed /* wildcard captures the remaining Express 4 path in req.params[0].
- The fallback returns home when the captured path is empty.
- Save src/app.js.
- Check the src folder for app.js.
You should now see app.js beside format.js.
Application file shows a syntax error?
Check the braces around buildApp() and both route callbacks. Confirm that the wildcard route contains req.params[0].
Help me debug the Express 4 application file.
- Create server.js inside the src folder using the Explorer sidebar.
- Paste this server entry point into src/server.js:
import { buildApp } from "./app.js";
const port = 3000;
const app = buildApp();
app.listen(port, () => {
console.log(`delivery-service listening on http://localhost:${port}`);
});
What does the server entry point do?
The entry point builds the application and listens on port 3000. Its log message becomes the visible evidence for the runtime gate in the next step.
- Save src/server.js.
- Check the src folder for server.js.
You should now see all three source files inside src.
Server import shows an error?
Confirm that server.js and app.js are both inside src. Keep the .js extension in the import path.
Help me debug the server entry point.
- Create format.test.mjs inside the test folder using the Explorer sidebar.
- Paste this unit test into test/format.test.mjs:
import assert from "node:assert/strict";
import test from "node:test";
import { serviceStatus } from "../dist/format.js";
test("serviceStatus reports a ready service", () => {
assert.deepStrictEqual(serviceStatus("delivery-service"), {
service: "delivery-service",
status: "ready"
});
});
What does this test cover?
The test imports the compiled formatter from dist/format.js and checks its exact response. It deliberately leaves route registration outside this narrow unit gate.
- Save test/format.test.mjs.
- Check the test folder for format.test.mjs.
You should see the unit test inside test.
Test file is in the wrong folder?
Confirm that the path is exactly test/format.test.mjs. The import expects compiled output to sit one level above the test folder in dist.
Help me fix the unit test structure.
Install dependencies and compile the baseline
The package configuration now pins the baseline dependencies. Installing them creates the lockfile that preserves the resolved dependency tree.
- Return to the terminal from earlier.
- Install the project dependencies by running:
npm install
What does the installation create?
npm installs Express 4.22.2 and TypeScript 7.0.2 from package.json. It also creates package-lock.json with the resolved dependency versions.
- Wait for the terminal prompt to return.
- Check the Explorer sidebar for package-lock.json.
- Check the Explorer sidebar for node_modules.
You should see the generated lockfile and dependency directory. The installation is ready for compilation.
Dependency installation failed?
Confirm that the terminal is inside the Desktop copy of claude-express-upgrade. Check the JSON syntax in package.json if npm cannot read the package configuration.
Help me debug the dependency installation.
Before you compile, which three source filenames do you expect TypeScript to emit inside dist?
- Compile the Express 4 fixture by running:
npm run build
What does the build command do?
The build script runs the local TypeScript compiler against tsconfig.json. Successful compilation copies the JavaScript module structure from src into dist.
- Expand the dist folder in the Explorer sidebar.
- Confirm that dist/app.js exists.
- Confirm that dist/format.js exists.
- Confirm that dist/server.js exists.
You should see all three emitted files without compiler errors. That is the deterministic green build needed before the upgrade begins.
Build did not emit all three files?
Check that every application file sits inside src. Compare the filename casing with the imports in app.js and server.js.
Help me debug the baseline build.
The reference tab contains the complete handwritten fixture. Use it to compare every file with the baseline that produced your green build.
✔️ Awesome, I've got everything!
Your Express 4 fixture is installed and compiled. Keep Visual Studio Code open to claude-express-upgrade for the baseline runtime checks in the next step.
ⓧ I'd like to double check the full code
Compare each complete file below with the matching file in your claude-express-upgrade folder.
node_modules/
dist/
What should match here?
Your ignore file should contain only the dependency directory and compiled output directory.
{
"name": "claude-express-upgrade",
"version": "1.0.0",
"private": true,
"type": "module",
"engines": {
"node": ">=20"
},
"scripts": {
"build": "tsc",
"test": "node --test",
"verify": "npm run build && npm test",
"start": "node dist/server.js"
},
"dependencies": {
"express": "4.22.2"
},
"devDependencies": {
"typescript": "7.0.2"
}
}
What should match here?
Your package file should pin Express 4.22.2 and TypeScript 7.0.2 with the four project scripts shown above.
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "src",
"outDir": "dist",
"noEmitOnError": true
},
"include": ["src/**/*.js"]
}
What should match here?
Your compiler configuration should read from src and emit into dist.
export function serviceStatus(name) {
return {
service: name,
status: "ready"
};
}
What should match here?
Your formatter should return the service name with the literal ready status.
import express from "express";
import { serviceStatus } from "./format.js";
export function buildApp() {
const app = express();
app.get("/health", (_request, response) => {
response.json(serviceStatus("delivery-service"));
});
app.get("/*", (req, response) => {
response.json({ view: req.params[0] || "home" });
});
return app;
}
What should match here?
Your baseline application should retain the Express 4 unnamed wildcard and read its capture from req.params[0].
import { buildApp } from "./app.js";
const port = 3000;
const app = buildApp();
app.listen(port, () => {
console.log(`delivery-service listening on http://localhost:${port}`);
});
What should match here?
Your server entry point should build the application and listen on port 3000.
import assert from "node:assert/strict";
import test from "node:test";
import { serviceStatus } from "../dist/format.js";
test("serviceStatus reports a ready service", () => {
assert.deepStrictEqual(serviceStatus("delivery-service"), {
service: "delivery-service",
status: "ready"
});
});
What should match here?
Your narrow unit test should import the compiled formatter and verify its exact object response.
Your green Express 4 fixture is ready for runtime verification. Next, you will lock its behavior into a committed baseline with delivery gates and a rollback target.
Lock the Baseline and Decision Rules
Your Express fixture now compiles. That green build gives the upgrade campaign a reproducible starting point.
A successful build cannot prove that the server starts. It also cannot prove that each route preserves its response contract.
Autonomous execution needs committed success criteria before any risky change begins. Git will anchor those criteria to a durable rollback target.
In this step, get ready to:
- Prove the baseline runtime contract.
- Predeclare the delivery gates and decision criteria.
- Commit the baseline with a rollback tag.
Verify the baseline runtime contract
Compilation checks whether the source can be emitted. A live start proves that Express can register every route.
Before you run the full verification, do you expect both the compiler and the narrow unit test to stay green?
- Verify the compiled service and its unit test by running:
npm run verify
What does this verification cover?
- The npm run build stage compiles the source into dist/.
- The npm test stage runs the narrow serviceStatus() unit test.
- A green result establishes the static verification evidence for the baseline.
You should see the build finish without compiler errors. The test summary should report one passing test.
Verification failed?
- Confirm your terminal is inside the claude-express-upgrade folder.
- Confirm package.json still contains the verify script.
Ask for help with the failing stage: help me diagnose my baseline verification failure
The start script runs the service in the foreground. Your terminal remains occupied until you stop the process.
Before you start the service, do you expect route registration to succeed with the current baseline?
- Start the baseline service by running:
npm start
What does this command prove?
The npm start script runs dist/server.js. Reaching the listening message proves that Express registered the baseline routes successfully.
The running process also makes the service available for browser checks on port 3000.
You should see delivery-service listening on http://localhost:3000 in the terminal.
Good. Your baseline process is alive and accepting requests.
- Switch to your browser.
- Enter http://localhost:3000/health in the address bar.
You should see {"service":"delivery-service","status":"ready"}.
- Replace the current address with http://localhost:3000/dashboard/settings.
You should see {"view":"dashboard/settings"}. This string response is the fallback contract that the upgrade must preserve.
Browser response missing?
- Confirm the terminal process is still running.
- Confirm the terminal printed delivery-service listening on http://localhost:3000.
- Retry the address with http:// at the beginning.
Get targeted help here: help me troubleshoot the local Express responses
- Switch back to the terminal from earlier.
- Stop the server with Ctrl+C.
Write the pre-execution delivery brief
A delivery brief fixes the evidence requirements before the upgrade begins. This prevents a later green result from weakening a gate that exposes risk.
- In the Visual Studio Code file sidebar, select the new-file icon beside claude-express-upgrade.
- Name the file DELIVERY_BRIEF.md.
- Build its objective, baseline evidence, and gates by pasting this first section:
# Express 5 Delivery Brief
## Objective
Upgrade Express from 4.22.2 to 5.2.1 without silently changing the service's startup behavior or fallback response contract.
## Baseline evidence
- `npm run build` completes successfully.
- `npm test` passes.
- `npm start` prints `delivery-service listening on http://localhost:3000`.
- `/health` returns `{"service":"delivery-service","status":"ready"}`.
- `/dashboard/settings` returns `{"view":"dashboard/settings"}`.
## Delivery gates
1. Dependency scope: only the intended Express dependency and lockfile resolution change.
2. Static verification: compilation and the existing unit test pass.
3. Runtime verification: the process reaches its listening message and `/health` preserves its response.
4. Contract verification: `/dashboard/settings` preserves the Express 4 string payload contract.
How do these gates work?
- The dependency gate limits the upgrade to the intended package change.
- The static gate checks compilation and the existing unit test.
- The runtime gate proves that the service starts and preserves its health response.
- The contract gate protects the fallback behavior that callers already receive.
- Save DELIVERY_BRIEF.md.
- Confirm the editor shows four numbered delivery gates.
Missing a gate?
Compare the numbered list with the snippet above. Each gate must remain separate so a later report can identify the exact boundary that failed.
Use this prompt for help: help me compare my four delivery gates
The gates define what evidence must exist. The decision criteria define what happens when that evidence turns red or remains incomplete.
- Place your cursor after the fourth delivery gate in DELIVERY_BRIEF.md.
- Complete the decision rules and rollback target by pasting:
## Decision criteria
Repair only when the cause is documented, the change is localized to one source file, no unrelated gate is red, the rollback tag exists, and a full verification rerun fits within ten minutes.
Revert when the baseline was green and the upgrade remains red after one bounded repair, or preserving the public contract requires work beyond the approved scope.
Stop without shipping or reverting when required evidence is missing, the failure is ambiguous, the working tree is contaminated, or the rollback target cannot be identified safely.
## Execution boundaries
- Work only on the local upgrade branch.
- Do not push or deploy.
- Commit only after a defined gate is green.
- Stop at the first red gate and report evidence before changing course.
## Rollback target
The immutable local tag is `baseline-express-4`.
How do the decision rules work?
Repair is available when the cause is documented. The change must also stay localized within the original timebox.
Revert protects the green baseline when one bounded repair cannot restore the failed gate. It also applies when preserving the public contract expands beyond the approved scope.
Stop applies when required evidence is missing or ambiguous. This keeps release authority tied to the written gates.
- Save DELIVERY_BRIEF.md.
- Confirm the final section names baseline-express-4 as the rollback target.
Decision sections out of order?
Keep the decision criteria before the execution boundaries. Keep the rollback target as the final section.
Ask for a focused comparison: help me check my delivery brief structure
✔️ Awesome, I've got everything!
Great. Your delivery gates and rollback criteria now exist before any upgrade work begins.
ⓧ I'd like to double check the full code
Compare your complete DELIVERY_BRIEF.md with this reference:
# Express 5 Delivery Brief
## Objective
Upgrade Express from 4.22.2 to 5.2.1 without silently changing the service's startup behavior or fallback response contract.
## Baseline evidence
- `npm run build` completes successfully.
- `npm test` passes.
- `npm start` prints `delivery-service listening on http://localhost:3000`.
- `/health` returns `{"service":"delivery-service","status":"ready"}`.
- `/dashboard/settings` returns `{"view":"dashboard/settings"}`.
## Delivery gates
1. Dependency scope: only the intended Express dependency and lockfile resolution change.
2. Static verification: compilation and the existing unit test pass.
3. Runtime verification: the process reaches its listening message and `/health` preserves its response.
4. Contract verification: `/dashboard/settings` preserves the Express 4 string payload contract.
## Decision criteria
Repair only when the cause is documented, the change is localized to one source file, no unrelated gate is red, the rollback tag exists, and a full verification rerun fits within ten minutes.
Revert when the baseline was green and the upgrade remains red after one bounded repair, or preserving the public contract requires work beyond the approved scope.
Stop without shipping or reverting when required evidence is missing, the failure is ambiguous, the working tree is contaminated, or the rollback target cannot be identified safely.
## Execution boundaries
- Work only on the local upgrade branch.
- Do not push or deploy.
- Commit only after a defined gate is green.
- Stop at the first red gate and report evidence before changing course.
## Rollback target
The immutable local tag is `baseline-express-4`.
What are you checking?
The brief contains the same four gates that the runtime evidence established. Its final line names baseline-express-4 as the rollback target.
Commit the baseline and rollback target
The runtime evidence and written rules now need durable history. A committed checkpoint preserves the exact state that later repair or revert decisions must reference.
Why use Git checkpoints?
Claude Code checkpoints help recover work inside a session. Git preserves repository history beyond that session.
The baseline tag gives every later decision a named rollback target. That makes recovery independent of the autonomous execution session.
- Initialize the repository, stage the fixture, commit the baseline, and create its rollback tag by running these commands:
git init -b main
git add .
git commit -m "baseline: verified Express 4 service"
git tag baseline-express-4
What does this checkpoint contain?
- The repository starts on the main branch.
- The staging command includes the service fixture and DELIVERY_BRIEF.md in the baseline snapshot.
- The commit records the verified Express 4 state before upgrade execution.
- The baseline-express-4 tag names the durable rollback target.
Before you check the repository, do you expect Git to report any uncommitted files?
- Verify the committed baseline by running:
git status
What does this status prove?
A clean status proves that the fixture and delivery brief are captured in the baseline commit. Future changes can now be compared against a known state.
That's the control point secured. You should see nothing to commit, working tree clean.
Working tree not clean?
- Review the paths listed by git status.
- Confirm DELIVERY_BRIEF.md was saved before the staging command ran.
- Repeat the staging and commit commands after saving any missing baseline file.
Get help without changing the upgrade scope: help me make my baseline commit clean
Your verified baseline and decision rules are now locked into Git. Next, you will let Claude investigate the upgrade while Plan mode keeps the source unchanged.
Investigate in Plan Mode
Your Express 4 baseline is green. Its delivery rules are committed in Git.
Claude Code now needs room to inspect the repository before the risky upgrade begins. Plan Mode keeps that investigation read-only.
The reviewed plan must preserve the four gates. Every repair, revert, or release decision must wait for evidence.
In this step, get ready to:
- Create the isolated upgrade branch.
- Run a read-only repository investigation with Claude Code.
- Verify the proposed plan against the committed delivery controls.
Create the upgrade branch and start Plan mode
The upgrade needs its own branch so the verified baseline stays available. Plan mode then gives Claude permission to inspect the repository without editing source files.
- Return to the terminal panel in Visual Studio Code.
- Create upgrade/express-5 from the clean baseline by running this command:
git switch -c upgrade/express-5
What does this command do?
The command creates upgrade/express-5 at HEAD. It also switches the current repository to that branch.
Git confirms that it switched to the new upgrade branch.
Branch creation failed?
- Confirm the terminal is inside claude-express-upgrade.
- Check that the branch name matches upgrade/express-5 exactly.
Ask for help with the branch error.
- Start Claude Code in Plan mode by running this command:
claude --permission-mode plan
Why start in Plan mode?
Plan mode lets Claude read files and investigate the repository. Source edits remain blocked until you approve a plan.
This preserves the boundary between understanding the upgrade and executing it.
- Confirm the Claude Code status bar shows plan mode on.
Claude Code did not enter Plan mode?
- Confirm the earlier Claude Code version check still succeeds in the same terminal.
- Exit the current Claude Code session.
- Run the Plan mode launch command again.
Ask for help starting Plan mode.
Give Claude the delivery brief
A bounded delivery prompt tells Claude which evidence controls the campaign. It also prevents a failed gate from turning into an unapproved repair.
- Paste this delivery prompt into Claude Code:
Read DELIVERY_BRIEF.md and inspect the repository. Plan an upgrade from Express 4.22.2 to 5.2.1. Use the existing gates in their written order. After the dependency change, run the build and narrow unit test before starting the service. Commit a checkpoint only after a gate is green. If any gate is red, do not repair, revert, or continue. Stop and report the evidence against the predeclared criteria. Do not push or deploy.
What does this prompt control?
- The DELIVERY_BRIEF.md file remains the authority for execution.
- The gates stay in their written order.
- Static verification runs before startup verification.
- A red gate pauses every repair, revert, or continuation decision.
- The local-only boundary blocks pushes and deployments.
- Let Claude Code inspect DELIVERY_BRIEF.md plus the repository.
You will see a proposed staged plan. Your project files remain unchanged during this investigation.
Review the plan against every gate
A useful plan makes every evidence boundary explicit. Missing controls must be added before execution receives approval.
- Match the proposed dependency stage to Gate 1.
- Confirm the static stage covers the build plus the narrow unit test.
- Confirm the runtime stage starts only after static verification is green.
- Confirm the contract stage checks /dashboard/settings against the Express 4 string payload.
- Confirm the plan preserves baseline-express-4 as the rollback target.
- Confirm each commit follows a green gate.
- Confirm the first red gate triggers an evidence report before any repair, revert, or release decision.
- Ask Claude to revise the plan until every missing control appears explicitly.
Before Claude checks the working tree, do you expect a read-only investigation to report any changed paths?
- Ask Claude to verify the working tree before plan approval by running this command:
git status --short
What does an empty result prove?
The command reports changed paths in short format. No path output proves that the planning investigation left the working tree clean.
Seeing changed paths?
- Keep the plan unapproved.
- Pause the upgrade until the working tree is clean.
Ask for help investigating the changed paths.
- Leave the reviewed plan unapproved at the end of this step.
- Keep Claude Code running in Plan mode.
You kept the evidence boundary intact. Claude now has a reviewed plan while your source files remain untouched.
Your upgrade campaign now has a reviewed route through every gate. Next, you will authorize the staged execution and let the evidence determine where autonomy stops.
Run the Runtime Gate
Your reviewed Claude Code plan has separated investigation from execution. The next test asks whether its gates can control a risky framework upgrade.
Static verification can look green while runtime behavior remains unchecked. Route registration only happens when the Express service starts.
The approved plan creates a controlled path from the dependency change to runtime evidence. Each green stage earns a durable Git checkpoint.
In this step, get ready to:
- Authorize the reviewed local execution plan.
- Confirm the committed Express 5.2.1 dependency stage includes the lockfile resolution.
- Use the runtime gate to test route registration.
Authorize the reviewed plan
Plan mode has kept the source files unchanged while Claude inspected the repository. Approval grants edit access only for the reviewed local plan.
Granting autonomous edit access can feel risky. Your clean baseline plus the stop-at-first-red rule bounds this run.
Claude Code can continue with auto mode when it is available. The fallback path accepts edits automatically while local commands still require approval.
✔️ Auto mode is available
- Select Yes, and use auto mode to authorize the reviewed plan.
ⓧ Auto mode is unavailable
- Select the auto-accept-edits option.
- Approve each planned local command when Claude requests it.
- Allow Claude to execute the reviewed dependency stage through Gate 2.
You'll see package.json move to Express 5.2.1. Claude refreshes package-lock.json with npm install.
Claude inspects the lockfile diff. It then runs npm run verify.
The build passes. The narrow unit test passes.
That's the first controlled win: the Express 5.2.1 dependency stage is green. Claude records it as a Git checkpoint before continuing.
✔️ Awesome, I've got everything!
The dependency change is committed with its matching lockfile resolution. Gate 2 is green.
ⓧ I'd like to double check the full code
- Compare your updated package.json with this complete file:
{
"name": "claude-express-upgrade",
"version": "1.0.0",
"private": true,
"type": "module",
"engines": {
"node": ">=20"
},
"scripts": {
"build": "tsc",
"test": "node --test",
"verify": "npm run build && npm test",
"start": "node dist/server.js"
},
"dependencies": {
"express": "5.2.1"
},
"devDependencies": {
"typescript": "7.0.2"
}
}
Run the startup gate
A compiler can accept code without proving that the application starts. Gate 3 constructs the service so Express must register every route.
Before Claude runs npm start, do you expect the service to reach its listening message?
- Let Claude test route registration by running the planned startup command:
npm start
What Does This Gate Test?
- The npm start command executes the start script from package.json.
- The buildApp() function constructs the Express application.
- Route registration happens before app.listen() reaches the listening callback.
Claude's process exits during route registration. You'll see an error mentioning Missing parameter name before the listening message can print.
This Red Gate Is Expected
The unchanged app.get("/*", (req, response) => { line uses an unnamed wildcard. Express 5.2.1 rejects that wildcard while registering the route.
The existing unit test exercises only serviceStatus(). It never constructs buildApp().
Confirm Claude stopped at the boundary
A red gate becomes useful evidence when autonomy stops before changing course. Claude's report must now connect the failure to the rules written before execution.
- Read Claude's stopped-gate report.
- Confirm it names Gate 3 as red.
- Confirm it identifies the Express 5.2.1 dependency commit as the last green checkpoint.
- Confirm it cites baseline-express-4 as the rollback target.
- Confirm it states that no repair was made.
- Leave Claude waiting for a repair, revert, or stop decision.
Before you inspect the history, do you expect to see a route repair commit or only the green dependency checkpoint?
- Confirm the latest three Git commits by running this command from the claude-express-upgrade folder in a second terminal:
git log -3
What Does This History Check Show?
The git log -3 command limits the history view to the latest three commits. The newest upgrade entry should be the green dependency checkpoint.
You should see the committed Express 5.2.1 dependency checkpoint among the latest entries. No route repair commit appears.
Your runtime gate has done its job. Claude is waiting at the evidence boundary with baseline-express-4 preserved.
The failed startup is now decision-quality evidence. Next, you'll compare it with the original criteria before authorizing any change.
Repair Startup and Stop Release
Your reviewed Claude Code campaign has reached Gate 3. The Express 5.2.1 dependency checkpoint is green.
Startup still fails at the legacy wildcard route. The criteria in DELIVERY_BRIEF.md now determine whether a bounded repair is allowed.
A green process supplies runtime evidence. Gate 4 still needs separate proof before this release can move forward.
In this step, get ready to:
- Authorize one bounded repair using the predeclared criteria.
- Record the evidence in RELEASE_DECISION.md.
- Verify the repaired runtime gate while keeping the release stopped.
Authorize one bounded repair
The failed route has a documented compatibility cause. The repair is confined to one source file.
Why is repair allowed?
Express 5 requires a named wildcard. The root-inclusive form uses /{*splat}.
The change is localized to src/app.js. The rollback tag still exists.
- In the Claude Code session from earlier, ask Claude to compare the Gate 3 failure with the Decision criteria in DELIVERY_BRIEF.md.
- Authorize one repair limited to src/app.js.
- Tell Claude to replace app.get("/*", (req, response) => { with app.get("/{*splat}", (request, response) => {.
- Tell Claude to replace response.json({ view: req.params[0] || "home" }); with response.json({ view: request.params.splat ?? [] });.
Before Claude reruns the gates, do you expect this one-file repair to restore startup without changing the dependency checkpoint?
- Tell Claude to rerun Gates 1 through 3 in their written order.
You should see compilation and the narrow unit test pass. The service should then reach its listening message.
Claude's runtime evidence should show the original response from /health.
- Tell Claude to commit the bounded startup repair after Gate 3 is green.
That's the bounded repair complete. Your Git history now contains a green runtime checkpoint without broadening the approved scope.
Gate 3 still red?
- Confirm that the route starts with /{*splat}.
- Confirm that the handler parameter is named request.
- Confirm that the response reads request.params.splat ?? [].
Ask Claude to stop before making any broader change. Help me diagnose why Gate 3 remains red after the bounded wildcard repair.
✔️ Awesome, I've got everything!
Your startup repair is committed. Gate 3 now has fresh runtime evidence.
ⓧ I'd like to double check the full code
Compare your repaired src/app.js with this complete file:
import express from "express";
import { serviceStatus } from "./format.js";
export function buildApp() {
const app = express();
app.get("/health", (_request, response) => {
response.json(serviceStatus("delivery-service"));
});
app.get("/{*splat}", (request, response) => {
response.json({ view: request.params.splat ?? [] });
});
return app;
}
This file contains only the approved wildcard repair. The health route remains unchanged.
Create the release decision
The repaired startup closes Gate 3. Gate 4 remains incomplete because no automated check protects the fallback payload contract.
- Ask Claude to create RELEASE_DECISION.md with the exact approved content below:
# Release Decision
Decision: STOP BEFORE RELEASE
## Pre-execution authority
This decision uses the gates and criteria in `DELIVERY_BRIEF.md`, committed before the upgrade and anchored by `baseline-express-4`.
## Evidence
- Gate 1 passed: the intended Express version and lockfile changed on the upgrade branch.
- Gate 2 passed: compilation and the narrow unit test remained green after the upgrade.
- Gate 3 first failed: the process exited while registering the unnamed `/*` route.
- The bounded repair changed the route to `/{*splat}` and read `request.params.splat`.
- Gate 3 then passed: the process listened successfully and `/health` preserved its response.
- Gate 4 is incomplete: the fallback payload contract has not been protected by an automated compatibility check.
## Criteria mapping
Repair was allowed because the startup cause was documented, localized to one source file, unrelated gates were green, the rollback tag existed, and verification fit the timebox.
Revert was not triggered because the first bounded repair restored the failed startup gate.
Stop is required because a required contract gate still lacks evidence. Shipping now would violate the rule written before execution.
## Rollback readiness
Rollback target: `baseline-express-4`.
No push or deployment has occurred. The branch can be retained for further evidence or reverted from its committed checkpoints.
How does this decision use the gates?
- The Evidence section records both the failed startup attempt and the successful repair.
- The Criteria mapping section explains why repair was allowed.
- The Rollback readiness section preserves baseline-express-4 as the immutable recovery point.
- Review the RELEASE_DECISION.md diff in Claude Code.
- Confirm that the first line after the title says Decision: STOP BEFORE RELEASE.
- Confirm that the evidence section records Gate 4 as incomplete.
Decision record drifted?
Keep every criterion anchored to DELIVERY_BRIEF.md. Remove any claim that Gate 4 passed.
The rollback target must remain baseline-express-4. Help me compare my release decision with the predeclared gates.
✔️ Awesome, I've got everything!
Your decision record preserves the original authority. The release remains stopped at the missing evidence boundary.
ⓧ I'd like to double check the full code
Compare your RELEASE_DECISION.md with this complete record:
# Release Decision
Decision: STOP BEFORE RELEASE
## Pre-execution authority
This decision uses the gates and criteria in `DELIVERY_BRIEF.md`, committed before the upgrade and anchored by `baseline-express-4`.
## Evidence
- Gate 1 passed: the intended Express version and lockfile changed on the upgrade branch.
- Gate 2 passed: compilation and the narrow unit test remained green after the upgrade.
- Gate 3 first failed: the process exited while registering the unnamed `/*` route.
- The bounded repair changed the route to `/{*splat}` and read `request.params.splat`.
- Gate 3 then passed: the process listened successfully and `/health` preserved its response.
- Gate 4 is incomplete: the fallback payload contract has not been protected by an automated compatibility check.
## Criteria mapping
Repair was allowed because the startup cause was documented, localized to one source file, unrelated gates were green, the rollback tag existed, and verification fit the timebox.
Revert was not triggered because the first bounded repair restored the failed startup gate.
Stop is required because a required contract gate still lacks evidence. Shipping now would violate the rule written before execution.
## Rollback readiness
Rollback target: `baseline-express-4`.
No push or deployment has occurred. The branch can be retained for further evidence or reverted from its committed checkpoints.
This record preserves the original criteria. It cites the failed gate before documenting the bounded repair.
Verify the evidence boundary
The final check separates service health from release readiness. Gate 3 can pass while Gate 4 remains open.
Before you run the final checks, which result can prove Gate 3? Which required gate will remain incomplete?
- Verify the build and narrow unit test by running:
npm run verify
What should I see?
The build should complete without compiler errors. The existing unit test should pass.
- Start the repaired service by running:
npm start
What should I see?
The process should stay running. You should see delivery-service listening on http://localhost:3000 in the terminal.
- Return to the browser window from the baseline check.
- Navigate to http://localhost:3000/health.
You should see {"service":"delivery-service","status":"ready"}. This confirms that the repaired service preserves the health response.
- Stop the local service with Ctrl+C.
- Select RELEASE_DECISION.md in the Visual Studio Code Explorer sidebar.
- Confirm that the decision remains STOP BEFORE RELEASE.
- Confirm that Gate 4 is recorded as incomplete.
You restored startup without turning partial evidence into permission to ship. The release remains safely stopped with baseline-express-4 preserved as its rollback target.
Secret mission
Expose the Wildcard-Array Contract Regression
The stopped release still lacks Gate 4 evidence. You will expose Express 5's wildcard-array behavior with a deterministic test. One bounded repair then earns an evidence-backed ship decision.
Clean Up Your Resources
Clean Up Your Resources
Everything you created stays on your Mac, so there are no ongoing cloud costs. Decide whether to keep the repository active, pause its local processes, or delete it entirely.
Resources you used:
- The local Git repository stored in claude-express-upgrade.
- The committed upgrade/express-5 branch.
- The immutable baseline-express-4 rollback tag.
- The installed dependencies under node_modules/.
- The compiled output under dist/.
- The local service process if it is still running.
- The interactive Claude Code session if it is still open.
Keep everything running
No action needed. Choose this if you want to keep testing the completed upgrade.
- Keep the claude-express-upgrade repository to preserve the complete evidence trail.
- Preserve the upgrade/express-5 branch to continue from the final green checkpoint.
- Retain the baseline-express-4 tag as the immutable rollback target.
- Leave the local service running if you are still testing its endpoints.
- Leave Claude Code open if you are continuing the session.
Pause - I'll come back to this later
Shut down the local processes to free up memory. Your repository remains available when you return.
- Stop the local service by pressing Ctrl+C in its terminal if it is running.
- Exit Claude Code by entering /exit if the session is open.
- Close Visual Studio Code to free its memory.
Your claude-express-upgrade repository stays on disk with its full Git history.
Delete - I don't want to use this again
Remove the local repository when you no longer need its evidence trail. Deletion is final because no remote copy was pushed.
- Stop the local service by pressing Ctrl+C in its terminal if it is running.
- Exit Claude Code by entering /exit if the session is open.
- Close Visual Studio Code.
- Use Finder to locate the claude-express-upgrade folder.
- Delete the claude-express-upgrade folder from Finder.
- Confirm the folder no longer appears in the Finder location where it was stored.
This one folder deletion removes every local project artifact listed above.
Nice Work!
Nice Work!
You did it! You completed a gated upgrade to Express version 5.2.1 with Claude Code. Your evidence trail now supports REPAIR AND SHIP under criteria fixed before execution.
You've learned how to:
- Establish a green Express baseline on version 4.22.2. Commit ordered verification gates before the risky upgrade begins. Preserve baseline-express-4 as the immutable Git rollback target.
- Use Plan Mode to inspect the repository without changing source files. Approve a bounded Claude Code campaign. Observe the build gate turn green. Observe the narrow unit gate turn green. Let route registration expose the deterministic runtime failure.
- Authorize one bounded startup repair from the original criteria. Keep the main-path decision at STOP BEFORE RELEASE while contract evidence is missing. Record each checkpoint in RELEASE_DECISION.md.
- Secret Mission: Add a deterministic fallback-contract test. Expose the wildcard capture array. Preserve the baseline string response with one localized compatibility repair. Update the final decision to REPAIR AND SHIP only after every gate passes.
Ready to quiz yourself?