Reconcile Claude Code Worktrees

Build a release dashboard while reconciling parallel Claude Code worktrees.

Introduction

30 Second Summary

Dividing a big job between people feels efficient until their pieces have to fit together. Each piece can work alone while the combined result quietly contradicts itself.

In this project, you will build Release Radar, a local dashboard that puts the riskiest releases first. Separate Claude Code worktrees will expose a shared contract failure inside a clean Git merge before tests guide the reconciliation.

What You'll Build

You will open Release Radar in Safari to demo urgent-first release cards beside a matching readiness summary.

By the end of this project, you'll have:

  • A working Release Radar dashboard where you can see the highest-risk release first beside an accurate readiness summary.
  • A parallel worktree workflow that gives each Claude Code session its own checkout on an independent branch.
  • A reconciled risk contract protected by a five-test suite. Its architecture decision record explains why the clean merge failed.
  • Secret Mission: Force a textual merge conflict by assigning two worktrees the same CSS line. Resolve it while preserving stronger card depth plus hover motion.

Are there any prerequisites?

You need a Claude subscription that includes Claude Code access. No prior worktree experience is required.

Before We Start

Before the hands-on work begins, lock in what you are building and why it matters. This commitment gives every worktree experiment a clear engineering purpose.

Verify the Toolchain and Create the Repository

Release Radar eventually grows through parallel Claude Code sessions. Those sessions are only useful when every worker starts with compatible tools.

A trusted Git repository gives each worktree the same starting point. In this step, you will verify the toolchain before creating that empty foundation.

In this step, get ready to:
  • Verify the required tool versions.
  • Create an empty repository on the main branch.
  • Confirm the main checkout is the only worktree.
Verify the toolchain

The macOS Terminal gives you one place to check every command-line tool. The project expects Claude Code 2.1.258 or later.

Release Radar uses Node.js v24.20.0 LTS. Git also needs to respond before you create the repository.

  • Press Cmd+Space to open macOS search.
  • Type Terminal and press Enter to open it.
  • Update Claude Code before collecting all three tool checks by running these commands:
claude update
claude --version
claude doctor
node --version
git --version

What Do These Checks Prove?

  • Claude Code updates itself before reporting its installed version.
  • The health check inspects the Claude Code installation.
  • The health check also inspects its settings.
  • Node.js prints the runtime version available to Release Radar.
  • Git prints the version that manages the repository.
  • Choose the tab that matches the output from your tool checks.

✔️ Both Tools Meet the Requirements

Claude Code reports 2.1.258 or later. Its health check completes without a blocking problem.

Node.js reports v24.20.0. Git also prints an installed version.

ⓧ Node.js Is Older Than Required

An older Node.js runtime can behave differently when you run the later tests. The official macOS package installs the required LTS release.

  • Download Node.js v24.20.0 using the official macOS installer.
  • Open the downloaded node-v24.20.0.pkg file from Finder.
  • Complete the installer prompts.
  • Close the current Terminal window.
  • Press Cmd+Space to open macOS search.
  • Type Terminal and press Enter to reopen it.
  • Verify the installed Node.js version by running this command:
node --version

What Does This Check Prove?

The new Terminal session loads the installed runtime. The version check confirms which Node.js release your shell can use.

You should now see v24.20.0.

ⓧ Claude Code Is Older or Unhealthy

Claude Code needs version 2.1.258 or later. Its diagnostics must also complete without a blocking installation problem.

  • Refresh Claude Code before rerunning its checks with these commands:
claude update
claude --version
claude doctor

What Does This Retry Do?

The update check retrieves the current Claude Code release. The following checks confirm its version and installation health.

Still Have a Toolchain Problem?

Review the Claude Code health-check output for the component that needs attention. Use the official setup guide if the installation needs repair.

Need help interpreting the result? Help me diagnose my Claude Code, Node.js, or Git tool check.

Your toolchain is now ready to support the repository.

Create the repository

The initial main branch becomes the shared starting point for every later feature branch. Creating it on your Desktop also gives the project a predictable location.

  • Move to your Desktop by running this command:
cd ~/Desktop

What Does This Command Do?

The cd command changes Terminal's current location. Your new repository will appear in the Desktop folder.

  • Create the repository and enter its directory by running these commands:
git init -b main parallel-release-lab
cd parallel-release-lab

What Did Git Create?

Git creates the parallel-release-lab directory with an empty repository on main. The second command makes that directory active in Terminal.

The repository now contains Git metadata only. No project files have been created or committed.

Claude Code opens an interactive session inside the repository. A workspace trust request may appear because this is its first session in the directory.

  • Start Claude Code once inside parallel-release-lab by running this command:
claude

Why Start Claude Code Now?

Workspace trust is required before an interactive worktree session can launch. Starting Claude Code now clears that prerequisite before parallel work begins.

  • Accept the workspace trust request if it appears.
  • Press Control+C to exit the session before creating any files.

Claude Code now recognizes the trusted repository. The repository still has no project files.

Repository Setup Not Working?

If Git cannot initialize the repository, confirm Terminal is on your Desktop before retrying the repository commands. If Claude Code does not start, review the diagnostics from the previous substep.

Need help with the setup? Help me diagnose why my empty Git repository or Claude Code trust check is failing.

Prove Git worktree support

A worktree is a linked checkout with its own files and branch. At this point, Git should know only about the main checkout you just created.

Before you run the check, predict whether Git knows about any linked worktrees yet.

  • List every registered worktree by running this command:
git worktree list

What Does This Command Show?

Git lists the main checkout plus any linked worktrees registered to the repository. A new repository has only its original checkout.

The output should contain one line for parallel-release-lab. That line should identify main as its branch.

That proves your version-checked main checkout is ready for future worktree branches.

See More Than One Worktree?

Confirm you ran the command from the new parallel-release-lab directory. Another repository can have its own linked worktrees.

Need help reading the list? Help me interpret my Git worktree output.

Your version-checked main checkout is ready. Next, you will build the first visible Release Radar baseline.

Build the Baseline Release Radar

Your verified toolchain now sits inside an empty Git repository. That clean starting point gives every later worktree the same history.

A visible baseline gives both feature agents a shared reference point. This step creates a deliberately incomplete dashboard so each later branch can solve one feature.

