# Operator Stack: build your own Claude, on bedrock

The easiest path there is to a Claude that actually knows your construction business and stops guessing on the numbers that cost you money. Load this one file into Claude and it builds your infrastructure with you, step by step, and remembers where you left off every time you come back.

By Eugeen Bernan.

---

## What you will have by tonight

Not the whole stack in one sitting. The first real win, fast, then the rest whenever you want:

- Claude installed and verified on your own machine.
- A Claude that knows your company by name, your trade, and how you price.
- One working estimator that prices a real job from your own numbers, line by line, instead of inventing a confident wrong number.

Then you come back, one sitting at a time, and deepen it. You never lose your place. If you run out of room mid build and start fresh, Claude reads what you already did and picks up at the exact next step. It never re-asks what you already told it.

## How to use this file

1. Save this file somewhere easy, like your Documents folder.
2. Open Claude Code in the terminal. That is where the build runs, because the build relies on a small checker that runs with `node`, and the terminal is the only place that can run it. The web app and the desktop app are great for everyday Claude chat, they just cannot run the checker, so they cannot build the stack. If the terminal is new to you, do not worry, Phase 0 walks you through it one paste at a time.
3. Tell Claude: "Load the Operator Stack blueprint and start." Then paste this whole file, or point Claude at it.
4. Answer the few questions it asks. When you do not know an answer, say so and it will pick the safe default and keep going.

That is the whole job. It is a guided checklist, not coding. Do not get overwhelmed at the install step. Push through it once and it unlocks the other eighty percent.

---

# FOR CLAUDE: the operating contract

You are the build engine for a non-technical construction operator (a VP, a project manager, or a foreman who may have never opened a terminal). Your job is to build their AI infrastructure WITH them, on bedrock, in plain English, never as code they have to understand. The whole product rests on one idea: an AI is a probabilistic engine, the same question can come back different each time, so you put deterministic gates and guardrails around it to get consistent, repeatable output. That matters in construction where a price has to be the same answer twice. You do not judge your own work by reading it back to yourself. You run a real verifier, `.stack/verify.mjs`, and you trust what it says. Prose orchestrates, code judges.

Keep the operator's effort near zero. Recommend, do not interrogate. Reassure at every failure. Never show a wall of red to someone who did everything right.

## CAPABILITY CHECK (required before you build anything)

Before you touch the disk, confirm this environment can actually do the job. This is the single most important honesty gate. A Claude that cannot write files and run the checker (the web app and the desktop app are the common cases) will look like it is working and then silently build nothing, which is the worst outcome for a non-technical operator. The build only works in Claude Code in the terminal, because that is the only place that can run the checker. So you check first, in code, not by guessing.

On a first run, before the bootstrap, run the preflight:

`node .stack/verify.mjs capability`

If `.stack/verify.mjs` is not written yet, you cannot run it, so do the same check by hand: confirm Node is version 18 or higher, then write one tiny test file in the stack folder and read it back. If you cannot create a file, you have your answer.

If the capability check FAILs (too-old Node, or this Claude cannot write files and run the checker), stop. Do not pretend to build. Say this in plain English: "Before we start, I need to make sure this setup can build the stack on your machine, and right now it cannot. This is not something you did wrong, it is just the kind of Claude you are talking to. The build runs in Claude Code in the terminal, because that is the only place the checker can run. The web app and the desktop app are great for everyday Claude chat, they just cannot build the stack. Open Claude Code in the terminal and we will pick right back up. Or reply CONCIERGE and a human gets you set up in minutes." Then route to the concierge hatch. Only when capability PASSes do you go to the bootstrap.

## ON EVERY LOAD, DO THIS FIRST (no exceptions)

Before you do any phase work, every single session, run the resume check and show the operator the evidence:

1. Run: `node .stack/verify.mjs reconcile`
2. Read the evidence table it prints. It is the truth: what is really on disk, not what a ledger claims. If a phase says verified but its files drifted, the table demotes it to needs-repair, and you fix that before anything else.
3. Read `.stack/DECISIONS.md` so you already know everything they told you. Never ask a question that is answered there. The decisions log has three parts: recommended defaults you chose for them, their explicit choices, and the hard fork answers (like pay regime). Trust all three.
4. Read `.stack/BUILD_PLAN.md` so you know the whole roadmap and can tell them where they are and what is next, in their words.
5. Read `foundation/cold-start.md` and follow it. It is the one instruction that loads their constitution and facts first, so you know their business by name before you do any real work. This auto-load is not optional, it runs every single session.
6. Then say one warm line, in plain English, like: "Welcome back. Last time we finished setting up your estimator. We were about to deepen your foundation. Want to keep going?" Then continue at the exact next step the table named.

If `.stack/verify.mjs` does not exist yet, this is a first run. Go to the bootstrap below.

## FIRST RUN ONLY: bootstrap the stack

Do this once, the very first time, then never again:

1. Pick the stack folder. Default is a folder named `operator-stack` in their home directory. Ask only if they have a strong preference. Log the choice.
2. Sync-folder safety check. If that folder sits inside iCloud Drive, OneDrive, Dropbox, or Google Drive, warn them plainly: cloud sync can corrupt a half-written file and break the resume. Recommend a plain local folder instead, and use it.
3. Permission handshake (do this before you write a single file). Show them exactly what you are about to create, then wait for a yes. If the engine is already in place you can run `node .stack/verify.mjs plan-bootstrap` to print the list. Either way, say it plainly: "Here is what I am about to build in your stack folder. The state ledger, the decisions log, the receipts trail, the build plan, the engine that does the checking, your constitution and facts, your estimator inputs, and a git safety file. That is about ten files. Nothing on your machine gets deleted, and if you ever want it gone you just delete the one folder. Want me to go ahead?" Wait for a clear yes. This is a checkpoint before the river, not a question in the middle of the stream. Only after the yes do you write anything.
4. Write the engine. Create `.stack/verify.mjs` and paste into it, verbatim, the entire script from the Appendix at the bottom of this file. Do not edit it. This is the deterministic judge.
5. Write a `.gitignore` at the stack root containing `.stack/`, `foundation/`, and `tools/`, so if they ever version their work, their pricing and client notes never get published.
6. Initialize the ledger: `node .stack/verify.mjs init`. This also writes `.stack/BUILD_PLAN.md`, the visible session-by-session roadmap. Open it with them for ten seconds so they see the whole path and the one hard fork coming in Phase 2.
7. Go to Phase 0.

## The golden path (how you ask questions)

- Ask at most a few real questions across the whole build, never an open ended interview. The forks are named inside each phase.
- When the operator does not know, recommend the highest fidelity, least buggy default, say so in one sentence, and proceed. Log it to `.stack/DECISIONS.md` tagged as a recommended default so a fresh session never re-asks.
- One real fork has no silent default: the pay regime in Phase 2, because a wrong default there can misprice a real job. If they are unsure, run the short diagnostic in that phase instead of guessing.
- Plain English only. No jargon. If you must name a technical thing, give the one sentence construction analogy for it.

---

# PHASE 0: get on the tools (install and verify)

This is the single hardest step for a non-technical operator, and the only one where a human may need to step in. Treat it as a supervised checklist with a real escape hatch, never as coding. You install two small, free tools: Node, the engine the checker runs on, and Claude Code, the assistant you work with. Both are quick, and you do not need Homebrew. Then you run Claude and sign in.

Detect their operating system and give them only the steps for it. Give them the literal command to paste, do not describe a command and expect them to figure it out.

## What you will see in the terminal (read this first)

The terminal is just a text box where you paste a line and press Enter, and it types back at you. Three things to know so nothing surprises you:

- When you paste an install line, it will print a lot of text as it works. That is normal. You do not have to read it. Wait for it to stop and give you back a fresh prompt.
- Later in the build, Claude will sometimes ask you to paste a checking line like `node .stack/verify.mjs check phase-1`. When that runs it prints one of three words: PASS, NEAR, or FAIL. PASS means that piece is built correctly. NEAR means it is basically right and just needs a small confirm from you. FAIL means one thing needs fixing, which Claude will do for you.
- NEAR and FAIL are normal, not errors you caused. The system is built to catch small problems and fix them. You will never have to figure out what to do, you just tell Claude and it handles it.

## Mac

1. Open the Terminal app: press Command and Space, type Terminal, press Enter.
2. Install Node, the engine the checker runs on. Open this page, download the macOS installer, open the file it downloads, and click through with the default choices. Pick the version marked LTS:

   `https://nodejs.org/en/download`

3. Install Claude Code, the assistant. Paste this one line and press Enter:

   `curl -fsSL https://claude.ai/install.sh | bash`

4. When it finishes, fully quit and reopen the Terminal app, so it picks up the new install.
5. Check both worked. Paste each of these and press Enter. Each should print a version number (a few numbers with dots):

   `node --version`

   `claude --version`

6. Start Claude. Paste this and press Enter:

   `claude`

   It will ask you to sign in on the first launch. Sign in with your paid Claude plan (Claude Pro at twenty dollars a month is the cheapest that works, or Max). There is no free version and no API key needed for this path.

## Windows

1. Open PowerShell: press the Start button, type PowerShell, press Enter.
2. Install Node, the engine the checker runs on. Open this page, download the Windows installer, run the file it downloads, and click through with the default choices. Pick the version marked LTS:

   `https://nodejs.org/en/download`

3. Install Claude Code, the assistant. Paste this one line and press Enter:

   `irm https://claude.ai/install.ps1 | iex`

4. When it finishes, fully close and reopen PowerShell.
5. Check both worked. Paste each of these and press Enter. Each should print a version number:

   `node --version`

   `claude --version`

6. Start Claude. Paste this and press Enter:

   `claude`

   Sign in on the first launch with your paid Claude plan (Pro or Max). There is no free version and no API key needed.

## If your work laptop is locked down by IT

Many corporate laptops block installs, antivirus quarantines, or a company proxy. Before you reach for a human concierge, try the simplest fix: ask your own IT department to allow it. You can forward them this paragraph as is:

"I would like to install Claude Code, Anthropic's official command-line tool, on my work machine. It installs with a single signed command from claude.ai (the Mac line is `curl -fsSL https://claude.ai/install.sh | bash`, the Windows PowerShell line is `irm https://claude.ai/install.ps1 | iex`). It runs locally and signs in with my existing paid Claude plan, no API keys stored on the machine. Could you allow this install, or run it for me, or grant the local permission it needs?"

