# Playbook kit — running the code-modernization playbook on SteelFrame

A project skeleton a team copies into its own repository to follow the four phases of Anthropic's code
modernization playbook (understand → rank the risk → tests before code → one component at a time, validated
continuously) with Claude Code on a SteelFrame estate reached over MCP. SteelFrame is CobolStack's clean-room
z/OS-compatible engine; it is aligned with the practices in the playbook and is not affiliated with or endorsed
by Anthropic or IBM. Live docs: https://steelframe.cobolstack.com/playbook.html

What is in the box:

| path | what |
|---|---|
| `CLAUDE.md` | the standing rules for Claude Code on this estate: which MCP server for which phase, workspaces, pinned clocks, costs, facts-not-verdicts, never the live estate |
| `.mcp.json` | two servers: `steelframe` (persistent lane, reads) and `steelframe-private` (`?lane=ephemeral`, RAM-only, every run); the key comes from `SF_TOKEN` in your environment |
| `.claude/settings.json` | a deny list that makes "an agent never changes the live estate" enforced, not advisory (Claude Code evaluates `permissions.deny` before anything runs) |
| `.claude/skills/sf-*` | one skill per phase: `/sf-discover`, `/sf-risk-rank`, `/sf-characterize`, `/sf-port`, `/sf-compare` |
| `tools/sfmcp.mjs` | one MCP tool call from a shell or CI (zero dependencies, Node 18+) |
| `tools/characterize.mjs` | workspace → prepare → seed → `run_batch` with a pinned clock → `fixtures/<name>/fixture.json` |
| `tools/fixture.mjs`, `tools/fixture-diff.mjs` | the `steelframe-fixture/1` format and the comparison (verdicts per artifact) |
| `carddemo-posttran/` | the worked example: CardDemo's POSTTRAN (CBTRN02C) — discovery note, characterization spec, the captured fixture, a Python port, tests that load the fixture |

## Ten minutes to a passing characterization test

1. **Get a key.** Create a SteelFrame user (Get User ID on https://steelframe.cobolstack.com/), then on your
   account page https://learn.cobolstack.com/account/#apikeys mint an API key with the **Developer** preset.
   It carries `ephemeral.exec` (RAM-only workspaces), `jobs.write`, `datasets.write` and the reads. The key is
   shown once.
2. **Put it in the environment**, never in a file: `export SF_TOKEN=sf_...` (and `SF_URL` only if you self-host).
3. **Run the worked example's capture** (Node 18+):
   ```
   node tools/characterize.mjs carddemo-posttran/posttran.characterize.json --out carddemo-posttran/fixtures
   ```
   It creates a RAM-only copy of the estate, defines an empty transaction master and the rejects GDG base under
   your own HLQ (what CardDemo's TRANBKP does before POSTTRAN; the installed masters are UPDATE-only for ordinary
   users and the transaction master may already be loaded), seeds seven daily transactions, runs the POSTTRAN
   deck with those three files pointed at your HLQ and the clock pinned to 2022-07-18 10:20:30, writes
   `carddemo-posttran/fixtures/posttran-d002/fixture.json` and destroys the workspace. Priced per the price
   list (three billable calls, about $0.09); a new account's signup credit covers it. If the run fails the tool
   says `RUN FAILED` with the joblog tail and exits 3 — the fixture is still written, but it is not a baseline.
4. **Run the port's tests on your own capture** (Python 3.10+, stdlib only):
   ```
   cd carddemo-posttran/python && python3 -m unittest -v test_posttran
   ```
   Seven tests: return code, SYSOUT lines, the transactions written, the account master after, the category
   balances after, the reject file byte for byte, and the roles the oracle reported. The tests read
   `../fixtures/posttran-d002/fixture.json`; point them at any other capture with `FIXTURE=path/to/fixture.json`.
5. **Open the folder in Claude Code** (`claude`), approve the two MCP servers, and start where the playbook
   starts: `/sf-discover CBTRN02C`, then `/sf-risk-rank`, then your own component.

## How the phases map (what each skill calls)

| playbook phase | skill | SteelFrame tools |
|---|---|---|
| 1 Discovery | `/sf-discover` | `find_program`, `read_member`, `dataset_info`, `decode_records`, `analysis_graph`, `analysis_impact`, `analysis_metrics`, `list_datasets` |
| 2 Risk | `/sf-risk-rank` | `inventory_scan`, `analysis_metrics`, `analysis_graph` |
| 3 Tests first | `/sf-characterize` | `create_workspace` (ephemeral lane) → `run_batch` (prepare) → `load_dataset` → `run_batch {before, records, clock}` → `destroy_workspace` |
| 4a Port | `/sf-port` | your toolchain (or the `steelframe-vscode` server inside a SteelFrame seat) |
| 4b Compare | `/sf-compare` | `tools/fixture-diff.mjs`; the engine's own `run_capture` → compare for legacy-side regression |

## Things you will hit, and what they mean

- **403 `KEY_SCOPE` on `create_workspace`** on the `steelframe` server: workspaces on the persistent lane need
  an administrator scope. The kit never uses them; every run goes through `steelframe-private` (the ephemeral
  lane), where a Developer key is enough.
- **`status: CREATING`**: poll `get_workspace` every 10–15 s; do not create again (the `name` returns the same
  one). **429**: you or the plane are at the workspace cap — wait, poll, destroy what you no longer need.
- **JCL ERROR with `GDG BASE NOT CATALOGED`**: the deck writes a `(+1)` generation whose base the estate does not
  have. Add an IDCAMS `DEFINE GENERATIONDATAGROUP` step to the spec's `prepare` (the worked example shows the
  idempotent form).