In this step, get ready to:
  • Ask Claude to generate the baseline Release Radar files.
  • Run the baseline contract test and inspect the baby dashboard.
  • Commit the shared baseline on main.
Ask Claude to create the baseline

The baseline needs enough working code to produce a visible dashboard. Two feature functions remain deliberately incomplete so the parallel sessions have focused jobs later.

What makes this a baseline?

The shared risk contract already defines the supported levels. The sorter preserves source order while the summary function returns no data.

The browser understands both future features from the start. Later branches can change their assigned modules without editing the server or browser files.

  • Review each file-edit permission from Claude Code before approving it.
  • Start Claude Code in the trusted main checkout by running this command:
claude

What does this command do?

This starts Claude Code inside the trusted parallel-release-lab checkout. Claude can now create the baseline files in the current repository.

  • Ask Claude to create the baseline by entering this prompt:
Create the baseline Release Radar in this repository.

Use only Node.js built-in modules. Do not install packages.

Create .gitignore, config/local.json, config/local.json.example, server.js, public/index.html, public/app.js, public/styles.css, src/releases.js, src/release-contract.js, src/sort-releases.js, src/release-summary.js, and test/release-contract.test.js.

Set .gitignore to ignore config/local.json, .claude/worktrees/, and .DS_Store.

Set both config files to host 127.0.0.1, port 4173, and environment portfolio-demo.

Define RISK_LEVELS as Object.freeze(['low', 'medium', 'high']). Export RISK_LEVELS and riskRank.

Create the four releases REL-103, REL-102, REL-104, and REL-101 in the source order from the project specification.

Make sortByRisk return a copied array without sorting it.

Make summarizeReleases return null.

Create one contract test that checks the supported risk-level set contains low, medium, and high. Do not test the array order yet.

Build the Node HTTP server and browser dashboard so the page can already display future sorting and summary results.

A null summary must hide the summary section and display Summary pending.

Do not implement urgent-first sorting or the summary calculation. Do not commit the files.

What does this prompt create?

  • The server reads the ignored local configuration before listening on the configured host and port.
  • The browser renders four release cards from the API response.
  • The sorter returns a new array without changing its order.
  • The summary stub returns null so the browser displays Summary pending.

Use the tabs below to confirm Claude created the intended baseline.

✔️ Awesome, I've got everything!

Your baseline files are ready. That is the first working slice of Release Radar in place.

ⓧ I'd like to double check the full code

  • Compare each generated file with the reference below.
  • Ask Claude to correct any file that differs from this baseline.
config/local.json
.claude/worktrees/
.DS_Store

Why ignore these files?

The local configuration belongs to one checkout. Claude-created worktrees also stay outside the committed project history.

{
  "host": "127.0.0.1",
  "port": 4173,
  "environment": "portfolio-demo"
}

What does the local configuration control?

This ignored file tells the server where to listen. Keeping it local lets each future worktree use a different port.

{
  "host": "127.0.0.1",
  "port": 4173,
  "environment": "portfolio-demo"
}

Why track an example configuration?

The example documents the required fields without tracking the local copy. Another developer can use it to recreate the expected configuration.

const releases = [
  {
    id: 'REL-103',
    name: 'Search relevance tuning',
    owner: 'Discovery',
    risk: 'medium',
    status: 'ready',
    checks: '6/6',
  },
  {
    id: 'REL-102',
    name: 'Profile image compression',
    owner: 'Identity',
    risk: 'low',
    status: 'ready',
    checks: '5/5',
  },
  {
    id: 'REL-104',
    name: 'Billing retry policy',
    owner: 'Payments',
    risk: 'high',
    status: 'blocked',
    checks: '5/7',
  },
  {
    id: 'REL-101',
    name: 'Mobile checkout telemetry',
    owner: 'Commerce',
    risk: 'medium',
    status: 'blocked',
    checks: '4/6',
  },
];

module.exports = { releases };

What does the release data represent?

This array supplies the four releases shown on the dashboard. Its initial order makes the missing urgent-first behavior visible.

const RISK_LEVELS = Object.freeze(['low', 'medium', 'high']);

function riskRank(risk) {
  const rank = RISK_LEVELS.indexOf(risk);

  if (rank === -1) {
    throw new Error(`Unknown risk level: ${risk}`);
  }

  return rank;
}

module.exports = { RISK_LEVELS, riskRank };

What does the risk contract define?

The frozen array defines the supported values in ascending severity. The riskRank() helper converts each supported value into its array position.

const { riskRank } = require('./release-contract');

function sortByRisk(releases) {
  return [...releases];
}

module.exports = { sortByRisk };

Why copy without sorting?

The copied array protects the source data from mutation. Its unchanged order leaves urgent-first sorting as a later feature.

function summarizeReleases(releases) {
  return null;
}

module.exports = { summarizeReleases };

Why return null?

The null result represents a summary that has not been calculated. The browser converts that deliberate partial state into the pending message.

const http = require('node:http');
const fs = require('node:fs');
const path = require('node:path');
const { releases } = require('./src/releases');
const { sortByRisk } = require('./src/sort-releases');
const { summarizeReleases } = require('./src/release-summary');

const ROOT = __dirname;

function readText(relativePath) {
  return fs.readFileSync(path.join(ROOT, relativePath), 'utf8');
}

function send(response, statusCode, contentType, body) {
  response.writeHead(statusCode, { 'Content-Type': contentType });
  response.end(body);
}

let localConfig;

try {
  localConfig = JSON.parse(readText('config/local.json'));
} catch (error) {
  console.error(
    'Missing or invalid config/local.json. Copy config/local.json.example and try again.',
  );
  process.exit(1);
}

const assetRoutes = {
  '/': ['public/index.html', 'text/html; charset=utf-8'],
  '/app.js': ['public/app.js', 'text/javascript; charset=utf-8'],
  '/styles.css': ['public/styles.css', 'text/css; charset=utf-8'],
};