If IT cannot help quickly, or you would rather not wait, fall back to the rescue prompt and the concierge hatch below.

## The rescue prompt (self serve, before the concierge)

If an install step fails, do not loop on it and do not jump straight to a human. Give them the self serve hatch first. Any Claude they can open today, claude.ai in the browser or the Claude desktop app, on the same paid account they already sign into, can read what the terminal said and talk them through the fix. Be honest about the division of labor: that Claude cannot run the install or reach their machine, the commands still happen in their terminal, but it is exactly the right tool for reading an error and giving the next move. Tell them: start a new chat, paste the prompt below, then paste exactly what the terminal showed, all of it, even the parts that look like noise.

```
I need help finishing an install that did not go as written. I have zero terminal experience, and I am following the Operator Stack guide. I am on a Mac working in the Terminal app, or I am on Windows working in PowerShell. I will say which at the bottom of this message.

The goal: install Node, the version marked LTS, and Claude Code, then sign in to Claude Code with my paid Claude account, so the rest of the guide can run in my terminal.

The guide's exact steps were:
1. Open the terminal. On a Mac that is the Terminal app. On Windows that is PowerShell, the plain one, not Administrator.
2. Install Node from https://nodejs.org/en/download, the version marked LTS, default choices all the way through.
3. Install Claude Code with one line. Mac: curl -fsSL https://claude.ai/install.sh | bash and Windows PowerShell: irm https://claude.ai/install.ps1 | iex
4. Fully close and reopen the terminal, then check with: claude --version and node --version
5. Run claude and sign in with a paid Claude account, Pro or Max.

A step did not go the way the guide said. At the bottom of this message I pasted exactly what my terminal showed.

How to help me:
1. Read my pasted terminal output first and diagnose from it. Do not guess from a list of generic fixes.
2. Assume I have never used a terminal. Tell me exactly what to type or click, and where.
3. Give me one instruction at a time, then stop and wait for me to tell you what happened before you give the next one.
4. Keep each reply short. One step, one expected result.
5. If claude starts but something still seems off, you can have me run claude doctor and read its report back to you.

The usual suspects, check these against my output first:
- I did not fully close and reopen the terminal after the install, so the claude command is not found yet.
- On a Mac, Claude Code installs to a folder called .local/bin inside my home folder. If a full restart did not fix command not found, walk me through adding that folder to my PATH.
- On Windows, I may have typed the install line into Command Prompt, cmd, instead of PowerShell. The PowerShell install line only works in PowerShell.
- My Node version is below 18. Have me run node --version and read what it prints.
- A company antivirus, proxy, or IT policy blocked the installer from downloading or running.
- I am signed into a free Claude account instead of a paid plan, Claude Pro or Max. A free account fails right after the browser sign in step.

One thing about your role: you cannot run these commands or reach my machine from this chat, and that is fine. The install happens in my terminal. You are the guide reading over my shoulder. Walk me through the fix until claude --version prints a version number, then keep going with me through the remaining steps of the guide until I am signed in and Claude Code is running.

Here is my operating system, and what my terminal showed:
```

If the rescue chat gets them to a version number, record the win and move on. If it stalls after two honest tries, go to the concierge hatch below.

## The concierge hatch (every install step)

If a step fails twice, stop trying to push them through it. Say, in plain English: "This is the step that trips most people up, and it is not your fault. Reply CONCIERGE and we will get you unstuck on a short screen share." Then give the exact failed command and its output so a human can pick it up fast.

When install verifies, record it and move on. There is no contract file to grade for Phase 0; the proof is the paste-back showing Claude runs.

---

# PHASE 0.5: the instant win (a taste, in under five minutes)

Do this the moment install verifies, before any setup, before any homework. No files to find, no past bids to dig up. Just one job out of the operator's head, priced two ways, so they feel the difference in the first five minutes. This is the wow moment, and the whole point of it is that it costs them nothing to get.

This is a taste, not the real estimator. The grounded estimator, the one that learns their actual rates and proves itself against their own past bids, comes in Phase 2. Say that plainly so they know there is more, but let them feel the spark first.

## Run it like this

1. Ask the operator to describe one small job out loud, in one sentence, off the top of their head. Give them an example so it is easy: "repaint a 1,200 square foot two-bedroom unit, occupied." Whatever they say, take it.
2. Price it back fast as a short line-item, not a paragraph. A few lines: labor, materials, a line or two for the obvious extras, a rough total. Keep it simple. This is a quick read, not a bid.
3. Stamp the pay regime on it. If they have not told you yet, take your best read from the job they described and label it, for example "looks like private open shop, confirm before bidding." Put the words "confirm before bidding" right on it, every time.
4. Then say this, in plain English: "Now do the same thing in a plain Claude with no setup. Open the Claude web app or a fresh chat and paste the exact same sentence you just gave me. Watch what comes back."
5. The plain Claude will hand them a confident lump sum with no line items, no regime, and no confirm note. It just guesses. That side by side, yours grounded and stamped versus the default that only guessed, is the whole point. It is the thing they will want to show somebody.

## Why this lands

The plain Claude is not wrong on purpose, it just has nothing to ground itself on, so it picks a number that sounds right. Yours stamps the regime, breaks the job into lines, and tells them to confirm before they ever bid it. Same question, two very different answers, and theirs is the one you can actually take to a client.

There is nothing to grade here and nothing to save. It is a demo, on purpose. When they have felt it, move on to the foundation so the real, provable estimator can be built.

---

# PHASE 1: the foundation (minimum viable, so the estimator can work)

Keep this lean. Capture only what the estimator in Phase 2 needs, and defer the rest to Phase 3. The goal is the fast win, not a perfect foundation.

## Fork 1: how deep on voice and identity (default: quick)

Ask once: quick or deep. Quick captures their name, company, trade, how they sign off, and a short list of phrases they never want Claude to use. Deep adds tone and hard rules. Recommend quick, because the deep version belongs in Phase 3 after they have felt a win. Log the choice.

## Write the foundation files

Create these three files under `foundation/`, filled with their real answers, in plain language, with no leftover template blanks and no fancy dashes:

- `Operating-Constitution.md`: who they are, their company, their trade, how Claude signs, the rule that Claude never invents a number it cannot trace to a source, and the note that pricing must be consistent so gates apply.
- `facts-registry.md`: the single source of truth. Company, operator name, trade, pay regime, default gross profit target.
- `cold-start.md`: the one instruction that every future session reads the Constitution and the facts first, so Claude knows their business before doing anything.

## Content echo (the check only the operator can do)

Before you mark this phase done, read the load-bearing facts back to them in plain English: company, their name, their trade, their pay regime, their default gross profit. Ask them to confirm "yes that is right." A structure check cannot catch a fact that is well written but wrong; only they can. Log their confirmation to `.stack/DECISIONS.md`.

## Health check

Touch `.stack/.lock` with this phase id before you write, then after the files are written and confirmed, run: `node .stack/verify.mjs check phase-1`. If it says PASS, you are done. If it says the result looks right but one check is short, that is a near miss, show them the friendly confirm, not a red FAIL. If it FAILs, read the plain English reason, fix the one thing, and re-run. Never advance on a FAIL.

---

# PHASE 2: the first real win (the estimator) with the back-test gate

This is the payoff. Build a working estimator on their real numbers, and prove it before they trust it.

## Fork 2 (no silent default): the pay regime

Pricing differs by regime, and a wrong assumption here can misprice a real bid, so do not guess. Ask which one this estimator is for: private open shop, prevailing wage (government funded or affordable housing), or union. If they are unsure, run the short diagnostic: is this work government funded or affordable housing, and is the labor from a union hall. Use their answer. Stamp the chosen regime on every estimate the tool produces, with the words "confirm before bidding," and flag prevailing wage and union for a verify-before-bidding review because those wage tables change.

## Capture their inputs and write the tool

- Capture their real pricing inputs into `tools/estimating/estimating-inputs.md`: which past bids and rate tables to load, their loaded hourly rate anchor, and at least three past jobs where they know the real final number (you need these for the back-test).
- Write `tools/estimating/estimating-skill.md`: a tool that prices a job line by line from their loaded rate table, never a guessed lump sum, that stamps the regime, carries a confidence band, shows the top three cost drivers it assumed, and refuses to produce a bid until the back-test passes.

## The estimator engine (build this exact engine, do not improvise)

Do not write a vague "I will estimate jobs" skill. Build a real line-item engine with the four parts below. The skill file must contain at least one fully worked calculation so the engine is provably real, not hand-wavy. The phase-2 check now reads the skill file for evidence of actual arithmetic: a labor-hours computation, a loaded rate, a quantity-times-unit-price line, the regime, and a computed total. A skill that only describes pricing in words will fail the check.

Part 1, the regime branch. The engine never lumps regimes. It picks the loaded labor rate from the chosen regime first, then prices. For example a painter line:

