# Argus extraction v1 — the owner's steps after the GREEN phase (2026-09-07)

*Written after R0–R3 and G1–G3 were delivered on `ops/argus-extraction-v1-red` (head after this guide's commit).
The record is `tests/Sibyla.Tests.Argus/evidence/extract-v2/RED-red.txt` and `RED-green.txt`. Every step below is
either a database action, a sitting on the pinned CLI under your own login, or a review — things the build session
may not do (hand-off hard rules). Run them in this order; each says what to expect and where its record lands.*

Repo: `D:\fileStorage\repos\GOTT.Sibyla`, branch `ops/argus-extraction-v1-red`. Commands are Windows PowerShell 5.1
unless said otherwise. Nothing here deploys anything.

**The order the owner set on 2026-09-07 (~10:40 UTC), after ruling D-EX-5.** Steps 1–4 are independent of D-EX-5 —
they measure the extraction contract itself and never read the company gate — and step 4 is the long pole (two
five-hour windows), so they start now. In parallel the build session takes D-EX-5 as G3's next step: the register
fiscal-key lookup in the gate, the hold as a possible duplicate OF the register entry, the ruling buttons, the
verified genuine repeat in the register's own vocabulary, with the intake-to-entry reference as a **second migration**
(`ExtractionV2RegisterReference`) that you apply to Main the same way as step 1 — `.\local\migrate.ps1 -Target All`
again once it is on the branch; Preview gets it from the build session. Steps 6 and 7 depend on D-EX-5: the checkpoint
covers the whole record and the release is one id for web, api and worker, so the register branch is built and green
before the reviewers start. Then step 5, one checkpoint over G1–G3 including the register branch, one release.

## 1. Migrate Main (five minutes)

The v2 schema adds one nullable column, `docint.hold_reason` (migration `20260907094356_ExtractionV2HoldReason`).
It is applied to the Preview database already; Main is yours (spec §4.6 step 1).

```powershell
cd D:\fileStorage\repos\GOTT.Sibyla
git checkout ops/argus-extraction-v1-red
.\local\migrate.ps1 -Target All
.\local\test.ps1
```

Expect: `== Main ==` applies `ExtractionV2HoldReason` (Preview reports nothing pending), then the suite is fully green —
Platform 332, Browser 133, Argus 193, Api 56, TenantIsolation 58 (the parity test was the one red test until now).
Nothing else changes on Main: the old release never reads the column, and rollback needs no schema step.

## 2. The reserve keys and the sync run id (ten minutes, SELECT-only)

The ten reserve documents (spec §5.6) are named in `tests/Sibyla.Tests.Argus/golden/extract-v2/reserve-set.csv` by entry
code and doclog code; their keys are exported with the same SELECTs as the sample keys, through the lookup script you
used on 2026-09-06 (it reads `local\secrets\sibyla-db.json` itself):

```powershell
$g = 'D:\fileStorage\repos\GOTT.Sibyla\tests\Sibyla.Tests.Argus\golden\extract-v2'
$env:PGCLIENTENCODING = 'UTF8'   # the sample keys were exported this way; without it psql writes the console code page
powershell -File D:\fileStorage\tmp\apollo-c2d2-final\lookup\lookup.ps1 -SqlFile "$g\reserve-keys\_sql\reserve-invoices.sql"
powershell -File D:\fileStorage\tmp\apollo-c2d2-final\lookup\lookup.ps1 -SqlFile "$g\reserve-keys\_sql\reserve-bank.sql"
python "$g\reserve-keys\_sql\build_reserve_keys.py"
powershell -File D:\fileStorage\tmp\apollo-c2d2-final\lookup\lookup.ps1 -SqlFile "$g\reserve-keys\_sql\sync-run-id.sql"
```

Expect: `built 10 missing 0`, ten JSON files under `reserve-keys\` (never opened while the package, EXTRACT.md, model or
effort is being tuned — the bench reads them only with `--set reserve` at R-EX-3). The last query prints the latest
successful sync run before the keys were read: paste its `id` into `answer-key.meta.json` → `source.syncRunId`. Commit
the reserve keys, the `_sql\*.out.jsonl` outputs and the meta edit on the branch.

## 3. The R-EX-2 probes on the pinned CLI — **done 2026-09-07**

All four questions are answered and one of them found a defect in the slice's own code: the deny rules were being
written as `Read(//D:/path/**)`, which matches nothing on 2.1.259, so a file denied by rule was read. The unprefixed
form binds; `PermissionsFile.Rule` writes it now, and a re-run of the three probes is clean. `--settings` does apply
under `--safe-mode`, `stream-json` needs `--verbose`, and the package reads work. The record and the traces are in
`tests\Sibyla.Tests.Argus\evidence\extract-v2\probes-20260907-114951\RECORD.md`. Nothing is left for you here; the
steps below are kept for the next time the tuple changes (Q-EX-14 re-runs the probes with it).

### How it was run (for a re-run)

Three probes settle the three things the spec left to the binary (spec §4.5): the Windows `Read(...)` rule syntax,
whether `--settings` still applies under `--safe-mode`, and whether `stream-json` needs `--verbose`. They run under
**your own** `CLAUDE_CONFIG_DIR` (never the worker's `D:\ApolloData\worker-claude`), on the pinned binary, with the
production flag set, over a copy of one sample PDF in a temporary root.

**First, the login — this is a prerequisite, not a detail.** Sitting 1 on 2026-09-07 ran the three probes correctly and
every one stopped at `Not logged in · Please run /login`, so three of the four questions went unanswered
(`tests\Sibyla.Tests.Argus\evidence\extract-v2\probes-20260907-112755\RECORD.md`). The same login is what the step-4
bench sittings run under, so it is needed either way. Once, interactively:

```powershell
$env:CLAUDE_CONFIG_DIR = 'D:\fileStorage\tmp\apollo-extraction\bench-config'
C:\Apps\Sibyla\tools\claude\claude.exe        # /login, then exit
```

Sitting 1 did answer the third question and it is already applied: **`stream-json` needs `--verbose` on 2.1.259** (the
CLI refuses before starting without it), so `ClaudeCliProcess.StreamJsonNeedsVerbose` is now `true`. The production
flag set is unchanged — it runs `--output-format json` — and the bench, which reads the trace, gets the flag. Without
that fix every bench document in step 4 would have failed on the first call.

```powershell
cd D:\fileStorage\repos\GOTT.Sibyla
powershell -NoProfile -ExecutionPolicy Bypass -File tools\skill-package\Invoke-PermissionProbes.ps1 `
  -ConfigDir D:\fileStorage\tmp\apollo-extraction\bench-config -Model <the full model id> -Effort <the effort>
```

Expect a `PROBES.md` under `tests\Sibyla.Tests.Argus\evidence\extract-v2\probes-<stamp>\` with a table and three token
checks, all "not seen (ok)": the sibling settings file refused (probe a), the planted file inside the sandbox refused
while `document.pdf` reads (probe a′), no planted `CLAUDE.md` content in the answer (probe b). Read the three stdout
files, fill in the four-line owner's record at the bottom of `PROBES.md`, and commit the folder.

If a token IS seen, or a probe exits non-zero: the answers change two constants and nothing else — the rule form in
`PermissionsFile.Rule` (`src/Sibyla.Worker.Documents/ClaudeCli.cs`) with the `Provisional_…` test in
`ClaudeCliPermissionTests`, and `ClaudeCliProcess.StreamJsonNeedsVerbose` (the script says in its table whether a
re-run with `--verbose` was needed). Tell the build session what the probes showed and it makes the amendment.

## 4. The R-EX-3 bench sittings (two sittings, each within one five-hour subscription window)

The bench runs `ExtractionRunner` over the sample files under the production flag set with the `stream-json` trace,
needs no database, and refuses anything outside the tuple (spec §5.7). Do a dry run first — it starts no CLI:

```powershell
cd D:\fileStorage\repos\GOTT.Sibyla
dotnet build Sibyla.slnx -c Release
$bench = 'tools\extraction-bench\Sibyla.Tools.ExtractionBench'
dotnet run --no-build -c Release --project $bench -- run --dry-run `
  --golden tests\Sibyla.Tests.Argus\golden\extract-v2 `
  --skill-build D:\fileStorage\repos\invoice-skill-build --commit a558523f3f0ad97e6c3f60707d635b3d257d3392 `
  --package src\Sibyla.Worker.Documents\skill `
  --cli C:\Apps\Sibyla\tools\claude\claude.exe --cli-version 2.1.259 `
  --model <the full model id> --effort <the effort> `
  --config-dir D:\fileStorage\tmp\apollo-extraction\bench-config `
  --work D:\fileStorage\tmp\apollo-extraction\bench-work `
  --evidence tests\Sibyla.Tests.Argus\evidence\extract-v2 `
  --set fiscal
```

Expect exit 0: the version, the package tree hash against the golden manifest, the skill-build clean at the pinned
commit, 35 documents listed with their file hashes verified. Then the same line without `--dry-run`, first
`--set fiscal` (35 documents), later `--set statements` (5). Add `--resume` to continue a stopped run (a usage-limit
message stops the bench; it never pauses and retries).

Each run writes `score-<stamp>-<apollo sha>-<skill sha>.json` and `.md` under the evidence folder (the gate per kind
at 95 %, the per-field floors, the misses with their page refs from the flags file, the configuration tuple, your
config dir as the bench identity) and `actual\<code>.json` per document. Read the `.md`: the misses are the tuning
list for the package and `EXTRACT.md`. A miss that is the key's fault is appealed with a page reference as a row in
`answer-key.corrections.csv` (spec §5.5), never by editing a key. Tuning the package changes its tree hash, so
`Build-SkillPackage.ps1 -UpdateManifest`, re-commit the package, and re-pin the golden manifest — the build session
does that on request. The **model id** to carry forward is the one the first run prints from `modelUsage`; the
**effort** is what you passed.

~~When both kinds pass, freeze the tuple and run `--set reserve` once (needs step 2). A reserve field under
85 % with 20 or more cells blocks the gate until explained.~~

**Superseded 2026-09-08 by ruling D-EX-7 — do not run `--set reserve` again.** All three sittings have been
run. The reserve was read on 2026-09-07 at 16:13, its keys were then amended on the strength of those answers
(ruling D-EX-6: three flag rows, one correction, and the new rule S-12), and it was read again at 22:50. The
second run’s 100 % is a second read scored against a key fitted to the first, so it is a measured result and
not a blind one, and the set is spent as a control. The blind run’s own numbers were header 76/80 = 95.00 %,
lines 27/51 = 52.94 %, movements 158/158.

The slice therefore has **three measured sittings and no held-out result**, which the spec now says in as many
words. Sittings 1 and 2 were always tuning sittings by §5.7, so their numbers are unaffected. A genuine
held-out measurement belongs to the persistence slice, with a set sealed before anyone reads it — not to a
set that has now been read twice.

### A sitting now prints FAILED, and that is correct

The checkpoint of 2026-09-08 found that each kind’s rate was being tallied over only its fields with
twenty or more cells, where the spec defines it over all of that kind’s cells and puts the twenty-cell
rule on the per-field floor alone. Both reviewers found it independently. Correcting it has a consequence
you will see the next time you run the bench:

- **A fiscal sitting has no movement cell; a statements sitting has no header or line cell.** A kind with
  nothing scored is now a gate *failure* with that reason, so `-Set fiscal` and `-Set statements` both
  print **FAILED** and exit non-zero. Nothing is wrong. Neither sitting can meet three gates on its own,
  and the old code printed PASSED over the two kinds it had not measured.
- **The gate is read off one combined report over the whole sample.** It scores the answers the sittings
  already kept, calls no model, and takes about a second:

```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tools\extraction-bench\Score-Combined.ps1
```

  The committed one is `tests\Sibyla.Tests.Argus\evidence\extract-v2\combined-20260908-fiscal-statements-a558523.md`:
  40 documents, header 445/447 = 99.55 %, lines 342/347 = 98.56 %, movements 1349/1419 = 95.07 %, **PASSED**.

  Movements clear the bar by 0.07 points. That margin is one cell, and it is decided by five movements on
  two BPI statements whose posting date is one day out — the blank movement-date column described in §8.4
  below. It is a true number, and it is thin.

## 5. Housekeeping the spec asks of you

Purge the CLI transcripts the deployed worker persists under `D:\ApolloData\worker-claude\projects` (Q-EX-22). Each
transcript holds the tool results of its session, which is the full text of a client document, and they accumulate
per job without limit. `CLAUDE_CONFIG_DIR` is set to `D:\ApolloData\worker-claude` as a service environment value
(`provision-production.ps1`), and nothing in this work reads that directory — this step is yours alone.

**`projects` only.** The login sits beside it in the same config dir. Deleting the config dir itself, or anything
next to `projects`, signs the worker out and every extraction then fails at "Not logged in — Please run /login".

```powershell
$p = 'D:\ApolloData\worker-claude\projects'

# 1. What is there. This deletes nothing.
$f = @(Get-ChildItem $p -Recurse -File -Force -ErrorAction SilentlyContinue)
'{0} files, {1:N1} MB, {2} session folders, {3:yyyy-MM-dd} to {4:yyyy-MM-dd}' -f $f.Count,
  (($f | Measure-Object Length -Sum).Sum / 1MB), @(Get-ChildItem $p -Directory -Force).Count,
  ($f | Sort-Object LastWriteTime | Select-Object -First 1).LastWriteTime,
  ($f | Sort-Object LastWriteTime | Select-Object -Last 1).LastWriteTime

# 2. Stop the worker so nothing is mid-write, and no new job starts during the purge.
Stop-Service Sibyla.Worker.Documents

# 3. Empty the folder, keeping the folder itself and everything beside it.
Get-ChildItem $p -Force | Remove-Item -Recurse -Force -Confirm:$false

# 4. Prove it: no files left, and the login files still present next door.
@(Get-ChildItem $p -Recurse -File -Force).Count
Get-ChildItem 'D:\ApolloData\worker-claude' -Force | Select-Object -ExpandProperty Name

# 5. Back on.
Start-Service Sibyla.Worker.Documents
(Get-Service Sibyla.Worker.Documents).Status
```

Step 4 must still list the login (a `.credentials.json` or equivalent) and `projects` must still exist and be empty.
A job the worker was leasing when it stopped is redelivered by the queue; nothing is lost by stopping it.

**It refills until the release.** `--no-session-persistence` ships with the v2 worker, so the currently deployed
worker keeps writing a transcript for every document it processes. Purging now clears the backlog; the folder only
stays empty from step 7 onward. Run the same purge once more after the release, then never again.

- Keep `local\skill-build.path` pointing at `D:\fileStorage\repos\invoice-skill-build` (the package test reads it).
- **After the checkpoint closes**, the bench run folders under
  `tests\Sibyla.Tests.Argus\evidence\extract-v2\score-*\` can go the same way: they hold the raw CLI output of the
  50 sample documents, tens of megabytes of client text, and they are untracked (the reports beside them are the
  committed evidence). Keep them until both reviewers have finished with them — they are the traces the checkpoint
  reads — and ask the build session to remove them by name rather than by wildcard.

## 6. The independent checkpoint review

When steps 1–4 are done, ask the build session for the checkpoint: two fresh reviewer agents over the whole record —
`RED-red.txt`, `RED-green.txt`, `red-notes-260907\`, the probes folder, the score reports — against spec §8 (R-EX-2 and
R-EX-3). The candidate findings are already listed in the two evidence files (the withdrawn Telles correction, 16 not
18 movement corrections, the scorer's gate tally over gated fields only, the mask patterns kept outside the package, the
V-15/V-16/V-17 readings, the held-statement refusal wording). Merge to main only if both ACCEPT.

## 7. The release (only after the reviewers accept)

**Rewritten 2026-09-08.** What stood here told you to copy the six release facts into
`appsettings.Production.json` before publishing. **Do not do that.** Since the change of 2026-09-08 the
release carries those values itself and the worker refuses to start if anything on the host disagrees with
them, so following the old instruction would have stopped your document pipeline. §8.2 is the step that
matters now: take them *out* of the host files.

### 7.1 Before you start

1. Both reviewers accept (§6).
2. **Main is migrated** — three v2 migrations, §8.1. The deployment module checks the schema in its install
   phase, before it writes anything, so an unmigrated Main fails the deploy having touched nothing. That is a
   clean failure, not a broken system, but it wastes a window.
3. **The six release facts are gone from the host** — §8.2. This is the one that stops the worker.
4. The release facts are already in the branch, in `src/Sibyla.Worker.Documents/appsettings.json`,
   verified at publish time against the pinned golden manifest. You do not type them anywhere.

### 7.2 Prepare (nothing here touches production)

```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File local\deploy\Prepare-Deploy.ps1
```

Both destination paths must be fresh; existing directories or worktrees are refused without removal.
Pass `-GateClone` and `-Candidate` to choose fresh destinations for a subsequent attempt.
It makes a detached local clone of the commit to deploy, publishes one clean release from it, and builds a
detached candidate worktree at the same commit with the module’s fingerprint equal to the clean export. It
prints distinct paths and commit IDs (never pass a path as a commit):

```
GATE_COMMIT=<sha>              -> -ExpectedCommit
RELEASE=<release id>
STAGING=<path>                 -> -StagingRoot
CANDIDATE_PATH=<path>          -> -WorktreeRoot
CANDIDATE_COMMIT=<sha>         -> -ExpectedCandidateCommit
CANDIDATE_FINGERPRINT=<sha256> -> -ExpectedCandidateFingerprint
```

This script lived only in `D:\fileStorage\tmp\apollo-deploy\` until 2026-09-08, which meant a cleared
temporary folder left no documented path to production. It is in the repository now.

### 7.3 Execute

Use the complete command printed by Prepare, which invokes the tracked `e2-launch-v2.ps1` launcher.
In an elevated interactive console, first change its `-Step Execute` to `-Step WhatIf` and read the plan.
Only after reviewer acceptance, Main migration parity, configuration checks and the agreed maintenance
window or the successful-renewal exception below, run the emitted command unchanged with `-Step Execute`.
It supplies `-WorktreeRoot` (candidate path), `-RepositoryRoot` (gate clone), `-StagingRoot`,
`-ExpectedCommit` (gate SHA), `-ExpectedCandidateCommit` (candidate SHA),
`-ExpectedCandidateFingerprint`, the pinned `-ExpectedC2Commit` and `-ResultDir`.
The launcher obtains fresh interactive privilege proof and passes the module manifest and output paths
to the runner; do not bypass it with guessed runner parameters. A failed health step compensates the
whole run in reverse phase order.

Web before worker means **inside the one release**, by the module’s plan order — never two releases. The
module refuses a release whose three trees are not all present.

### 7.4 Afterwards

**Owner amendment, 2026-09-08 — renewal-aware E2 authorization.** The E2 launcher no longer
requires waiting until 15:00 UTC if the scheduler proves today's renewal completed successfully.
The runner checks every discovered `win-acme renew*` task is Ready, its next run is later than
the entire step budget, and no `wacs` process is running. For budgets touching 09:00–15:00 UTC,
the last run must additionally be today (UTC), not future-dated, with result 0. Missing tasks,
unknown scheduler/process state, queued/running tasks, failed/stale runs or midnight-crossing
budgets are refused. The evidence is printed to the transcript and checked again immediately
before module invocation. Nothing disables renewal, and a concurrent manual renewal is still
prohibited: these observations are not an atomic lock against another administrator starting it.
WhatIf and Execute use the same guard. Interactive privilege proof, confirmation, candidate
binding and rollback controls remain unchanged. Older D2/disposable probe scripts retain their
original time-window restriction; this exception applies to the production E2 runner only.

- Purge the transcripts again (§5). From this release on the worker writes none, so this is the last time.
- Confirm the worker is **Running**, not merely installed. If it is stopped, read §8.5 before anything else.

## 8. After the checkpoints of 2026-09-08 — what is yours

Both reviewers returned REVISE and the findings are fixed on the branch. Five areas leave you something to
do or to decide.

### 8.1 A third migration to apply to Main

The register entry code column was `varchar(20)` while the register’s own column is unbounded text, so an entry
code longer than twenty characters would have raised a database error inside the gate and failed the whole job.
Codes are nine characters today, so nothing has hit it. Migration `20260908014913_ExtractionV2RegisterEntryCodeWidth`
is applied to Preview; Main is yours, as before:

```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File local\migrate.ps1 -Target All
```

That is now **three** v2 migrations Main needs — the hold reason, the register reference, and this width — and
`PreviewParityTests` stays red until it runs.

### 8.2 The release facts are now in the release, so check the file that can override them

The configuration tuple that gates production — package hash, skill commit, CLI version, CLI absolute path, model, effort
— used to exist only in a hand-written `appsettings.Production.json` on the host. It is now committed in the
worker’s own `appsettings.json`, which puts it inside the published tree, inside the clean-export fingerprint,
and makes it roll back with the release. The publish step refuses a release missing any of the six and refuses
one whose hash or commit disagrees with the pinned golden.

**This changed again on 2026-09-08, and it is now a stop rather than a trap.** Committing the tuple was not
enough on its own. The worker appends the secrets JSON last, so the release’s own `appsettings.json` was the
weakest of four configuration sources, and `appsettings.Production.json`, the secrets file, or a
`Worker__*` environment variable each silently outranked it. A fix that reads as protection and is not one
is worse than none, so the release’s copy is now the authority: the worker compares its effective options
against it at start-up and **refuses to start on any difference**, naming the key, both values, and the three
places the override could have come from.

**What you must do before the release.** If any of the six release facts is set in any of these, take it out,
or the worker will not start:

- `C:\Apps\Sibyla\worker\<release>\appsettings.Production.json`
- the secrets file the service points at through `SIBYLA_SECRETS_FILE` (its `worker.json`)
- any `Worker__SkillPackageSha256`-style environment variable on the service

Leave in those files only what is secret or host-specific. The six release facts are neither. I cannot read
the secrets file to check for you — it is one of the paths this work never touches — so this one is yours.
### 8.3 A decision: should the register check also cover uploads made into a company?

Ruling D-EX-5 put the register duplicate check in the company gate, "immediately after the assignment". The gate
runs only when there is an assignment to make, so an upload made **into** a company already carries one, the gate
returns early, and that document gets **no register check at all** in this slice. Your motivating case — an
upload with no company — is protected; the commonest shape is not, until the persistence slice adds the second
guard that D-EX-5 (3) already schedules.

This was left as it is deliberately, because extending it is a ruling rather than a fix: a pre-assigned row gets
no company audit row at all today, and neither "inferred" nor "unresolved" is a truthful word for a company
nobody inferred, so it needs a third action and a fourth transition row. A characterisation test pins the gap so
it cannot be forgotten.

**The question for you:** extend the gate to pre-assigned rows now, or leave the gap until the persistence slice?
Leaving it is defensible and is what the ruling literally says. Extending it closes the commoner hole sooner and
costs a small vocabulary decision.

*Status 2026-09-10:* the persistence slice has landed and is in production, so an upload made into a company **is**
checked against the register — at entry, one poll cycle after extraction, rather than inside the gate. The remaining
question is only whether you also want the hold at the gate; see [the C-17 decision note](c17-register-gap-decision-260910.md).

### 8.4 Queued for the next skill-package build

Four wording fixes wait until after the release, because rebuilding the package changes `SkillTreeSha256`
and ruling Q-EX-14 would then require the sittings to run again:

1. A movement row whose movement-date column is blank takes the date of the group above it, across a page
   break, rather than its own value date. This is the whole of the statements sitting’s missed cells.
2. A document titled as a receipt that prints a clearance condition and no payment date is not a receipt
   (ruling D-EX-6, appeal A-1).
3. `evidence.notes` items are declared at 300 characters in the instruction and 500 in the schema.
4. The instruction never carried the exemption and payment-terms caps at all, which is why five documents
   were refused in the first sitting by bounds the model could not see.

### 8.5 If the worker will not start

This is new on 2026-09-08 and it is the sharpest new failure mode, so it is worth reading before you need it.

The worker now compares its effective configuration against the release’s own copy of the six release facts
and **refuses to start on any difference**. That is deliberate — it is what stops a stale package hash from
silently running the wrong skill — but a worker that will not start is a stopped document pipeline.

**What it looks like.** Windows restarts the service three times over about six minutes, then leaves it
Stopped for twenty-four hours. Nothing watches for that: the module’s health checks run only during a deploy,
and the Uploads page simply shows documents sitting at `Queued`. The refusal message names the key, both
values, and the three places an override can come from, but it surfaces as a service crash rather than a log
line, so look in the Windows event log rather than the application log.

**What causes it.** In order of likelihood:

1. One of the six facts still set in `appsettings.Production.json`, in the secrets file the service
   points at through `SIBYLA_SECRETS_FILE`, or in a `Worker__*` environment variable (§8.2).
2. A package hash that does not match the published skill tree — the release and its package always ship and
   roll back together, so this means something was edited in place.
3. The start-up checks share a ten-second budget. On a cold start, or a slow disk, that budget can expire
   before the package is hashed, and the check then reports a version mismatch rather than a timeout.

**How to get out of it.** Cause 3 has an escape hatch that is not obvious: `Worker:StartupCheckSeconds` can
be raised in the secrets file without tripping the refusal, because it is not one of the six facts. For
causes 1 and 2, remove the override or roll the release back — rollback restores the worker and its package
together and needs no schema rollback, because all three v2 migrations are additive and nullable and the old
code ignores the columns.

None of this is a data loss path. Documents queue and wait; nothing is dropped while the worker is down.