const server = http.createServer((request, response) => {
  if (request.url === '/api/dashboard') {
    const orderedReleases = sortByRisk(releases);
    const summary = summarizeReleases(orderedReleases);

    send(
      response,
      200,
      'application/json; charset=utf-8',
      JSON.stringify({
        environment: localConfig.environment,
        releases: orderedReleases,
        summary,
      }),
    );
    return;
  }

  const asset = assetRoutes[request.url];

  if (asset) {
    send(response, 200, asset[1], readText(asset[0]));
    return;
  }

  send(response, 404, 'text/plain; charset=utf-8', 'Not found');
});

server.listen(localConfig.port, localConfig.host, () => {
  console.log(
    `Release Radar running on ${localConfig.host}:${localConfig.port}`,
  );
});

How does the server connect the app?

The server reads the local configuration before serving browser assets. Its dashboard endpoint passes release data through both feature functions.

This wiring lets later branches replace the stubs without changing the server.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Release Radar</title>
    <link rel="stylesheet" href="/styles.css">
  </head>
  <body>
    <main class="shell">
      <header class="hero">
        <p class="eyebrow">Agentic delivery portfolio</p>
        <h1>Release Radar</h1>
        <p>See the riskiest releases first and check whether the train is ready.</p>
        <span id="environment" class="environment">Loading environment...</span>
      </header>

      <section id="summary" class="summary" hidden>
        <h2>Release summary</h2>
        <div id="summary-grid" class="summary-grid"></div>
      </section>

      <section aria-labelledby="release-heading">
        <div class="section-heading">
          <h2 id="release-heading">Release train</h2>
          <span id="status-message">Loading releases...</span>
        </div>
        <div id="release-list" class="release-list"></div>
      </section>
    </main>

    <script src="/app.js"></script>
  </body>
</html>

What structure does the page provide?

The page includes an environment badge plus a hidden summary section. It also provides the release list that the browser script fills with cards.

const environment = document.querySelector('#environment');
const summary = document.querySelector('#summary');
const summaryGrid = document.querySelector('#summary-grid');
const releaseList = document.querySelector('#release-list');
const statusMessage = document.querySelector('#status-message');

function createMetric(label, value) {
  const metric = document.createElement('article');
  metric.className = 'metric';

  const metricValue = document.createElement('strong');
  metricValue.textContent = value;

  const metricLabel = document.createElement('span');
  metricLabel.textContent = label;

  metric.append(metricValue, metricLabel);
  return metric;
}

function renderSummary(data) {
  if (!data) {
    summary.hidden = true;
    statusMessage.textContent = 'Summary pending';
    return;
  }

  const metrics = [
    ['Total', data.total],
    ['Ready', data.ready],
    ['Blocked', data.blocked],
    ['Highest risk', `${data.highestRiskLevel} (${data.highestRiskCount})`],
  ];

  summaryGrid.replaceChildren(
    ...metrics.map(([label, value]) => createMetric(label, String(value))),
  );
  summary.hidden = false;
}

function renderReleases(releases) {
  const cards = releases.map((release) => {
    const card = document.createElement('article');
    card.className = `release-card risk-${release.risk}`;

    const heading = document.createElement('div');
    heading.className = 'card-heading';

    const title = document.createElement('h3');
    title.textContent = release.name;

    const risk = document.createElement('span');
    risk.className = 'risk-badge';
    risk.textContent = release.risk;

    heading.append(title, risk);

    const details = document.createElement('p');
    details.textContent = `${release.id} · ${release.owner}`;

    const checks = document.createElement('p');
    checks.className = 'checks';
    checks.textContent = `${release.checks} checks · ${release.status}`;

    card.append(heading, details, checks);
    return card;
  });

  releaseList.replaceChildren(...cards);
  statusMessage.textContent = `${releases.length} releases loaded`;
}

async function loadDashboard() {
  try {
    const response = await fetch('/api/dashboard');

    if (!response.ok) {
      throw new Error(`Dashboard request failed: ${response.status}`);
    }

    const data = await response.json();
    environment.textContent = data.environment;
    renderSummary(data.summary);
    renderReleases(data.releases);
  } catch (error) {
    statusMessage.textContent = error.message;
  }
}

loadDashboard();

How does the browser render the data?

The script fetches the dashboard response before creating release cards. A null summary keeps the summary section hidden while exposing the deliberate pending state.

:root {
  color-scheme: light;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
  color: #182033;
  background: #f4f6fb;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100vh;
  background:
    radial-gradient(circle at top right, #dce6ff 0, transparent 34%),
    #f4f6fb;
}

.shell {
  width: min(1080px, calc(100% - 32px));
  margin: 0 auto;
  padding: 56px 0 72px;
}

.hero {
  margin-bottom: 32px;
}

.eyebrow {
  margin: 0 0 8px;
  color: #5967d8;
  font-size: 0.78rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

h1,
h2,
h3,
p {
  margin-top: 0;
}

h1 {
  margin-bottom: 10px;
  font-size: clamp(2.5rem, 7vw, 5rem);
  line-height: 0.95;
}

.hero > p:not(.eyebrow) {
  max-width: 650px;
  color: #596176;
  font-size: 1.1rem;
}

.environment {
  display: inline-flex;
  padding: 7px 11px;
  border: 1px solid #cdd5ef;
  border-radius: 999px;
  background: #ffffff;
  color: #47516b;
  font-size: 0.85rem;
}

.summary {
  margin-bottom: 36px;
  padding: 24px;
  border: 1px solid #dfe4f2;
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.85);
}

.summary-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 14px;
}

.metric {
  padding: 16px;
  border-radius: 14px;
  background: #eef1fb;
}

.metric strong,
.metric span {
  display: block;
}

.metric strong {
  margin-bottom: 4px;
  font-size: 1.45rem;
}

.metric span,
.section-heading span,
.release-card p {
  color: #667087;
}

.section-heading {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
}

.release-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 18px;
}

.release-card {
  padding: 22px;
  border: 1px solid #dfe4f2;
  border-left: 5px solid transparent;
  border-radius: 18px;
  background: #ffffff;
  box-shadow: 0 8px 24px rgba(36, 50, 92, 0.12);
}

.release-card.risk-high {
  border-left-color: #d6485f;
}

.release-card.risk-medium {
  border-left-color: #d99022;
}

.release-card.risk-low {
  border-left-color: #2f9b73;
}

.card-heading {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}

.card-heading h3 {
  margin-bottom: 12px;
}