- Private open shop: unburdened wage plus burden. A 40 dollar per hour painter at a 32 percent burden is 40 x 1.32 = 52.80 dollars per hour loaded. (In the worked example below we use the operator's own published 35 dollar per hour loaded rate, so the whole guide agrees.)
- Prevailing wage (Davis-Bacon): use the published wage determination package, for example a painter package near 88.88 dollars per hour. Flag for a verify-before-bidding review because the determination changes.
- Union CBA: use the local package, for example a DC-9 painter near 93.11 dollars per hour. Flag the same way.

Part 2, the line-item formula. Every line follows the same shape:

- Labor line: quantity divided by production rate gives hours; hours times the loaded regime rate gives the labor cost. Real production rates: wall paint at 175 SF per hour per coat, ceilings and soffits at 200 SF per hour per coat, door prep and paint at 1.1 hours each, cove base and trim at 35 LF per hour.
- Material line: quantity times unit cost gives material cost; material cost times one plus the margin spread gives the sell price. Commodity paint carries a single-digit to low-double-digit spread, specialty items carry 70 to 82 percent gross profit.
- Then add the occupied-building aggravation factor on labor (access, staging, and resident coordination on every elevation), then the hidden costs (permits, dumpster, traffic control, management, overhead), then company overhead, then price to the gross profit target. Private target is about 43 percent, allowed band 40 to 45 percent. Never let price fall below about 30 percent gross profit, the net-negative trap.

Part 3, a fully worked sample, the running facade job. A 20,000 square foot exterior facade fix-and-repaint, 96-unit occupied HOA, Orange County, private open shop, painter at 35 dollars per hour loaded, Sherwin-Williams Loxon XP, the high-build exterior masonry coating, at 48 dollars per gallon contractor price. Show every line:

```
LABOR (quantity / production rate = hours; hours x $35 loaded)
  Wash + surface prep         20,000 SF / 100 SF-hr   = 200 hr   x $35 = $7,000
  Stucco/EIFS patch + repair  allowance               =  70 hr   x $35 = $2,450
  Cut + replace facade caulk  1,400 LF / 35 LF-hr      =  40 hr   x $35 = $1,400
  Spot-prime repairs          6,000 SF / 300 SF-hr     =  20 hr   x $35 =   $700
  Field paint, 2 coats        20,000 SF / 175 SF-hr x2 = 229 hr   x $35 = $8,000
  Soffits + overhangs, 2 coats 3,000 SF / 200 SF-hr x2 =  30 hr   x $35 = $1,050
  Door prep + paint           48 doors x 1.1 hr        =  53 hr   x $35 = $1,848
  Trim + cove detail          900 LF / 35 LF-hr        =  26 hr   x $35 =   $900
  Railings/penetrations/misc  allowance                =  35 hr   x $35 = $1,225
  Labor subtotal: 702 hr x $35 loaded                                   = $24,573

MATERIALS (quantity x unit cost, then sell at the spread)
  Loxon XP, 2 coats over textured stucco at 200 SF/gal-coat
                       200 gal x $48/gal = $9,600
  Patch/mesh $1,900 + caulk $1,190 + sundries $1,100
  Material cost $13,790  x 1.12 spread  = sell $15,445

OCCUPIED AGGRAVATION  22% of labor  = $5,406
HIDDEN COSTS  permits $2,000 + dumpster $2,400 + traffic $2,800
              + lifts $3,000 + management (8%) $2,398 = $12,598

  Direct cost                                 = $58,022
  Company overhead 10%                         =  $5,802
  Total cost                                   = $63,824
  Price at 43% gross profit                    = $111,972  (about $112,000)
```

Part 4, the stamped output line every estimate ends with. It names the regime and says confirm before bidding, every time:

```
Regime: private open shop. Estimate $112,000 at 43% gross profit. Confidence: medium. Confirm before bidding.
```

## The back-test gate (this is what makes the number trustworthy)

A confident wrong number is the one failure that ends this product, so the estimator must earn its first PASS. Have the operator give you their three or more past jobs with known final numbers, as a small file like `past-bids.json` (a list of jobs, each with `estimate` and `actual`). Run: `node .stack/verify.mjs backtest past-bids.json`. If the worst case is within tolerance, it passes and records a receipt. If it is off, it fails with a plain English cause (wrong regime, inconsistent units, too few jobs), and Phase 2 stays blocked until it reproduces their own bids.

## Health check

Run: `node .stack/verify.mjs check phase-2`. It will refuse to pass until the back-test receipt exists, so the estimator cannot be marked done while it still misprices their own history. On PASS, the operator has a real, trustworthy estimator. That is the win.

There is one optional specialization, HOA facade work, that some operators will want. It is not part of the main path and most operators skip it. It lives in the Specializations appendix near the end of this file, so it does not interrupt the momentum right after the first win. Do not bring it up here unless the operator's main work is exterior facade. Go straight to Phase 3.

---

# PHASE 3: deepen the foundation and install the gates

Now that they have felt a win, deepen what Phase 1 kept lean, one sitting at a time.

- Memory and pointers: teach Claude to keep an index card that says where each file is, rather than stapling the whole binder into every conversation. Explain why: it saves room so Claude does not run out of context mid job.
- Rules and voice: add the deeper tone and hard rules deferred from Phase 1.
- Hooks: deterministic gates, installed. A hook is a building inspector who stops the pour when something is wrong, not a sign politely asking. It is deterministic where the model is probabilistic.
- Daemons: optional background helpers. A daemon is the night-shift watchman who runs the checklist while they sleep.
- The jury: for a high-stakes answer, more than one model checks it before it ships, the same way the estimator had two checks before it let a number out.

## Memory pointers template (foundation/memory-pointers.md)

A pointer file is a one-page index card, not a full binder. It tells a future session where to look for a thing, so you load only the page you need instead of stapling the whole file into the conversation and running out of room. Write it like this, in their words:

"When you need [X], load [file path] and search for [keyword]. Do not load the whole file into context."

For example:
- Rates: load `tools/estimating/estimating-inputs.md`, search for "loaded hourly rate."
- Company facts: load `foundation/facts-registry.md`, search for the company name.
- Voice rules: load `foundation/rules.md`, search for "never."

This is what keeps Claude fast and stops it from running out of room mid job.

Capture each as a file, content-echo the important choices, and health-check the phase before advancing.

---

# PHASE 4: the rest of the tools (locked order)

Same pattern as the estimator: ask two or three questions, pick safe defaults, write the tool, prove it, in this order.

1. Proposal Builder: which style, the best research inputs, the proposal structure and financial framing.
2. Client Research: research a prospect or client to feed business development and the Proposal Builder.

Then others as their work shows the need.

---

# Closeout: the finished stack

When the last phase passes (Client Research, phase-4b), the engine writes one final file for them: `.stack/FINISHED-README.md`. You did not have to write it by hand, the `check` command does it on the final PASS. Open it with them and read it out, because this is the moment a non-technical operator finally sees that they built something real.

The README tells them, in plain English: what they built and where each piece lives, how to run the estimator (load `tools/estimating/estimating-skill.md`, give it a job, it prices line by line and says confirm before bidding), how to resume and add more later (run reconcile, reply, and you pick up at the next step), and that all of their data is local on their own machine with no cloud and no account. It carries the quiet credit, by Eugeen Bernan, and nothing else.

Say it simply: "Your stack is done. You have a real estimator that knows your business by name and prices from your own numbers. Here is how to use it, and here is how to come back and add more."

---

# Recovery playbook (when something breaks)

The first rule of recovery is that the operator never has to fix anything by hand. They do not open hidden folders, they do not delete files, they do not edit anything. If something looks off, they say one plain line, "something looks off, fix it," and Claude runs the repair. Claude does the work, the operator watches.

- A file came out empty or too short: the session likely ran out of room mid write. The operator just says "something looks off, fix it." Claude re-runs the resume check, sees which file is short, rewrites that one file, and re-runs the phase. The pre-check keeps everything that was already good, so the operator never re-answers a question.
- The resume check says needs-repair: a verified file drifted (truncated, hand edited, or a cloud sync conflict). The operator does not have to know which file. They say "fix it," and Claude reads the health note that names the exact file, recovers that one file, and re-runs the phase check. Never build the next phase on a needs-repair phase.
- They edited a file by hand: tell them, gently, that it is easier to let you edit through the build, or to just tell you after they change something, because a plain text editor can quietly swap quote marks and dashes that break a later check. If a hand edit did break something, the same line, "something looks off, fix it," is all they need.
- They are on two machines: the resume is deterministic on one machine. If the receipts show two machines, warn them and reconcile to one.
- Always, the first time they ever see a FAIL: open with "this is normal and fixable, nothing is broken on your end," then do the one thing yourself and tell them it is handled.

---

# How the stack remembers you between sessions

Your stack is built to survive a restart, a crash, or just a long gap. You never lose your place. Every session, in order, you:

1. Run `node .stack/verify.mjs reconcile`. It reads STATE.json and the receipts and prints what is verified and what is next.
2. Read `.stack/DECISIONS.md` so you know what they already told you. Never re-ask.
3. Read `.stack/BUILD_PLAN.md` so you know the plan and where they are on it.
4. Read `foundation/cold-start.md` so you load their constitution and facts first.

That is the resume protocol, and it is deterministic: same disk, same result, no re-asking, no guessing. The artifacts on disk are authoritative, not the ledger's claim. If STATE.json says a phase is verified but a file drifted, reconcile demotes it to needs-repair and names the exact file. You do not lose work, you fix the one thing.

Four files do the remembering, and it helps to know what each one is for:

- STATE.json: the ledger. Which phases are verified, the operator's facts, when it was last touched.
- DECISIONS.md: a plain log of what they said, in three sections (recommended defaults you chose, their explicit choices, and the hard fork answers like pay regime), so a fresh session never asks twice.
- RECEIPTS.jsonl: one line per grading run. Timestamp, phase, verdict, score, evidence. This is the audit trail.
- BUILD_PLAN.md: the roadmap they followed, written once and unchanged.

If they ever hand-edit a file (say they change the company name in facts-registry.md), ask them to tell you after they do it. The file hash will have drifted and reconcile will catch it. That is the system working, not breaking. If STATE.json ever becomes unreadable, reconcile rebuilds a provisional state from the receipts and the files, shows a WARNING, and asks them to confirm the table. Nothing is ever silently wiped.

---

# Specializations (only if this is your trade)

This section is optional and off the main path. The Phase 2 estimator is already complete and trustworthy on its own. Nothing here fixes a gap in the main build, it just tunes the estimator for one specific kind of work. Skip the whole section unless it is clearly your trade. If you skip it, nothing breaks and the resume table will never nag you about it.

## HOA facade calculator (optional personalization)

This only makes sense for operators whose main work is HOA facade, exterior defect, or weatherproofing in the California open-shop regime. If that is not your main work, skip it and stay on the main path. This specialization does not fix anything in Phase 2, it just rebases the estimator on exterior items instead of interior ones.

### Fork: is HOA facade your primary work

Ask once, plainly: is most of your work HOA facade, exterior defect, or weatherproofing, as opposed to interior finishes, framing, or fit-out? If no, log the skip to DECISIONS.md. The resume table will not nag them about a specialization they chose to skip.

If yes, this rebases the estimator on a real exterior catalog and California open-shop rates, so it stops thinking in interior items it will never bid.

### Walk the catalog and set the rates

You can build this specialization from what is in this file alone. There is also a live demo on the website if the operator wants to feel it first, but it is not required to finish:

- A live demo runs at `/calculator` on the site, so they can try it before they personalize their own. This is optional and just a preview.
- If you ever see a reference to a separate file named `operator-stack-calculator-blueprint.md`, treat it as optional. It is not part of this download and you do not need it to build the specialization here.

Together with them:

1. Load the exterior catalog and confirm the defect types match their work: stucco and EIFS, facade caulk, power wash, exterior paint, balcony membrane and railing, gutters, fascia, soffit, roof edge, windows, doors, siding, waterproofing, dry rot, scaffolding. Cut what they never do.
2. Set the California open-shop labor rates by trade (painter, carpenter, laborer, plasterer, waterproofer, and the rest). These are seed numbers. Make them set their own before they trust a bid.
3. Confirm the material and fixture costs match their suppliers.
4. Price one real HOA job together to feel it.

### Capture and write

Create these three files under `tools/estimating/`:

- `hoa-catalog.json`: the exterior facade items, tuned to their work, each with a confidence tag and marked a starting point.
- `california-open-shop-rates.json`: their California open-shop labor rates and material tiers.
- `hoa-estimator-skill.md`: the estimator tuned for HOA facade work. It still stamps the regime, still says confirm before bidding, and still refuses to guess a lump sum.

### Health check

Run: `node .stack/verify.mjs check phase-2b`. It confirms the catalog and rates landed, the files name the exterior scope, and every estimate still says confirm before bidding. On PASS, the estimator is tuned for HOA facade work and will not confuse interior and exterior items. If they skip this specialization, nothing breaks: the Phase 2 estimator is still complete, just more general.

---

# Appendix: .stack/verify.mjs

Write this file verbatim to `.stack/verify.mjs` during bootstrap. It is the deterministic judge. Do not change it.

```javascript
#!/usr/bin/env node
/**
 * Operator Stack, the deterministic verifier the master blueprint emits and runs.
 *
 * The product's own thesis is that a probabilistic model needs deterministic
 * gates. So the blueprint does NOT ask Claude to judge a phase by
 * reading prose. It writes THIS file into .stack/ on first load, and every
 * phase shells out to it. Prose orchestrates, code judges.
 *
 * The verifier is a small, deterministic, offline judge:
 *   - normalize + matcher + scored verdict (substance with wide tolerance, so a
 *     correct setup is never failed on formatting)
 *   - receipts-authoritative integrity (the artifact on disk beats the claim)
 *   - a versioned {v,data} ledger envelope, migrate-on-read, no silent wipe
 * Node built-ins only, runs on the operator's own machine, no network.
 *
 * Commands:
 *   node verify.mjs reconcile [--root DIR]        the resume evidence table; demotes drift
 *   node verify.mjs check <phaseId> [--root DIR]  grade a phase; PASS/NEAR/FAIL; writes a receipt
 *   node verify.mjs firewall <file> [--denylist F] fail-closed brand/firewall scan
 *   node verify.mjs backtest <file.json> [--tolerance N]  estimator variance gate
 *   node verify.mjs init [--root DIR] [--flavor F]  create a fresh .stack ledger
 *   node verify.mjs capability [--root DIR]         write-access preflight; PASS/FAIL
 *   node verify.mjs plan-bootstrap [--root DIR]     list what first run will create
 *
 * Exit codes: 0 PASS/ok, 1 NEAR/warn, 2 FAIL/blocked, 3 usage/internal error.
 *
 * No em dashes in this source. Dash and smart-quote detection targets are built
 * at runtime with String.fromCharCode so no banned glyph appears here, while the
 * verifier still catches them inside an artifact.
 */

import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, appendFileSync, rmSync } from 'node:fs'
import { createHash } from 'node:crypto'
import { join, dirname, basename } from 'node:path'

const SCHEMA_VERSION = 1
const MAX_INSPECT_CHARS = 200_000

// ---------------------------------------------------------------------------
// Unicode glyphs built from code points so no banned character is in source.
// ---------------------------------------------------------------------------
const EM_DASH = String.fromCharCode(0x2014)
const EN_DASH = String.fromCharCode(0x2013)
const HORIZONTAL_BAR = String.fromCharCode(0x2015)
const DASH_CLASS = new RegExp('[' + EN_DASH + EM_DASH + HORIZONTAL_BAR + ']', 'g')
const SMART_SINGLE = new RegExp('[' + String.fromCharCode(0x2018, 0x2019, 0x201a, 0x201b) + ']', 'g')
const SMART_DOUBLE = new RegExp('[' + String.fromCharCode(0x201c, 0x201d, 0x201e, 0x201f) + ']', 'g')

// ---------------------------------------------------------------------------
// Normalization for substance matching: wide tolerance, never brittle.
// ---------------------------------------------------------------------------
function normalize(input) {
  return String(input)
    .slice(0, MAX_INSPECT_CHARS)
    .toLowerCase()
    .replace(SMART_SINGLE, "'")
    .replace(SMART_DOUBLE, '"')
    .replace(DASH_CLASS, '-')
    .replace(/[*_`#>]/g, '')
    .replace(/\s+/g, ' ')
    .trim()
}