- **Abend with `FILE STATUS IS: NNNN0037` on an OPEN OUTPUT**: the program loads a VSAM cluster that is
  already loaded and was defined without REUSE — VSAM refuses that (RC 8 reason 232), and so does SteelFrame.
  Deleting the records is not enough; the cluster must be re-defined, and on a shared estate the installed
  masters are UPDATE-only for ordinary users (no DELETE/DEFINE, even inside your workspace — RACF is copied
  with the estate). Point the DDs the job WRITES at datasets under your own HLQ (`{{USER}}` in the spec is
  replaced by your user id) and define them in `prepare`; the worked example does exactly that.
- **`whoami` on `steelframe-private` answers 403**: on the ephemeral lane every tool runs inside a workspace;
  identity questions go to the `steelframe` server.
- **A second capture differs from the shipped fixture** in `ACCTDATA` / `TCATBALF` rows or in the DALYREJS
  generation number: the hosted estate's reference files are whatever the day left them (other users, course
  exercises); the tests still pass against your own capture because before- and after-state come from the same
  file. Compare two captures of your own for regression; compare the shipped one only to see the drift.
- **`CREDIT_REQUIRED`**: the key's wallet is at $0 — top up on the account page; do not retry.
- **Numbers**: decoded rows carry a JSON number when a double carries the exact decimal, else the exact decimal
  string; compare as decimals.
- **Two captures differ** in a timestamp the clock could not pin or in a JOBID: that is a fact to mask, not a
  failure. `fixture-diff` shows the first differing line/row so you can decide.

## Costs and limits (SteelFrame credit, read on 2026-10-21)

Reads, lists and status free; writes, compiles and SQL $0.03 per call; a run $0.03 + $0.99 per core-hour after
the first 10 s of CPU; workspaces unmetered; failed and refused calls free. Keys expire (default 90 days), carry a
rolling 24-hour spend cap and an optional IP allow-list. Per user 2 workspaces, per plane 2 RAM sandboxes by
default. Everything the key does is audited as your user id (tool, route, status; never content).

## Adapting the kit to your estate

Replace `carddemo-posttran/` with your first component: run `/sf-discover` on it, write its
`*.characterize.json` (job, clock, copybooks, prepare, seed), capture, then port with tests. Keep `CLAUDE.md`
and the deny list; add your own conventions below the rules. If your estate is reached through your own
SteelFrame plane or a self-hosted engine, set `SF_URL`.

Licence: the kit's own files are MIT (see LICENSE); the CardDemo material it refers to is Amazon's AWS
CardDemo (Apache-2.0), used as the estate's sample application.