.risk-badge {
  padding: 5px 9px;
  border-radius: 999px;
  background: #eef1fb;
  font-size: 0.75rem;
  font-weight: 800;
  text-transform: uppercase;
}

.checks {
  margin-bottom: 0;
  font-weight: 700;
}

What does the baseline styling establish?

The stylesheet turns the release data into a responsive card dashboard. Each risk level receives a distinct border color.

The card has one base shadow. It has no hover behavior at this stage.

const test = require('node:test');
const assert = require('node:assert/strict');
const { RISK_LEVELS, riskRank } = require('../src/release-contract');

test('the contract contains every supported risk level', () => {
  assert.deepEqual(new Set(RISK_LEVELS), new Set(['low', 'medium', 'high']));
});

What does the baseline test protect?

This test confirms that all three supported values exist. It deliberately leaves their canonical order unspecified.

  • Exit Claude Code after the generated files match the baseline reference.
Run the baby version

The built-in Node.js test runner checks the contract without adding dependencies. This first test should pass even though sorting and summary behavior are unfinished.

Before you run the test, do you expect the partial feature functions to prevent the contract check from passing?

  • Run the baseline contract test with this command:
node --test

What does this command check?

The Node.js test runner discovers the test inside test/release-contract.test.js. The test checks the supported values without requiring either future feature.

You should see one passing test. That confirms the shared risk vocabulary is already working.

Test not passing?

Check that test/release-contract.test.js imports from ../src/release-contract.

Confirm that RISK_LEVELS contains low, medium, and high.

Still stuck? Help me diagnose my failing baseline Node.js contract test.

The server reads config/local.json before exposing the dashboard. Its running process keeps the current Terminal window occupied until you stop it.

Before you start the server, which release do you expect to appear first when the sorter preserves source order?

  • Start the local Release Radar server by running this command:
node server.js

What does this command start?

The server loads the ignored local configuration before listening on 127.0.0.1 at port 4173. It serves the dashboard assets plus the local dashboard endpoint.

  • Press Cmd+Space to open Spotlight.
  • Type Safari into Spotlight.
  • Press Enter to open Safari.
  • Press Cmd+L to focus the address bar.
  • Enter http://127.0.0.1:4173 to load Release Radar.

You should see four cards in this order: Search relevance tuning, Profile image compression, Billing retry policy, then Mobile checkout telemetry.

The status area should display Summary pending. This visible gap proves the summary feature has not been implemented.

Dashboard not loading?

Confirm that the Terminal still shows the running server process. A returned command prompt means the server stopped or failed to start.

Check that config/local.json exists inside the config folder. Confirm that its host and port match the browser address.

Need help? Help me troubleshoot why my local Release Radar dashboard is not loading.

  • Return to the Terminal running the server.
  • Press Control-C to stop the local server.
Commit the shared baseline

A commit gives both future feature branches the same starting point. Git should record the source files while leaving the ignored local configuration outside the commit.

Before you commit, what do you expect the final status check to show if every tracked baseline file is saved?

  • Stage the tracked baseline files, create the commit, and inspect the branch state by running these commands:
git add .
git commit -m "Build baseline release dashboard"
git status --short --branch

What do these commands record?

The first command stages new files except those ignored by .gitignore. The second command records the baseline with the required message.

The final command shows the current branch plus any remaining changes. A clean result proves the tracked baseline is committed.

You should see main as the current branch with no staged or unstaged file entries. The ignored config/local.json should stay absent from the status output.

Unexpected files in the status output?

Check that .gitignore contains config/local.json on its own line. Confirm that the file name matches the path exactly.

If tracked files still appear as changed, confirm that Claude finished saving each file before you created the commit.

Need a second pair of eyes? Help me understand why my baseline Git status is not clean.

Your commit is now the baseline that every later worktree inherits. One final browser run proves the committed main checkout still serves the intended partial state.

Before you run the server again, do you expect the commit to change the card order or pending summary?

  • Start the committed baseline for the final check by running this command:
node server.js

What does the final run prove?

This run starts the same server from the committed main checkout. The ignored local configuration still supplies its host and port.

  • Return to Safari from earlier.
  • Refresh http://127.0.0.1:4173.

You should still see four source-order cards with Summary pending. That confirms the committed baby dashboard matches the intended baseline.

  • Return to the Terminal running the server.
  • Press Control-C to stop the server.

Your visible baseline is committed and ready for parallel work. Next, you will discover why a fresh worktree cannot automatically see this checkout's ignored local configuration.

Provision the Worktree Environment

Your committed baseline now runs from main. Its four release cards remain in source order.

Claude Code uses Git worktree isolation to give each worker a separate checkout. A parallel worker still needs a complete local environment.

In this step, get ready to:
  • Reproduce the missing local configuration failure in a disposable worktree.
  • Commit a rule that provisions the ignored configuration.
  • Launch two feature worktrees with independent local ports.
Reproduce the missing configuration failure

A disposable probe reveals what a fresh checkout receives. The Node.js server turns any missing requirement into an immediate result.

  • Create the disposable probe worktree from the main checkout by running this command:
claude --worktree config-probe

What does this command create?

  • Claude Code creates a linked checkout under .claude/worktrees/.
  • The new checkout uses the branch worktree-config-probe.
  • The session starts from the committed baseline on main.

Before you test the server, do you think this fresh checkout has every local file the baseline needs?

  • Ask Claude to test the baseline server inside the probe worktree by running this command:
node server.js

The missing configuration is intentional

You'll see Missing or invalid config/local.json. Copy config/local.json.example and try again..

The server exits because the fresh checkout did not receive the ignored config/local.json file. This is the environment gap you needed to expose.

Did the probe server start instead?

  • Confirm the active Claude Code session uses the branch worktree-config-probe.
  • Check whether config/local.json already exists inside the probe checkout.

Still unsure? Help me trace how config/local.json reached the probe worktree.

Removing this clean probe deletes only its linked checkout. Your committed baseline on main remains untouched.

  • Exit the probe Claude Code session.
  • Choose to remove the clean named worktree when Claude Code prompts you.

You have now reproduced the environment gap firsthand. The disposable probe is removed before it can clutter the repository.