function firstNameToken(value) {
  const n = normalize(value)
  return n.split(' ')[0] ?? n
}

function stripLeadingArticle(s) {
  return s.replace(/^(a|an|the)\s+/, '')
}

function countSentences(text) {
  const m = String(text).match(/[.!?]+(\s|$)/g)
  return m ? m.length : 0
}

function nonWhitespaceLen(text) {
  return String(text).replace(/\s/g, '').length
}

function sha256(text) {
  return createHash('sha256').update(String(text), 'utf8').digest('hex').slice(0, 16)
}

// ---------------------------------------------------------------------------
// Matchers. containsResolved is first-name and substring tolerant, exactly as
// the Climb verifier, so a correct operator is never failed on formatting.
// ---------------------------------------------------------------------------
function containsResolved(token, haystack, facts) {
  const value = facts[token]
  if (!value) return false
  const norm = normalize(value)
  if (norm.length === 0) return false
  if (haystack.includes(norm)) return true
  const first = firstNameToken(value)
  if (first.length >= 2 && haystack.includes(first)) return true
  return false
}

function containsAnyResolved(token, haystack, facts) {
  const value = facts[token]
  if (!value) return false
  const items = String(value)
    .split(/[,;]+/)
    .map((s) => stripLeadingArticle(normalize(s)))
    .filter((s) => s.length >= 4)
  if (items.length === 0) return false
  return items.some((it) => haystack.includes(it))
}

function safe(fn, fallback) {
  try {
    return fn()
  } catch {
    return fallback
  }
}

/**
 * Grade artifact text against a contract. Deterministic, offline. Returns
 * { verdict: PASS|NEAR|FAIL, score, reason, required[], forbidden[] }.
 * Verdict math: a forbidden hit or a too-short artifact is an
 * immediate FAIL; weighted score over passThreshold is PASS; a clean near miss
 * (no forbidden hit, length ok, score >= 0.6) is NEAR, surfaced as
 * "looks right, confirm" instead of a red FAIL.
 */
function gradeText(contract, raw, facts) {
  const rawText = String(raw).slice(0, MAX_INSPECT_CHARS)
  const norm = normalize(rawText)

  // 1. Forbidden checks run against the UN-normalized text where they target a
  //    raw glyph, because normalization erases the very thing they look for.
  const forbidden = (contract.forbiddenTokens ?? []).map((m) => {
    const found = safe(() => {
      switch (m.kind) {
        case 'unresolvedSentinel':
          return rawText.includes('{{')
        case 'emDash':
          return rawText.includes(EM_DASH) || rawText.includes(EN_DASH) || rawText.includes(HORIZONTAL_BAR)
        case 'literal':
          return norm.includes(normalize(m.value))
        default:
          return false
      }
    }, false)
    return { label: m.label, matched: found }
  })
  const forbiddenHit = forbidden.find((o) => o.matched)

  // 2. minLength floor.
  let minLenOk = true
  let minLenReason = ''
  if (contract.minLength) {
    const { minChars, minSentences } = contract.minLength
    if (minChars && nonWhitespaceLen(rawText) < minChars) {
      minLenOk = false
      minLenReason = 'the file looks too short to be the real thing'
    }
    if (minSentences && countSentences(rawText) < minSentences) {
      minLenOk = false
      minLenReason = 'the file looks too short to be the real thing'
    }
  }

  // 3. Required matchers, weighted.
  const required = (contract.requiredTokens ?? []).map((m) => {
    const weight = m.weight || 1
    const matched = safe(() => {
      switch (m.kind) {
        case 'containsResolvedValue':
          return containsResolved(m.token, norm, facts)
        case 'containsAnyResolvedValue':
          return containsAnyResolved(m.token, norm, facts)
        case 'containsLiteral':
          return norm.includes(normalize(m.value))
        case 'matchesPattern':
          // Deterministic regex over the normalized text, so a real worked
          // calculation (hours math, a loaded rate, a quantity-times-unit-price
          // line, a computed total) is proven by structure, not by a string the
          // operator could paste without ever doing the arithmetic. Pattern is a
          // plain RegExp source string, compiled case-insensitively. A bad
          // pattern fails closed via the surrounding safe().
          return new RegExp(m.pattern, 'i').test(norm)
        case 'hasNSentences':
          return countSentences(rawText) >= m.n
        default:
          return false
      }
    }, false)
    return { label: m.label, matched, weight, detail: matched ? undefined : m.detail }
  })

  const totalWeight = required.reduce((s, o) => s + o.weight, 0) || 1
  const matchedWeight = required.filter((o) => o.matched).reduce((s, o) => s + o.weight, 0)
  const score = matchedWeight / totalWeight
  const passT = contract.passThreshold ?? 0.85

  let verdict
  if (forbiddenHit || !minLenOk) verdict = 'FAIL'
  else if (score >= passT) verdict = 'PASS'
  else if (score >= 0.6) verdict = 'NEAR'
  else verdict = 'FAIL'

  let reason
  if (forbiddenHit) reason = forbiddenHit.label
  else if (!minLenOk) reason = minLenReason
  else reason = required.find((o) => !o.matched)?.label ?? ''

  return { verdict, score, reason, required, forbidden }
}

// ---------------------------------------------------------------------------
// Ledger: versioned {v,data} envelope, migrate-on-read, no-silent-wipe.
// On a genuine parse failure of the ledger, we DO NOT treat it as empty (the
// classic silent-wipe bug); the caller rebuilds from receipts plus files.
// ---------------------------------------------------------------------------
const MIGRATIONS = {} // greenfield; seam is here so a later version is a data change

function stackPaths(root) {
  const dot = join(root, '.stack')
  return {
    root,
    dot,
    state: join(dot, 'STATE.json'),
    stateMd: join(dot, 'STATE.md'),
    decisions: join(dot, 'DECISIONS.md'),
    receipts: join(dot, 'RECEIPTS.jsonl'),
    health: join(dot, 'health'),
    lock: join(dot, '.lock'),
    buildPlan: join(dot, 'BUILD_PLAN.md'),
    finishedReadme: join(dot, 'FINISHED-README.md'),
  }
}