Provision ignored configuration

The .worktreeinclude file tells Claude Code which gitignored files to copy into new worktrees. A tracked rule makes the required local configuration available to every future worker.

  • Switch back to the trusted Claude Code workspace in the main parallel-release-lab checkout.
  • Ask Claude to create .worktreeinclude inside parallel-release-lab with exactly this content:
config/local.json

What does this file do?

  • The path selects config/local.json for copying into new Claude-created worktrees.
  • Each worktree receives its own local copy.
  • Git continues to ignore every copied configuration file.
  • Confirm Claude saved .worktreeinclude with exactly one line.

Configuration rule looks different?

  • Check that the filename starts with a dot.
  • Remove any content other than config/local.json.

Need another pair of eyes? Help me verify my worktree provisioning rule.

✔️ Awesome, I've got everything!

Your provisioning rule contains the exact ignored path that future worktrees need.

ⓧ I'd like to double check the full code

config/local.json

What should the full file contain?

The complete file contains one path. No other ignored files are copied into Claude-created worktrees.

  • Commit the provisioning rule on main by running these commands:
git add .worktreeinclude
git commit -m "Provision local config into Claude worktrees"

What does this commit protect?

  • The first command stages the provisioning rule.
  • The second command records the rule in the shared repository history.
  • Every feature branch created next inherits this environment policy.

Good work. Git should report a new commit named Provision local config into Claude worktrees.

Commit did not complete?

  • Confirm .worktreeinclude exists in the main parallel-release-lab checkout.
  • Check that the file contains config/local.json.

Still blocked? Help me diagnose why Git did not commit .worktreeinclude.

Create two usable feature worktrees

The provisioning commit becomes the shared starting point for both feature workers. Each copied configuration can use a different local port.

  • Open a second Terminal window in the main parallel-release-lab checkout.

Each launch can request workspace trust. The linked session starts after you approve the repository.

  • Launch the sorting feature session in the first Terminal window by running this command:
claude --worktree urgent-sort

What does the sorting launch create?

  • Claude Code creates a linked checkout for urgent-sort.
  • The checkout uses the branch worktree-urgent-sort.
  • The branch descends from the provisioned baseline.
  • Approve the repository if Claude Code requests workspace trust.
  • Confirm the session identifies its branch as worktree-urgent-sort.

Urgent-sort session did not open?

  • Confirm the first Terminal window is in the main parallel-release-lab checkout.
  • Check that the provisioning commit exists on main.

Still blocked? Help me diagnose the urgent-sort worktree launch.

  • Launch the summary feature session in the second Terminal window by running this command:
claude --worktree release-summary

What does the summary launch create?

  • Claude Code creates a second linked checkout for release-summary.
  • The checkout uses the branch worktree-release-summary.
  • Both feature branches now share the same committed starting point.
  • Approve the repository if Claude Code requests workspace trust.
  • Confirm the session identifies its branch as worktree-release-summary.

Release-summary session did not open?

  • Confirm the second Terminal window is in the main parallel-release-lab checkout.
  • Check that the session name is release-summary.

Need help with the launch? Help me diagnose the release-summary worktree.

Both feature sessions now have isolated checkouts. The next checks prove that each checkout also received a runnable local environment.

  • Confirm config/local.json exists inside the urgent-sort worktree.
  • Ask the urgent-sort session to change the port value in config/local.json from 4173 to 4174.

Before you start the first server, which port should prove that urgent-sort has an independent configuration?

  • Ask the urgent-sort session to start its local server by running this command:
node server.js

What does the first server check prove?

  • The server reads the urgent-sort copy of config/local.json.
  • A successful launch proves .worktreeinclude provisioned the ignored file.
  • The separate port proves this local copy can change independently.

You'll see Release Radar running on 127.0.0.1:4174.

Urgent-sort is not using port 4174?

  • Confirm the edit was made inside the urgent-sort copy of config/local.json.
  • Check that the main checkout still uses port 4173.

Still seeing the wrong port? Help me trace the urgent-sort configuration.

  • Keep the urgent-sort server running for the side-by-side verification.
  • Confirm config/local.json exists inside the release-summary worktree.
  • Ask the release-summary session to change the port value in config/local.json from 4173 to 4175.

Before you start the second server, which port should distinguish release-summary from urgent-sort?

  • Ask the release-summary session to start its local server by running this command:
node server.js

What does the second server check prove?

  • The server reads the release-summary copy of config/local.json.
  • Its local configuration is separate from the urgent-sort copy.
  • Both feature workers can now run without competing for one port.

You'll see Release Radar running on 127.0.0.1:4175.

Release-summary is not using port 4175?

  • Confirm the edit was made inside the release-summary copy of config/local.json.
  • Check that urgent-sort still uses port 4174.

Need help separating the configurations? Help me verify the ports across all three checkouts.

  • Stop the urgent-sort server with Control-C.
  • Stop the release-summary server with Control-C.

That environment boundary is now secured. Next, the two agents can deliver separate features from the same provisioned baseline.

Deliver Features in Parallel

Your two Claude Code worktrees now have independent local configuration. Each worker can build from the same trusted baseline without changing the other checkout.

File isolation protects each agent's ownership boundary. The shared risk order remains underspecified, so each agent can make a locally reasonable choice that its own tests accept.

In this step, get ready to:
  • Delegate urgent-first sorting within a strict file boundary.
  • Delegate release-summary calculation within a separate file boundary.
  • Verify both branch results before comparing their history.
Launch urgent-first delivery

The sorting agent owns the shared risk contract because its feature depends on ranking each risk value. A narrow ownership boundary keeps its changes easy to inspect.

Before you send the brief, consider this question: which release risk should the sorting branch place first.

  • Return to the existing urgent-sort Claude Code session.
  • Give the agent its ownership boundary by pasting this brief:
You are working in the urgent-sort worktree.

Edit only src/release-contract.js, src/sort-releases.js, and test/sort-releases.test.js.

Implement urgent-first ordering.
Change RISK_LEVELS to ['high', 'medium', 'low'].
Sort copied releases with riskRank(a.risk) - riskRank(b.risk).
Add a unit test proving high-risk releases appear first from low, high, medium input.
Do not inspect or message the other worktree.
Run node --test.
Run node server.js and verify the dashboard on port 4174.
Stop the server after verification.
Commit the changes with Add urgent-first release sorting.
Start node server.js again and leave it running for my browser review.