function readLedger(root) {
  const p = stackPaths(root)
  if (!existsSync(p.state)) return { ok: false, reason: 'no-state', data: null }
  let parsed
  try {
    parsed = JSON.parse(readFileSync(p.state, 'utf8'))
  } catch {
    // NOT empty state. Corrupt ledger. Caller rebuilds from receipts.
    return { ok: false, reason: 'corrupt', data: null }
  }
  let version = 0
  let data = parsed
  if (parsed && typeof parsed === 'object' && 'v' in parsed && 'data' in parsed) {
    version = parsed.v
    data = parsed.data
  }
  if (version < SCHEMA_VERSION) {
    const migrate = MIGRATIONS['STATE']
    if (migrate) {
      data = migrate(data, version)
      writeLedger(root, data)
    }
  }
  return { ok: true, reason: 'ok', data }
}

function writeLedger(root, data) {
  const p = stackPaths(root)
  if (!existsSync(p.dot)) mkdirSync(p.dot, { recursive: true })
  data.lastTouchedAt = data.lastTouchedAt || nowIso()
  writeFileSync(p.state, JSON.stringify({ v: SCHEMA_VERSION, data }, null, 2))
}

function appendReceipt(root, receipt) {
  const p = stackPaths(root)
  if (!existsSync(p.dot)) mkdirSync(p.dot, { recursive: true })
  appendFileSync(p.receipts, JSON.stringify(receipt) + '\n')
}

function readReceipts(root) {
  const p = stackPaths(root)
  if (!existsSync(p.receipts)) return []
  return readFileSync(p.receipts, 'utf8')
    .split('\n')
    .filter((l) => l.trim().length > 0)
    .map((l) => safe(() => JSON.parse(l), null))
    .filter(Boolean)
}

// Time is passed in via env for deterministic tests; falls back to a fixed
// stamp so the script never depends on a wall clock it cannot reproduce.
function nowIso() {
  return process.env.OPERATOR_STACK_NOW || '1970-01-01T00:00:00.000Z'
}

// ---------------------------------------------------------------------------
// Phase contracts. Keyed by phaseId. Tokens resolve from data.facts. This is
// the construction-flavor-agnostic engine: examples live in the flavor pack,
// the contracts here only assert the operator's own resolved values landed.
// ---------------------------------------------------------------------------
const CONTRACTS = {
  'phase-1': {
    name: 'Minimum-viable foundation',
    artifacts: [
      { path: 'foundation/Operating-Constitution.md', minChars: 200 },
      { path: 'foundation/facts-registry.md', minChars: 120 },
      { path: 'foundation/cold-start.md', minChars: 80 },
    ],
    // Graded against the concatenation of the phase artifacts.
    contract: {
      passThreshold: 0.75,
      minLength: { minChars: 400 },
      requiredTokens: [
        { kind: 'containsResolvedValue', token: 'COMPANY_NAME', label: 'your company name is written into the foundation', weight: 2 },
        { kind: 'containsResolvedValue', token: 'OPERATOR_NAME', label: 'your name is in the facts registry', weight: 1 },
        { kind: 'containsResolvedValue', token: 'PRIMARY_TRADE', label: 'your trade is captured', weight: 1 },
        { kind: 'containsResolvedValue', token: 'PAY_REGIME', label: 'your pay regime is captured', weight: 1 },
      ],
      forbiddenTokens: [
        { kind: 'unresolvedSentinel', label: 'a template placeholder did not get filled in' },
        { kind: 'emDash', label: 'an em dash slipped into a written file' },
      ],
    },
    // The load-bearing facts surfaced for content-echo confirmation.
    echoFacts: ['COMPANY_NAME', 'OPERATOR_NAME', 'PRIMARY_TRADE', 'PAY_REGIME', 'DEFAULT_GP'],
  },
  'phase-2': {
    name: 'First real tool, Estimating',
    artifacts: [
      { path: 'tools/estimating/estimating-skill.md', minChars: 400 },
      { path: 'tools/estimating/estimating-inputs.md', minChars: 120 },
    ],
    contract: {
      passThreshold: 0.75,
      minLength: { minChars: 500 },
      requiredTokens: [
        { kind: 'containsResolvedValue', token: 'PAY_REGIME', label: 'the estimator knows your pay regime', weight: 2 },
        { kind: 'containsLiteral', value: 'regime', label: 'the estimator stamps the pricing regime on its output', weight: 1 },
        { kind: 'containsLiteral', value: 'confirm before bidding', label: 'every estimate says confirm before bidding', weight: 2 },
        { kind: 'containsResolvedValue', token: 'COMPANY_NAME', label: 'the estimator is tied to your company', weight: 1 },
        // Evidence of a REAL worked calculation, not just the right words. Each
        // of these proves a piece of arithmetic actually happened in the skill
        // file, so a plausible-but-hand-wavy estimator cannot pass. Patterns run
        // against normalized (lowercased, markdown-stripped) text.
        // A labor-hours computation: a quantity divided by a production rate, or
        // a number stated in hours (for example "20,000 sf / 175 sf-hr" or "229 hr").
        { kind: 'matchesPattern', pattern: '([0-9][0-9,\\.]*\\s*(sf|lf|sq\\s*ft)?\\s*(/|per)\\s*[0-9][0-9,\\.]*|[0-9][0-9,\\.]*\\s*(hr|hrs|hours))', label: 'a worked labor-hours calculation is shown (quantity, production rate, or hours)', weight: 2 },
        // A loaded labor rate: a dollar rate tied to the word loaded or per hour.
        { kind: 'matchesPattern', pattern: '(\\$\\s?[0-9][0-9,\\.]*\\s*(/|per)\\s*(hr|hour)|[0-9][0-9,\\.]*\\s*(dollars|\\$)?\\s*(per\\s*hour|/\\s*hr|loaded)|loaded\\s*(rate|labor|hourly))', label: 'a loaded labor rate is used in the math', weight: 2 },
        // A quantity times unit price line: "200 gal x $48" or "x $48".
        { kind: 'matchesPattern', pattern: '([0-9][0-9,\\.]*\\s*(gal|gallon|gallons|ea|each|lf|sf|unit|units|doors?)?\\s*(x|times|\\*)\\s*\\$\\s?[0-9]|\\$\\s?[0-9][0-9,\\.]*\\s*(/|per)\\s*(gal|gallon|unit|ea|each|lf|sf))', label: 'a quantity-times-unit-price material line is shown', weight: 2 },
        // A computed total: a dollar figure tied to a total/price/bid word.
        { kind: 'matchesPattern', pattern: '(total|price|bid|estimate|sell)[^\\$]{0,40}\\$\\s?[0-9][0-9,\\.]{2,}', label: 'a computed total or priced number is shown', weight: 2 },
      ],
      forbiddenTokens: [
        { kind: 'unresolvedSentinel', label: 'a template placeholder did not get filled in' },
        { kind: 'emDash', label: 'an em dash slipped into a written file' },
      ],
    },
    // Phase 2 also requires the back-test gate to pass (see backtest command).
    requiresBacktest: true,
    echoFacts: ['PAY_REGIME', 'RATE_ANCHOR'],
  },
  // Optional. Only runs when HOA/facade is the operator's primary work. Phase 2
  // already ships a complete estimator on its own, so a skipped phase-2b is not
  // a gap in the resume table, it is a chosen path. The companion calculator
  // bones (public/downloads/operator-stack-calculator-blueprint.md and the live
  // /calculator starter) are referenced from the prose; this contract only
  // grades that the operator's own catalog and rates actually landed on disk.
  'phase-2b': {
    name: 'Personalize the estimator for HOA facade work (optional)',
    optional: true,
    artifacts: [
      { path: 'tools/estimating/hoa-catalog.json', minChars: 300 },
      { path: 'tools/estimating/california-open-shop-rates.json', minChars: 200 },
      { path: 'tools/estimating/hoa-estimator-skill.md', minChars: 500 },
    ],
    contract: {
      passThreshold: 0.75,
      minLength: { minChars: 1000 },
      requiredTokens: [
        { kind: 'containsLiteral', value: 'facade', label: 'the catalog names exterior facade items', weight: 1 },
        { kind: 'containsLiteral', value: 'california', label: 'the rates reflect a california open-shop set', weight: 1 },
        { kind: 'containsLiteral', value: 'open-shop', label: 'the pay regime is open-shop', weight: 1 },
        { kind: 'containsLiteral', value: 'hoa', label: 'the estimator is tuned for hoa work', weight: 1 },
        { kind: 'containsLiteral', value: 'confirm before bidding', label: 'every estimate still says confirm before bidding', weight: 1 },
      ],
      forbiddenTokens: [
        { kind: 'unresolvedSentinel', label: 'a template placeholder did not get filled in' },
        { kind: 'emDash', label: 'an em dash slipped into a written file' },
      ],
    },
    echoFacts: [],
  },
  'phase-3': {
    name: 'Deepen the foundation and install the gates',
    artifacts: [
      { path: 'foundation/memory-pointers.md', minChars: 120 },
      { path: 'foundation/rules.md', minChars: 120 },
    ],
    contract: {
      passThreshold: 0.6,
      minLength: { minChars: 300 },
      requiredTokens: [
        { kind: 'containsResolvedValue', token: 'COMPANY_NAME', label: 'the deepened foundation is tied to your company', weight: 1 },
        { kind: 'containsLiteral', value: 'pointer', label: 'the memory pointer pattern is written down', weight: 1 },
      ],
      forbiddenTokens: [
        { kind: 'unresolvedSentinel', label: 'a template placeholder did not get filled in' },
        { kind: 'emDash', label: 'an em dash slipped into a written file' },
      ],
    },
    echoFacts: [],
  },
  'phase-4a': {
    name: 'Proposal Builder',
    artifacts: [{ path: 'tools/proposal-builder/proposal-builder-skill.md', minChars: 300 }],
    contract: {
      passThreshold: 0.6,
      minLength: { minChars: 300 },
      requiredTokens: [
        { kind: 'containsResolvedValue', token: 'COMPANY_NAME', label: 'the proposal builder is tied to your company', weight: 1 },
        { kind: 'containsLiteral', value: 'proposal', label: 'it builds proposals', weight: 1 },
      ],
      forbiddenTokens: [
        { kind: 'unresolvedSentinel', label: 'a template placeholder did not get filled in' },
        { kind: 'emDash', label: 'an em dash slipped into a written file' },
      ],
    },
    echoFacts: [],
  },
  'phase-4b': {
    name: 'Client Research',
    artifacts: [{ path: 'tools/client-research/client-research-skill.md', minChars: 300 }],
    contract: {
      passThreshold: 0.6,
      minLength: { minChars: 300 },
      requiredTokens: [
        { kind: 'containsResolvedValue', token: 'COMPANY_NAME', label: 'the research tool is tied to your company', weight: 1 },
        { kind: 'containsLiteral', value: 'research', label: 'it researches clients', weight: 1 },
      ],
      forbiddenTokens: [
        { kind: 'unresolvedSentinel', label: 'a template placeholder did not get filled in' },
        { kind: 'emDash', label: 'an em dash slipped into a written file' },
      ],
    },
    echoFacts: [],
  },
}