Why This Boundary Matters

The agent can change the meaning of RISK_LEVELS because it also owns sortByRisk(). The branch reverses the array so an ascending comparator places high-risk releases first.

The browser files remain untouched. This keeps the branch focused on one feature interpretation.

  • Watch the session until Claude confirms that it is editing only the three named files.
  • Leave the Claude Code session running while the agent completes the brief.

Agent Reaching Outside Its Boundary?

Stop the proposed change if Claude tries to edit server.js or any file under public/. Restate the three allowed file paths from the brief.

Need help reviewing the proposed edits? Help me check whether my urgent-sort Claude Code session stayed inside its ownership boundary.

Launch release-summary delivery

The summary agent receives a separate ownership boundary. It consumes the existing risk contract without editing that contract.

This branch treats the final item in the baseline risk array as the highest severity. Its local test checks that interpretation directly.

  • Switch back to the existing release-summary Claude Code session.
  • Give the second agent its separate ownership brief by pasting:
You are working in the release-summary worktree.

Edit only src/release-summary.js and test/release-summary.test.js.

Treat RISK_LEVELS as ascending severity.
Derive the highest level with RISK_LEVELS[RISK_LEVELS.length - 1].
Calculate total, ready, blocked, highest-risk level, and highest-risk count.
Add a unit test proving the highest-risk level is high.
Do not inspect or message the other worktree.
Run node --test.
Run node server.js and verify the dashboard on port 4175.
Stop the server after verification.
Commit the changes with Add release readiness summary.
Start node server.js again and leave it running for my browser review.

How the Summary Interprets Risk

The summary reads RISK_LEVELS[RISK_LEVELS.length - 1] as the highest supported level. The unchanged baseline order makes that value high inside this worktree.

The summary test proves the branch is internally consistent. It does not compare this interpretation with the sorting branch.

  • Watch the session until Claude confirms that it is editing only the two named files.
  • Leave the release-summary session running while the agent completes the brief.

Summary Agent Editing Shared Files?

Stop the proposed change if Claude tries to edit src/release-contract.js. Remind it that this branch can only consume the existing contract.

Need help checking the file boundary? Help me review whether my release-summary session edited only its two owned files.

Verify both isolated results

Each agent now has an independently tested feature commit. You will check the visible result from each branch before inspecting their shared ancestry.

Before you compare the pages, consider this question: which visible result belongs to each port.

  • Return to the urgent-sort session.
  • Review the Node.js test output from node --test.

You'll see every test in the urgent-sort worktree pass. The session also creates the Add urgent-first release sorting commit.

  • In Safari, enter http://127.0.0.1:4174 in the address bar.

You'll see Billing retry policy as the first card because it has high risk. The summary area still says Summary pending in this branch.

  • Switch back to the release-summary session.
  • Review its test output from node --test.

You'll see every test in the release-summary worktree pass. The session also creates the Add release readiness summary commit.

  • Open a new tab in Safari.
  • Enter http://127.0.0.1:4175 in the address bar.

You'll see a release summary with four total releases. It reports two ready releases, two blocked releases, and high (1) as the highest risk.

Both branches now work on their own terms. You have two visible features delivered from isolated sessions without either agent crossing its file boundary.

One of the Dashboards Missing?

Check that the urgent-sort server uses port 4174. Check that the release-summary server uses port 4175.

If a session stopped its server, ask that session to run node server.js again from its existing worktree.

Still stuck? Help me diagnose why one of my isolated Release Radar dashboards is unavailable.

  • Return to the urgent-sort session and press Control-C to stop its server.
  • Return to the release-summary session and press Control-C to stop its server.

Passing branch tests prove that each feature works with its own checkout's assumptions. Git can now show whether the agents respected their ownership boundaries.

Before you run the comparison, consider this question: will the file lists overlap even though both features depend on the risk contract.

  • Switch back to the Terminal window for the main checkout.
  • Compare both feature branches with their common ancestor by running:
git diff --stat main...worktree-urgent-sort
git diff --stat main...worktree-release-summary
git log --graph --oneline --all

What Does This Comparison Show?

The first two commands summarize the files changed on each branch since its shared ancestor with main. The final command displays the commit graph across every branch.

Together, these views check file ownership and branch ancestry. They do not test whether the two feature interpretations work together.

The urgent-sort diffstat lists only src/release-contract.js, src/sort-releases.js, and test/sort-releases.test.js. The release-summary diffstat lists only src/release-summary.js and test/release-summary.test.js.

The graph shows Add urgent-first release sorting and Add release readiness summary on separate branches. Both branches descend from the shared baseline plus Provision local config into Claude worktrees.

Why Can Both Suites Pass?

The urgent-sort branch defines the first risk value as the most severe. Its ascending comparator therefore places high risk first.

The release-summary branch keeps the baseline ascending order. It reads the final risk value as the highest severity.

Each suite validates one interpretation in isolation. The two interpretations have not been combined.

Branch Missing From the Graph?

Return to the affected Claude Code session if its feature commit is missing. Confirm that Claude completed the exact commit message from its brief.

Need help reading the history? Help me interpret my Git graph and find a missing feature branch or commit.

Your two feature branches are committed, tested, and visibly correct in isolation. Next, you'll merge them and test whether their separate assumptions compose into one working system.

Reconcile the Shared Risk Contract

Your two isolated Claude Code worktrees now contain independently tested features. Each branch passes its own tests.

A clean Git merge only reconciles file changes. It cannot reconcile two meanings of the same shared contract.

In this step, get ready to:
  • Merge both feature branches to expose the semantic failure.
  • Define one canonical risk order for every consumer.
  • Verify the dashboard with five passing tests.
Merge cleanly and expose the semantic failure

Git decides whether changed lines overlap. The test suite decides whether the combined behavior still agrees.

Before you merge the branches, do you think a conflict-free merge guarantees that the combined dashboard works?

  • Return to the Terminal window for the main parallel-release-lab checkout.
  • Merge both feature branches and run the combined test suite by running:
git merge --no-ff worktree-urgent-sort -m "Merge urgent sort feature"
git merge --no-ff worktree-release-summary -m "Merge release summary feature"
node --test

This Failure Is the Lesson

Both merges complete without a textual conflict. The merged summary test then fails.

The sorting branch changed RISK_LEVELS to place high first. The summary branch reads the final array item as the highest risk.

Those assumptions combine to make the summary report low as the highest risk. The failure is expected.

Make the risk contract explicit

The failed test identifies the architectural gap. The risk array needs one stable meaning that every feature can rely on.

Reconciliation Requirements

  • The RISK_LEVELS contract uses the canonical ascending order of low, medium, high.
  • The sortByRisk() consumer expresses urgent-first presentation with a descending comparison.
  • The contract test locks the canonical order.
  • The integration test proves that the first card agrees with the summary.
  • The architecture decision record explains why file isolation did not protect the shared assumption.
  • The README.md file records the worktree lesson for future readers.
  • Start or resume Claude Code from the main parallel-release-lab checkout.
  • Ask Claude to diagnose the failed test as a shared-contract conflict.
  • Ask Claude to apply every reconciliation requirement shown above.

✔️ Awesome, I've got everything!

Your reconciled source files are ready for the final test run.

ⓧ I'd like to double check the full code

const RISK_LEVELS = Object.freeze(['low', 'medium', 'high']);

function riskRank(risk) {
  const rank = RISK_LEVELS.indexOf(risk);

  if (rank === -1) {
    throw new Error(`Unknown risk level: ${risk}`);
  }

  return rank;
}

module.exports = { RISK_LEVELS, riskRank };

Why This Contract Works

The frozen array defines one ascending severity order. riskRank() turns that order into a reusable numeric rank.

const { riskRank } = require('./release-contract');

function sortByRisk(releases) {
  return [...releases].sort(
    (a, b) => riskRank(b.risk) - riskRank(a.risk),
  );
}

module.exports = { sortByRisk };

Why the Sort Is Explicit

The comparator requests descending presentation without changing the shared contract. Higher numeric ranks now appear first.

const { RISK_LEVELS } = require('./release-contract');

function summarizeReleases(releases) {
  const highestRiskLevel = RISK_LEVELS[RISK_LEVELS.length - 1];

  return {
    total: releases.length,
    ready: releases.filter((release) => release.status === 'ready').length,
    blocked: releases.filter((release) => release.status === 'blocked').length,
    highestRiskLevel,
    highestRiskCount: releases.filter(
      (release) => release.risk === highestRiskLevel,
    ).length,
  };
}

module.exports = { summarizeReleases };

Why the Summary Works

The summary reads the final canonical level as the highest severity. It counts releases against that stable value.

const test = require('node:test');
const assert = require('node:assert/strict');
const { RISK_LEVELS, riskRank } = require('../src/release-contract');

test('the contract contains every supported risk level', () => {
  assert.deepEqual(new Set(RISK_LEVELS), new Set(['low', 'medium', 'high']));
});

test('the canonical contract increases in severity', () => {
  assert.deepEqual(RISK_LEVELS, ['low', 'medium', 'high']);
  assert.ok(riskRank('high') > riskRank('medium'));
});

What the Contract Tests Protect

One test protects the supported values. The second test protects their architectural meaning.

const test = require('node:test');
const assert = require('node:assert/strict');
const { sortByRisk } = require('../src/sort-releases');

test('sortByRisk puts the most urgent release first', () => {
  const input = [
    { id: 'low', risk: 'low' },
    { id: 'high', risk: 'high' },
    { id: 'medium', risk: 'medium' },
  ];

  assert.deepEqual(
    sortByRisk(input).map((release) => release.id),
    ['high', 'medium', 'low'],
  );
});

What the Sorting Test Protects

This test locks the visible urgent-first order. It stays independent from the internal array direction.

const test = require('node:test');
const assert = require('node:assert/strict');
const { releases } = require('../src/releases');
const { summarizeReleases } = require('../src/release-summary');

test('summarizeReleases reports readiness and the highest risk', () => {
  assert.deepEqual(summarizeReleases(releases), {
    total: 4,
    ready: 2,
    blocked: 2,
    highestRiskLevel: 'high',
    highestRiskCount: 1,
  });
});

What the Summary Test Protects

This test protects the readiness totals. It also protects the meaning of the highest-risk level.

const test = require('node:test');
const assert = require('node:assert/strict');
const { releases } = require('../src/releases');
const { sortByRisk } = require('../src/sort-releases');
const { summarizeReleases } = require('../src/release-summary');

test('the first card agrees with the summary about highest risk', () => {
  const orderedReleases = sortByRisk(releases);
  const summary = summarizeReleases(orderedReleases);

  assert.strictEqual(summary.highestRiskLevel, 'high');
  assert.strictEqual(orderedReleases[0].risk, summary.highestRiskLevel);
});

Why the Integration Test Matters

The integration test exercises both features together. It catches disagreements that isolated unit tests can miss.

# ADR 001: Risk order is a canonical ascending contract

## Context

Two Claude Code worktrees implemented features from the same baseline. The sorting branch changed `RISK_LEVELS` to descending order so an ascending comparator displayed urgent releases first. The summary branch kept the original ascending interpretation and treated the final array item as the highest risk.

Both branches passed independently. Git then merged their non-overlapping files without a textual conflict, but the combined summary identified `low` as the highest risk.

## Decision

`RISK_LEVELS` is a canonical ascending severity contract: `low`, `medium`, `high`.

Consumers must express presentation direction explicitly. `sortByRisk` therefore uses a descending comparator without changing the contract.

## Consequences

- Unit tests lock the shared ordering.
- An integration test checks agreement between sorting and summary behavior.
- Worktree ownership protects files and branches, but contract ownership still requires explicit coordination.

What the Decision Record Captures

The record preserves the context behind the fix. Future contributors can see the decision before changing the contract.

# Release Radar

Release Radar is a dependency-free Node.js dashboard built through parallel Claude Code worktrees. It demonstrates a subtle agentic engineering failure: isolated branches can merge cleanly while their architectural assumptions conflict.

## Run locally