const PHASE_ORDER = ['phase-0', 'phase-1', 'phase-2', 'phase-2b', 'phase-3', 'phase-4a', 'phase-4b']

// ---------------------------------------------------------------------------
// reconcile: the resume evidence table. The artifact on disk is authoritative,
// never the ledger's claim (the integrity principle: proof beats claim).
// ---------------------------------------------------------------------------
// Shared artifact state: read each artifact ONCE and report existence,
// non-empty, too-short, and hash-vs-recorded, so reconcile and check never
// duplicate the read-and-hash loop.
function artifactsState(root, def, recordedArtifacts) {
  const recorded = recordedArtifacts || []
  const files = def.artifacts.map((art) => {
    const full = join(root, art.path)
    const exists = existsSync(full)
    const text = exists ? readFileSync(full, 'utf8') : ''
    const nonEmpty = exists && nonWhitespaceLen(text) > 0
    const tooShort = nonEmpty && art.minChars ? nonWhitespaceLen(text) < art.minChars : false
    const rec = recorded.find((a) => a.path === art.path)
    const hashMatch = rec && rec.hash ? rec.hash === sha256(text) : null
    return { path: art.path, exists, text, nonEmpty, tooShort, hashMatch }
  })
  return {
    files,
    combined: files.map((f) => f.text).join('\n'),
    allPresentNonEmpty: files.every((f) => f.nonEmpty),
    hashOk: files.every((f) => f.hashMatch !== false),
    firstMissing: files.find((f) => !f.nonEmpty) || null,
    firstShort: files.find((f) => f.tooShort) || null,
    firstChanged: files.find((f) => f.hashMatch === false) || null,
  }
}

// Emit the reconcile result as either the human evidence table or one JSON line
// (--json), so the website progress screen and a future Supabase wrapper consume
// one machine format instead of scraping prose.
function emitReconcile(jsonMode, summary, code) {
  if (jsonMode) {
    print(JSON.stringify(summary))
    return summary.needsRepair ? 1 : code || 0
  }
  if (summary.state === 'first-run') {
    print('Resume: ' + summary.message)
    return code || 0
  }
  if (summary.state === 'rebuilt') {
    print('WARNING: ' + summary.message)
    print('Confirm the table below before continuing.')
    print('')
  }
  print('Resume evidence table:')
  print('  phase      claim                 artifact        hash      verdict')
  for (const r of summary.rows) {
    print('  ' + pad(r.id, 10) + ' ' + pad(r.claim, 21) + ' ' + pad(r.artifact, 15) + ' ' + pad(r.hash, 9) + ' ' + r.verdict)
  }
  print('')
  if (!summary.next) print('Next step: every phase is verified. Your stack is complete.')
  else if (summary.needsRepair)
    print(`Next step: REPAIR ${summary.next}. Its files drifted from what was verified. Do not build on it until repaired.`)
  else print(`Next step: ${summary.next}.`)
  return code || 0
}

function cmdReconcile(root) {
  const p = stackPaths(root)
  const jsonMode = process.argv.includes('--json')
  const led = readLedger(root)

  if (!led.ok && led.reason === 'no-state') {
    return emitReconcile(jsonMode, { state: 'first-run', rows: [], next: 'phase-0', needsRepair: false, message: 'No stack found. This is a first run, starting at Phase 0 (install).' }, 0)
  }

  let data = led.data
  let rebuilt = false
  if (!led.ok && led.reason === 'corrupt') {
    // Rebuild a provisional ledger from receipts plus files. Never silent-wipe.
    const receipts0 = readReceipts(root)
    const verifiedPhases = [...new Set(receipts0.filter((r) => r.kind === 'phase' && r.verdict === 'PASS').map((r) => r.phase))]
    data = {
      stackVersion: '1.0.0',
      phases: PHASE_ORDER.map((id) => ({ id, status: verifiedPhases.includes(id) ? 'verified' : 'not-started', idempotencyKeys: {}, artifacts: [] })),
      decisions: [],
      facts: {},
    }
    rebuilt = true
  }

  const receipts = readReceipts(root)
  const lockInfo = readLock(root)

  const rows = []
  for (const id of PHASE_ORDER) {
    const phase = (data.phases || []).find((ph) => ph.id === id)
    if (!phase) {
      rows.push({ id, claim: 'not-started', artifact: '-', hash: '-', verdict: 'not-started' })
      continue
    }
    const def = CONTRACTS[id]
    let claim = phase.status || 'not-started'

    // Crash sentinel: resolved-stale clears, otherwise force in-progress.
    if (lockInfo && lockInfo.phase === id) {
      const hasCleanHealth = existsSync(join(p.health, `${id}.health.md`))
      const hasPassReceipt = receipts.some((r) => r.phase === id && r.kind === 'phase' && r.verdict === 'PASS')
      if (hasCleanHealth && hasPassReceipt) clearLock(root)
      else if (claim === 'verified' || claim === 'verified-with-override') claim = 'in-progress'
    }

    let verdict = claim
    let artifactState = 'n/a'
    let hashState = 'n/a'
    if ((claim === 'verified' || claim === 'verified-with-override') && def) {
      const st = artifactsState(root, def, phase.artifacts)
      artifactState = st.allPresentNonEmpty ? 'present' : 'MISSING/empty'
      hashState = st.hashOk ? 'match' : 'CHANGED'
      if (!st.allPresentNonEmpty || !st.hashOk) {
        verdict = 'needs-repair'
        phase.status = 'needs-repair'
        // self-heal: name the exact failed file in the health note so recovery
        // does not have to re-run check to discover what drifted.
        const culprit = st.firstMissing || st.firstChanged
        const why = st.firstMissing ? 'missing or empty' : 'changed since it was verified'
        writeHealth(root, id, `needs-repair: ${culprit ? culprit.path : 'an artifact'} is ${why}. Recover it, then re-run: node .stack/verify.mjs check ${id}`)
      }
    }
    rows.push({ id, claim, artifact: artifactState, hash: hashState, verdict, optional: !!(def && def.optional) })
  }

  if (led.ok) writeLedger(root, data)

  // "next" picks the first unfinished phase, but an OPTIONAL phase that is still
  // not-started is a chosen skip, not a blocker: the operator opted out, so it
  // does not become the next step. The moment an optional phase is opened
  // (in-progress) or drifts (needs-repair), it surfaces normally and must be
  // resolved like any other phase.
  const isDone = (v) => v === 'verified' || v === 'verified-with-override'
  const isSkippableOptional = (r) => r.optional && r.verdict === 'not-started'
  const repair = rows.find((r) => r.verdict === 'needs-repair')
  const next = repair || rows.find((r) => !isDone(r.verdict) && !isSkippableOptional(r))
  return emitReconcile(
    jsonMode,
    {
      state: rebuilt ? 'rebuilt' : 'ok',
      rows,
      next: next ? next.id : null,
      needsRepair: !!repair,
      message: rebuilt ? 'STATE.json was unreadable. Rebuilt provisional state from the receipts trail and your files. Nothing was wiped.' : null,
    },
    repair ? 1 : 0,
  )
}

// ---------------------------------------------------------------------------
// check: grade a phase, write a receipt, update STATE.json.
// ---------------------------------------------------------------------------
function cmdCheck(root, phaseId) {
  const def = CONTRACTS[phaseId]
  if (!def) {
    print(`No contract for ${phaseId}.`)
    return 3
  }
  const led = readLedger(root)
  if (!led.ok) {
    print(`Cannot check ${phaseId}: no readable ledger. Run reconcile first.`)
    return 3
  }
  const data = led.data
  const facts = data.facts || {}

  // Idempotent: an already-verified phase whose artifacts still match their
  // recorded hashes is a no-op (no duplicate receipt, no rewrite). Mirrors
  // a verified phase re-checked is a no-op.
  const existing = (data.phases || []).find((ph) => ph.id === phaseId)
  if (existing && (existing.status === 'verified' || existing.status === 'verified-with-override')) {
    const unchanged =
      (existing.artifacts || []).length > 0 &&
      existing.artifacts.every((a) => {
        const full = join(root, a.path)
        return existsSync(full) && sha256(readFileSync(full, 'utf8')) === a.hash
      })
    if (unchanged) {
      print(`PASS: ${def.name} already verified (no change, idempotent no-op).`)
      return 0
    }
  }

  // Artifacts present, non-empty, and long enough? Read each once via the helper.
  const st = artifactsState(root, def, existing ? existing.artifacts : [])
  if (st.firstMissing) {
    writeHealth(root, phaseId, `FAIL: ${st.firstMissing.path} is missing or empty.`)
    print(`FAIL: ${st.firstMissing.path} is missing. This is normal and fixable, nothing is broken on your end.`)
    return 2
  }
  if (st.firstShort) {
    writeHealth(root, phaseId, `FAIL: ${st.firstShort.path} is shorter than expected.`)
    print(`FAIL: ${st.firstShort.path} came out too short. Most likely the session ran out of room mid-write. Re-run this phase.`)
    return 2
  }

  const result = gradeText(def.contract, st.combined, facts)

  // Phase 2 also gates on the back-test having passed.
  if (def.requiresBacktest) {
    const passed = readReceipts(root).some((r) => r.phase === phaseId && r.kind === 'backtest' && r.verdict === 'PASS')
    if (!passed) {
      writeHealth(root, phaseId, 'FAIL: the estimator has not passed the back-test gate yet.')
      print('FAIL: the estimator must reproduce your own past bids within tolerance before this phase can pass.')
      print('Run: node .stack/verify.mjs backtest <your-past-bids.json>')
      return 2
    }
  }

  // Record artifact hashes so future reconciles catch hand-edits.
  const phase = (data.phases || []).find((ph) => ph.id === phaseId) || { id: phaseId }
  phase.artifacts = def.artifacts.map((art) => ({
    path: art.path,
    expected: 'present',
    hash: sha256(readFileSync(join(root, art.path), 'utf8')),
  }))

  if (result.verdict === 'PASS') {
    phase.status = 'verified'
    phase.healthCheck = { ran: true, verdict: 'PASS', ranAt: nowIso() }
    upsertPhase(data, phase)
    writeLedger(root, data)
    appendReceipt(root, { phase: phaseId, kind: 'phase', verdict: 'PASS', score: result.score, contractHash: sha256(JSON.stringify(def.contract)), override: false, timestamp: nowIso() })
    writeHealth(root, phaseId, `PASS: score ${result.score.toFixed(2)}.`)
    clearLock(root)
    print(`PASS: ${def.name} verified (score ${result.score.toFixed(2)}).`)
    // Closeout: when the final phase passes, write the finished-stack README.
    // Idempotent (it overwrites), fail-safe (never blocks the PASS it follows).
    if (phaseId === PHASE_ORDER[PHASE_ORDER.length - 1]) {
      safe(() => writeCloseout(root), undefined)
      print('Your stack is done. Wrote .stack/FINISHED-README.md with what you built and how to use it.')
    }
    return 0
  }

  if (result.verdict === 'NEAR') {
    phase.status = 'in-progress'
    upsertPhase(data, phase)
    writeLedger(root, data)
    writeHealth(root, phaseId, `NEAR: score ${result.score.toFixed(2)}; ${result.reason}.`)
    print(`This looks right but one check is short: ${result.reason}.`)
    print(`To accept it as-is, run: node .stack/verify.mjs confirm ${phaseId}`)
    return 1
  }

  phase.status = 'needs-repair'
  upsertPhase(data, phase)
  writeLedger(root, data)
  writeHealth(root, phaseId, `FAIL: ${result.reason}.`)
  print(`FAIL: ${result.reason}. This is normal and fixable, nothing is broken on your end.`)
  return 2
}

// confirm: the one-tap near-miss override. Reserved; the blueprint refuses to
// offer it on Phase 1 facts or the Phase 2 back-test (handled in prose).
function cmdConfirm(root, phaseId) {
  const led = readLedger(root)
  if (!led.ok) return 3
  const data = led.data
  const phase = (data.phases || []).find((ph) => ph.id === phaseId) || { id: phaseId }
  phase.status = 'verified-with-override'
  upsertPhase(data, phase)
  writeLedger(root, data)
  appendReceipt(root, { phase: phaseId, kind: 'phase', verdict: 'PASS', override: true, timestamp: nowIso() })
  print(`Confirmed: ${phaseId} accepted as verified-with-override.`)
  return 0
}

// ---------------------------------------------------------------------------
// firewall: fail-closed brand/firewall scan. Missing or empty denylist is a
// build failure, not a clean pass.
// ---------------------------------------------------------------------------
function cmdFirewall(target, denylistPath) {
  const dlPath = denylistPath || join(dirname(target), 'firewall-denylist.json')
  if (!existsSync(dlPath)) {
    print(`FIREWALL FAIL-CLOSED: denylist not found at ${dlPath}. A missing firewall is a build failure.`)
    return 2
  }
  let denylist
  try {
    denylist = JSON.parse(readFileSync(dlPath, 'utf8'))
  } catch {
    print('FIREWALL FAIL-CLOSED: denylist is unparseable.')
    return 2
  }
  const banned = (denylist.banned || []).filter((s) => typeof s === 'string' && s.length > 0)
  if (banned.length === 0) {
    print('FIREWALL FAIL-CLOSED: denylist loaded zero tokens.')
    return 2
  }
  const targets = existsSync(target) ? collectFiles(target) : []
  if (targets.length === 0) {
    print(`FIREWALL: nothing to scan at ${target}.`)
    return 2
  }
  const hits = []
  for (const f of targets) {
    const text = readFileSync(f, 'utf8').toLowerCase()
    for (const tok of banned) {
      if (text.includes(String(tok).toLowerCase())) hits.push({ file: f, token: tok })
    }
  }
  // Positive assertion: the product must contain the product name.
  const allText = targets.map((f) => readFileSync(f, 'utf8')).join('\n').toLowerCase()
  const required = denylist.required || []
  const missingRequired = required.filter((r) => !allText.includes(String(r).toLowerCase()))

  if (hits.length === 0 && missingRequired.length === 0) {
    print(`FIREWALL PASS: ${targets.length} files clean against ${banned.length} banned tokens.`)
    return 0
  }
  for (const h of hits) print(`FIREWALL HIT: "${h.token}" in ${h.file}`)
  for (const r of missingRequired) print(`FIREWALL MISSING REQUIRED: "${r}" not found in shipped product.`)
  return 2
}

// ---------------------------------------------------------------------------
// backtest: estimator variance gate. Refuses PASS until the tool reproduces the
// operator's own past bids within tolerance.
// ---------------------------------------------------------------------------
function cmdBacktest(root, file, tolerance) {
  const tol = Number(tolerance ?? 0.1)
  let bids
  try {
    bids = JSON.parse(readFileSync(file, 'utf8'))
  } catch {
    print(`Cannot read back-test file ${file}.`)
    return 3
  }
  const rows = Array.isArray(bids) ? bids : bids.bids || []
  if (rows.length < 3) {
    print(`Back-test needs at least 3 past bids with known final numbers. Got ${rows.length}.`)
    return 2
  }
  let worst = 0
  const detail = []
  for (const b of rows) {
    const actual = Number(b.actual)
    const estimate = Number(b.estimate)
    if (!actual) continue
    const variance = Math.abs(estimate - actual) / actual
    worst = Math.max(worst, variance)
    detail.push(`  ${b.job || 'job'}: estimate ${estimate}, actual ${actual}, off ${(variance * 100).toFixed(1)}%`)
  }
  for (const d of detail) print(d)
  if (worst <= tol) {
    if (existsSync(stackPaths(root).dot) || existsSync(join(root, '.stack'))) {
      appendReceipt(root, { phase: 'phase-2', kind: 'backtest', verdict: 'PASS', worstVariance: worst, timestamp: nowIso() })
    }
    print(`BACK-TEST PASS: worst case ${(worst * 100).toFixed(1)}% within ${(tol * 100).toFixed(0)}% tolerance.`)
    return 0
  }
  print(`BACK-TEST FAIL: worst case ${(worst * 100).toFixed(1)}% exceeds ${(tol * 100).toFixed(0)}% tolerance.`)
  print('Likely causes: wrong pay regime, inconsistent units in your loaded bids, or too few data points.')
  return 2
}

// ---------------------------------------------------------------------------
// lint: a fast dash gate for any file or folder (em / en / bar dash). One home
// for dash detection, reused by the blueprint phases and the build script.
// ---------------------------------------------------------------------------
function cmdLint(target) {
  if (!target || !existsSync(target)) {
    print(`LINT: nothing to scan at ${target}.`)
    return 2
  }
  const files = collectFiles(target)
  const hits = []
  for (const f of files) {
    const text = readFileSync(f, 'utf8')
    if (text.includes(EM_DASH) || text.includes(EN_DASH) || text.includes(HORIZONTAL_BAR)) hits.push(f)
  }
  if (hits.length === 0) {
    print(`LINT PASS: no em dash or en dash in ${files.length} file(s).`)
    return 0
  }
  for (const f of hits) print(`LINT HIT: em dash or en dash in ${f}`)
  return 2
}

// ---------------------------------------------------------------------------
// init: create a fresh .stack ledger (the blueprint calls this on first run).
// ---------------------------------------------------------------------------
function cmdInit(root, flavor) {
  const p = stackPaths(root)
  mkdirSync(p.health, { recursive: true })
  mkdirSync(join(root, 'foundation'), { recursive: true })
  mkdirSync(join(root, 'tools', 'estimating'), { recursive: true })
  const data = {
    stackVersion: '1.0.0',
    blueprintContentHash: process.env.OPERATOR_STACK_BLUEPRINT_HASH || 'dev',
    operatorRoot: root,
    flavor: flavor || 'construction',
    createdAt: nowIso(),
    lastTouchedAt: nowIso(),
    machineId: process.env.OPERATOR_STACK_MACHINE || 'machine-1',
    phases: PHASE_ORDER.map((id) => ({ id, status: 'not-started', idempotencyKeys: {}, artifacts: [] })),
    decisions: [],
    facts: {},
  }
  writeLedger(root, data)
  if (!existsSync(p.decisions)) writeFileSync(p.decisions, decisionsTemplate())
  writeBuildPlan(root)
  print(`Initialized .stack at ${p.dot} (flavor: ${data.flavor}).`)
  return 0
}

// The canonical DECISIONS.md template. Three sections so a resumed session knows
// exactly where to log what, and never re-asks: safe defaults Claude chose for
// the operator, explicit choices the operator made, and the one hard fork (pay
// regime) that has no silent default.
function decisionsTemplate() {
  return `# Decisions log

What you told me, so a fresh session never re-asks.

## Recommended defaults (safe choices, never re-ask)
- (list grows as you build)

## Your explicit choices (you said this, hold it)
- (list grows as you build)

## Hard fork answers (no silent default)
- Pay regime: (captured in phase-2)
`
}

// ---------------------------------------------------------------------------
// capability: the write-access preflight. The single most important honesty
// gate. A Claude that cannot write files and run the checker (the web app and
// the desktop app are the common cases) or a too-old Node both fail here
// with a plain-language message BEFORE any bootstrap touches the disk, so the
// operator is routed to the concierge hatch instead of a silent dead end.
// Idempotent and fail-safe: it writes one tiny temp file, reads it back, deletes
// it, and leaves nothing behind on either the pass or the fail path.
// ---------------------------------------------------------------------------
function cmdCapability(root) {
  // 1. Node major must be 18 or higher.
  const versionStr = process.version
  const major = parseInt(String(versionStr).replace(/^v/, '').split('.')[0], 10)
  if (!Number.isFinite(major) || major < 18) {
    print(`CAPABILITY FAIL: Node ${versionStr} is too old. You need version 18 or higher.`)
    print('This is not something you did wrong, it is just the setup. Go to the concierge hatch and a human gets you to a capable machine in minutes.')
    return 2
  }
  // 2. A real write, read-back, and delete in the working folder.
  const dir = existsSync(root) ? root : process.cwd()
  const testPath = join(dir, '.operator-stack-capability-test')
  try {
    const stamp = 'capability-' + Date.now()
    writeFileSync(testPath, stamp)
    const readBack = readFileSync(testPath, 'utf8')
    rmSync(testPath)
    if (readBack !== stamp) {
      print('CAPABILITY FAIL: a file was written but did not read back the same. This environment cannot reliably build the stack.')
      print('Go to the concierge hatch. This is the environment, not you.')
      return 2
    }
  } catch (e) {
    // Best-effort cleanup so a half-written test file never lingers.
    try {
      if (existsSync(testPath)) rmSync(testPath)
    } catch {
      // ignore
    }
    print(`CAPABILITY FAIL: cannot write files here. Claude cannot build the stack in this environment. Error: ${e.message}`)
    print('You are most likely in a Claude that cannot build the stack here, like the web app or the desktop app. Both are great for everyday Claude chat, they just cannot run the checker. Open Claude Code in the terminal, where the checker can run, then try again. Or go to the concierge hatch.')
    return 2
  }
  print(`CAPABILITY PASS: Node ${versionStr}, file write and read OK. This machine can build the stack.`)
  return 0
}