1. Copy `config/local.json.example` to `config/local.json` if the ignored local file is missing.
2. Run `node --test`.
3. Run `node server.js`.
4. Open `http://127.0.0.1:4173`.

## What the repository demonstrates

- Claude Code worktree isolation
- Gitignored configuration provisioning with `.worktreeinclude`
- Parallel feature ownership
- A clean Git merge followed by a semantic contract failure
- Contract reconciliation through unit and integration tests

See `docs/architecture-decision.md` for the failure analysis and decision.

What the README Communicates

The README gives portfolio users a repeatable local run path. It also states the engineering lesson demonstrated by the repository.

Verify and commit the reconciled system

The server keeps this Terminal window busy until you stop it with Control-C.

Before you run the reconciled system, do you think the first card and summary now agree about the highest risk?

  • Run the complete test suite and start Release Radar by running:
node --test
node server.js

What These Commands Verify

The first command checks the contract tests and both feature tests. It also checks their integration.

The second command serves the reconciled dashboard from the main checkout.

The test runner should report five passing tests. The server should report that Release Radar is running on the configured main port.

  • In Safari, visit http://127.0.0.1:4173.
  • Confirm the first release card has a high risk badge.
  • Confirm the summary reports high as the highest risk.
  • Return to the Terminal window running the server.
  • Stop the server by pressing Control-C.

You have reconciled both interpretations into one working system. The dashboard now proves that sorting and summary behavior share the same contract.

Dashboard Still Disagrees About Risk?

Check that RISK_LEVELS remains in ascending order inside src/release-contract.js.

Check that sortByRisk() subtracts the rank of a.risk from the rank of b.risk.

Still stuck? Help me diagnose why Release Radar sorting and summary behavior still disagree.

Before you create the reconciliation commit, what should the final status show once every change is recorded?

  • Commit the reconciled system and inspect the final repository state by running:
git add .
git commit -m "Reconcile shared risk contract"
git status --short --branch

What This Commit Records

The commit records the canonical contract. It includes the stronger tests and architecture documentation.

The final status check confirms that every intended change is committed on main.

That is the integration complete. Your main working tree is clean with five passing tests and a documented architectural decision.

Working Tree Still Shows Changes?

Review the listed paths for files that Claude created or changed outside the reconciliation requirements.

Confirm the server is stopped before checking whether a process is still using the local files.

Need help reading the result? Help me understand why my main working tree is not clean after the reconciliation commit.

Secret mission

Resolve Deliberate Overlapping Ownership

Give two agents ownership of the same CSS line. Integrate their competing card-shadow changes while preserving stronger depth and responsive hover motion.

Clean Up Your Resources

Clean Up Your Resources

Everything you created lives on your Mac. No cloud resources or paid external services are running, so there are no ongoing project costs.

Resources you used:

  • Local Git repository named parallel-release-lab with source files, tests, branch history, integration commits, local configuration, and the architecture decision record.
  • Claude Code linked worktrees under .claude/worktrees/ with gitignored configuration copies.

Keep everything running

No action needed. Choose this if you want to keep demonstrating Release Radar or reviewing its integration history.

  • Retain the parallel-release-lab repository with its commits, tests, documentation, and resolved dashboard.
  • Leave every remaining linked worktree under .claude/worktrees/ in place for future experiments.
  • Use the clean main branch whenever you want to demonstrate the reconciled dashboard.

Pause - I'll come back to this later

The final local server is already stopped. Pausing means closing any remaining sessions while preserving every repository file.

  • Check each Terminal window from earlier for a running local server.
  • Press Control-C in any Terminal window where a server is still running.
  • Return to each Claude Code session from earlier.
  • Exit the session.
  • Choose the option to keep its worktree if Claude Code prompts you.
  • Leave the parallel-release-lab folder in its current location.

Delete - I don't want to use this again

Deleting the repository removes its local branches, commits, configuration, and documentation from this Mac. The checks below protect any work you still want to keep.

  • Copy the parallel-release-lab folder to another location if you want a backup.
  • Return to the main checkout from earlier.
  • Confirm the final branch has no uncommitted work by running:
git status --short --branch

What Does This Check Prove?

  • The branch line confirms you are on main.
  • No changed or untracked file entries means your final work is committed.
  • Inspect every remaining linked worktree path by running:
git worktree list

How to Read the Worktree List

  • Each output line identifies one checkout path and its branch.
  • The main checkout stays in place until you delete the complete repository folder.
  • Replace <path> with one clean linked worktree path from the list.
  • Remove that linked worktree by running this template:
git worktree remove <path>

What Does This Command Remove?

  • The command unregisters one clean linked checkout from Git.
  • The clean-worktree requirement protects local changes from accidental deletion.

Worktree Refuses to Be Removed?

Return to that linked checkout. Save any work you want to keep before trying the removal again.

Ask for help checking why the linked worktree is not clean.

  • Repeat the removal command for every remaining linked worktree path.
  • Confirm only the main checkout remains by running:
git worktree list

What Should You See?

  • You should see only the main checkout in the output.
  • The fiddly part is complete. Your linked checkouts are gone while the main repository still holds the integrated history.
  • Press Cmd+Space to open macOS search.
  • Type Finder into the search field.
  • Press Enter to open Finder.
  • Locate the parallel-release-lab folder at the main checkout path shown in the worktree list.
  • Delete the parallel-release-lab folder in Finder.
  • Confirm the folder no longer appears at that location.

Your repository, local branches, worktree configuration, and project files are now removed from this Mac.

Nice Work!

Nice Work!

Strong finish! You turned parallel Claude Code worktrees into a tested Release Radar dashboard.

You've learned how to:

  • Build a local Release Radar dashboard with urgent-first cards. Verify that its readiness summary reports high as the highest risk.
  • Use Claude Code worktrees to create isolated feature branches. Provision gitignored local configuration through .worktreeinclude so each checkout can run on its own port.
  • Diagnose a clean Git merge that exposes incompatible assumptions about RISK_LEVELS. Protect the reconciled shared contract with unit tests plus an integration test. Document the decision in an architecture decision record.
  • Secret Mission: Resolve a deliberate textual merge conflict in public/styles.css. Preserve the stronger base shadow with lift-and-shadow hover feedback.

Ready to quiz yourself?