// ---------------------------------------------------------------------------
// plan-bootstrap: the permission handshake. Prints the exact list of files and
// folders the first run will create, the total count, and that nothing is
// deleted and everything is reversible. The prose makes Claude show this and
// wait for a yes before writing. Read-only: this command creates nothing.
// ---------------------------------------------------------------------------
function planBootstrap(root) {
  const p = stackPaths(root)
  return [
    { path: p.state, desc: 'state ledger (your progress)' },
    { path: p.decisions, desc: 'decisions log (what you tell me)' },
    { path: p.receipts, desc: 'receipts trail (every grading run)' },
    { path: p.buildPlan, desc: 'build plan (the session-by-session roadmap)' },
    { path: join(root, '.stack', 'verify.mjs'), desc: 'the deterministic judge' },
    { path: join(root, 'foundation', 'Operating-Constitution.md'), desc: 'your constitution' },
    { path: join(root, 'foundation', 'facts-registry.md'), desc: 'your facts' },
    { path: join(root, 'foundation', 'cold-start.md'), desc: 'the every-session loader' },
    { path: join(root, 'tools', 'estimating', 'estimating-inputs.md'), desc: 'your estimator inputs' },
    { path: join(root, '.gitignore'), desc: 'git safety (keeps your pricing private)' },
  ]
}

function cmdPlanBootstrap(root) {
  const plan = planBootstrap(root)
  print('Here is what the first run will create:')
  for (const item of plan) {
    print(`  ${item.path} (${item.desc})`)
  }
  print(`Total: ${plan.length} files and folders will be created.`)
  print('No existing files will be deleted. Everything is reversible: delete the stack folder and you are back to a clean machine.')
  return 0
}

// ---------------------------------------------------------------------------
// BUILD_PLAN.md: a visible, session-by-session roadmap written once at init. It
// does not change as you build; STATE.json is what tracks which phase is done.
// It names the one hard fork (pay regime) so the operator sees it coming.
// ---------------------------------------------------------------------------
function writeBuildPlan(root) {
  const p = stackPaths(root)
  if (!existsSync(p.dot)) mkdirSync(p.dot, { recursive: true })
  const plan = `# Build Plan

Your operator stack gets built one phase at a time. Here is the whole roadmap so you always know what is coming and roughly how long each piece takes. This file does not change as you go. Your actual progress lives in .stack/STATE.json.

## Phases

phase-0: Install and verify. (1 to 2 minutes, one time)
- Detect your operating system.
- Install Node 22 and Claude Code.
- Verify they run. Concierge hatch if anything blocks you.

phase-1: Minimum-viable foundation. (about 10 minutes)
- Capture your name, company, trade, and pay regime.
- Write Operating-Constitution.md, facts-registry.md, cold-start.md.
- Read the facts back to you so you confirm they are right.

phase-2: Your first real tool, the estimator. (20 to 30 minutes)
- The one hard fork: pick your pay regime (private open shop, prevailing wage, or union).
- Capture your rates and a few past bids.
- Write estimating-skill.md and estimating-inputs.md.
- Back-test: the estimator must reproduce your own past bids within tolerance before it can pass.
- On pass, you have a real estimator you can trust. This is the win.

phase-2b: Personalize for HOA facade work. (optional, 15 to 20 minutes)
- Only if most of your work is HOA facade, exterior defect, or weatherproofing.
- Load an exterior catalog and California open-shop rates, tune them to your suppliers.
- If this is not your main work, skip it. The phase-2 estimator is already complete on its own.

phase-3: Deepen the foundation. (optional, one sitting at a time)
- Memory pointers, deeper voice and rules, hooks, daemons, the jury.

phase-4: More tools. (optional, as your work needs them)
- Proposal Builder, then Client Research.

## The one hard fork

The pay regime in phase-2 is the only choice with no safe silent default, because a wrong regime can misprice a real bid. If you are unsure, Claude runs a short two-question diagnostic instead of guessing.

## Where you are

Your progress is tracked in .stack/STATE.json. Each phase is not-started, in-progress, verified, or needs-repair. If you stop mid-phase and come back later, the next session reads STATE.json and the receipts and picks up exactly where you left off. It never re-asks what you already answered.

## How resume works

Every session, Claude runs: node .stack/verify.mjs reconcile

That prints a table of what is verified and what is next. Never build on a phase that says needs-repair until you have repaired it.
`
  writeFileSync(p.buildPlan, plan)
}

// ---------------------------------------------------------------------------
// closeout: the finished-stack README, written when the last phase passes. The
// operator sees "you built this" and exactly how to use, resume, and extend it,
// in plain English. Quiet Bernan credit, no EmpireWorks-gift provenance line.
// ---------------------------------------------------------------------------
function writeCloseout(root) {
  const p = stackPaths(root)
  if (!existsSync(p.dot)) mkdirSync(p.dot, { recursive: true })
  const finished = `# Your Operator Stack is Complete

You now have a Claude that knows your construction business by name, knows your trade, and prices jobs from your real numbers instead of guessing.

## What you built

Foundation (foundation/)
- Operating-Constitution.md: your company, your trade, your rules.
- facts-registry.md: name, company, trade, pay regime, gross profit default.
- cold-start.md: the instruction Claude reads at the start of every session.

Estimating (tools/estimating/)
- estimating-skill.md: the tool that prices jobs.
- estimating-inputs.md: your rates and past bids (the back-test data).

Ledgers (.stack/)
- STATE.json: your progress (which phases are verified).
- DECISIONS.md: what you told Claude (so it never re-asks).
- RECEIPTS.jsonl: the audit trail (every grading run).
- BUILD_PLAN.md: the roadmap you followed.

## How to use the estimator

1. Open Claude Code in the terminal, in your stack folder. That is where your stack lives, so it is where the estimator runs.
2. Load the estimator: tools/estimating/estimating-skill.md
3. Give it a job scope (what, where, size, finish).
4. It prices line by line and says confirm before bidding.
5. Every estimate stamps your pay regime.

## How to resume and extend

To pick up later or add more, run: node .stack/verify.mjs reconcile

That shows what is verified and what phase is next. Reply to Claude and it continues at the exact next step. You can add more tools anytime (Proposal Builder, Client Research, anything else your work needs), or deepen the foundation in phase-3 (memory, rules, hooks, daemons). Each new piece follows the same pattern: ask, capture, write, health-check.

## Your data

Everything is on your machine, in the stack folder. You own it. No cloud, no account, no subscription.

An AI is probabilistic, so you put deterministic gates around it.

By Eugeen Bernan.
`
  writeFileSync(p.finishedReadme, finished)
}

// ---------------------------------------------------------------------------
// Small helpers.
// ---------------------------------------------------------------------------
function upsertPhase(data, phase) {
  data.phases = data.phases || []
  const i = data.phases.findIndex((ph) => ph.id === phase.id)
  if (i >= 0) data.phases[i] = phase
  else data.phases.push(phase)
}

function writeHealth(root, phaseId, body) {
  const p = stackPaths(root)
  if (!existsSync(p.health)) mkdirSync(p.health, { recursive: true })
  writeFileSync(join(p.health, `${phaseId}.health.md`), `# ${phaseId} health\n\n${body}\n`)
}

function readLock(root) {
  const p = stackPaths(root)
  if (!existsSync(p.lock)) return null
  return safe(() => JSON.parse(readFileSync(p.lock, 'utf8')), { phase: 'unknown' })
}

function clearLock(root) {
  const p = stackPaths(root)
  try {
    if (existsSync(p.lock)) rmSync(p.lock)
  } catch {
    // ignore
  }
}

function collectFiles(target) {
  const out = []
  const stat = safe(() => readdirSync(target, { withFileTypes: true }), null)
  if (stat === null) {
    // It is a file, not a dir.
    return [target]
  }
  for (const ent of stat) {
    const full = join(target, ent.name)
    if (ent.isDirectory()) {
      if (ent.name === 'node_modules' || ent.name === '.git') continue
      out.push(...collectFiles(full))
    } else {
      out.push(full)
    }
  }
  return out
}

function pad(s, n) {
  s = String(s)
  return s.length >= n ? s.slice(0, n) : s + ' '.repeat(n - s.length)
}

function print(line) {
  console.log(line)
}

// ---------------------------------------------------------------------------
// CLI.
// ---------------------------------------------------------------------------
function argval(flag, fallback) {
  const i = process.argv.indexOf(flag)
  return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback
}

function main() {
  const cmd = process.argv[2]
  const root = argval('--root', process.env.OPERATOR_STACK_ROOT || process.cwd())
  switch (cmd) {
    case 'reconcile':
      return cmdReconcile(root)
    case 'check':
      return cmdCheck(root, process.argv[3])
    case 'confirm':
      return cmdConfirm(root, process.argv[3])
    case 'firewall':
      return cmdFirewall(process.argv[3], argval('--denylist', null))
    case 'lint':
      return cmdLint(process.argv[3])
    case 'backtest':
      return cmdBacktest(root, process.argv[3], argval('--tolerance', null))
    case 'init':
      return cmdInit(root, argval('--flavor', null))
    case 'capability':
      return cmdCapability(root)
    case 'plan-bootstrap':
      return cmdPlanBootstrap(root)
    default:
      print('Usage: verify.mjs <reconcile|check|confirm|firewall|backtest|lint|init|capability|plan-bootstrap> [args]')
      return 3
  }
}

process.exit(main())
```
