clearotron 0.4.0 → 0.4.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/THIRD-PARTY-NOTICES.md +2 -2
  2. package/build-info.json +2 -2
  3. package/docs/SECURITY-OWASP.md +21 -13
  4. package/docs/SECURITY.md +20 -14
  5. package/docs/architecture/04-configuration-reference.md +3 -2
  6. package/docs/architecture/05-config-governance.md +1 -1
  7. package/docs/architecture/06-operations-runbook.md +1 -1
  8. package/docs/architecture/09-security-and-data.md +16 -11
  9. package/driver/CHANGELOG.md +18 -0
  10. package/driver/binding-layers.mjs +28 -6
  11. package/driver/citation-census.json +3 -3
  12. package/driver/common-law-coverage-status.mjs +52 -0
  13. package/driver/common-law-receipts.mjs +1 -1
  14. package/driver/connotation-search.mjs +5 -5
  15. package/driver/contract-dictation-registry.mjs +1 -1
  16. package/driver/contract-e3-backlog.mjs +1 -1
  17. package/driver/contract-vocabulary.mjs +18 -18
  18. package/driver/degraded-parts.mjs +10 -3
  19. package/driver/dispatch-record.mjs +19 -0
  20. package/driver/doubt-closure-tool.mjs +2 -2
  21. package/driver/driver.config.mjs +39 -6
  22. package/driver/engagement-receipt.mjs +1 -1
  23. package/driver/engine/CONTRACT.md +41 -1
  24. package/driver/engine/anthropic-agent.mjs +56 -4
  25. package/driver/engine/mcp/codex-config.mjs +1 -1
  26. package/driver/engine/mcp/coverage-server.mjs +1 -1
  27. package/driver/engine/mcp/gather-config.mjs +1 -1
  28. package/driver/engine/mcp/recording-server.mjs +31 -4
  29. package/driver/engine/mcp/stdio-server.mjs +45 -7
  30. package/driver/engine/openai-agent.mjs +100 -17
  31. package/driver/engine/session-record.mjs +223 -0
  32. package/driver/enqueue-schema.mjs +2 -2
  33. package/driver/findings-model.mjs +3 -3
  34. package/driver/form-neighbourhood.mjs +7 -7
  35. package/driver/frame-diff-model.mjs +3 -3
  36. package/driver/framework.mjs +22 -0
  37. package/driver/gateway.mjs +94 -12
  38. package/driver/named-band.mjs +2 -0
  39. package/driver/package.json +1 -1
  40. package/driver/phase0.mjs +13 -2
  41. package/driver/pipeline-knockout.mjs +1 -1
  42. package/driver/pipeline.mjs +46 -33
  43. package/driver/portal-families.mjs +3 -3
  44. package/driver/portal-service.mjs +1 -1
  45. package/driver/predelivery-lint.mjs +19 -19
  46. package/driver/publish/index.mjs +1 -1
  47. package/driver/publish/knockout.mjs +5 -4
  48. package/driver/publish/office-record-links.mjs +17 -2
  49. package/driver/publish/publish-inputs.mjs +1 -1
  50. package/driver/publish/render-knockout.mjs +17 -5
  51. package/driver/publish/render.mjs +4 -2
  52. package/driver/record-fold.mjs +177 -0
  53. package/driver/register-count.mjs +18 -1
  54. package/driver/register-plan.mjs +71 -17
  55. package/driver/register-records.mjs +7 -0
  56. package/driver/registry-fidelity.mjs +2 -2
  57. package/driver/repairs.mjs +1 -1
  58. package/driver/report-card-record.mjs +4 -4
  59. package/driver/scope-facts.mjs +11 -3
  60. package/driver/search-policy.mjs +20 -1
  61. package/driver/skills/clearance-common-law/SKILL.md +22 -22
  62. package/driver/skills/clearance-register/digest.md +3 -3
  63. package/driver/skills/clearance-register/register-recipes.md +11 -11
  64. package/driver/skills/clearance-register/status-rules.md +1 -1
  65. package/driver/skills/clearance-register/unit.md +1 -1
  66. package/driver/skills/clearance-search/firm-wide-reasoning.md +3 -2
  67. package/driver/skills/clearance-search/phase2-execution.md +6 -5
  68. package/driver/skills/clearance-search/synthesis-rules.md +4 -4
  69. package/driver/skills/clearance-search/worked-examples.md +39 -39
  70. package/driver/skills/clearance-variants/SKILL.md +16 -16
  71. package/driver/skills/frame-diff/SKILL.md +2 -2
  72. package/driver/stage-context.mjs +27 -1
  73. package/driver/stages.mjs +12 -12
  74. package/driver/suite-census.json +175 -31
  75. package/driver/synthesis-record.mjs +5 -0
  76. package/driver/unit-inventory.mjs +17 -28
  77. package/driver/verify.mjs +6 -6
  78. package/mcp-server/CHANGELOG.md +4 -0
  79. package/mcp-server/lib/evidence.mjs +8 -8
  80. package/mcp-server/lib/knockout.mjs +1 -1
  81. package/mcp-server/package.json +2 -2
  82. package/package.json +2 -2
  83. package/portal-ui/package.json +1 -1
  84. package/providers/_shared/count.mjs +4 -1
  85. package/providers/_shared/enumerate.mjs +61 -22
  86. package/providers/_shared/execute-plan.mjs +2 -0
  87. package/providers/corsearch/src/capabilities.js +1 -1
  88. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  89. package/providers/oauth-mcp-bridge/package.json +2 -2
  90. package/providers/signa/src/capabilities.js +17 -2
  91. package/providers/signa/src/core.js +234 -37
  92. package/scripts/band-shape-probe.mjs +2 -2
  93. package/scripts/deprecate-below.mjs +28 -4
  94. package/scripts/e2e-first-time.mjs +16 -1
  95. package/scripts/freeze-example-run.mjs +2 -2
  96. package/shared/scope.mjs +18 -2
@@ -110,7 +110,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
110
110
  SOFTWARE.
111
111
  ```
112
112
 
113
- ## @modelcontextprotocol/sdk@1.30.1
113
+ ## @modelcontextprotocol/sdk@1.31.0
114
114
 
115
115
  - **Licence declared:** `MIT`
116
116
  - **Repository:** https://github.com/modelcontextprotocol/typescript-sdk
@@ -4896,7 +4896,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
4896
4896
  SOFTWARE.**
4897
4897
  ```
4898
4898
 
4899
- ## proxy-addr@2.0.7
4899
+ ## proxy-addr@2.0.8
4900
4900
 
4901
4901
  - **Licence declared:** `MIT`
4902
4902
  - **Repository:** jshttp/proxy-addr
package/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "9b8ab0f275a16e9b586425c3574c94b5974e225b",
3
- "version": "0.4.0"
2
+ "commit": "4f2133e937195c6875d4be055c76383ceaca0138",
3
+ "version": "0.4.1-beta.0"
4
4
  }
@@ -45,10 +45,11 @@ shows the engine's internals to a company's people.
45
45
  **What Clearotron does.** Every read passes one authorization check, and a person sees only what their
46
46
  grant names. A key bound to one run reads that run and writes nothing. The copy of a report a company
47
47
  receives drops the staff-only notes. No real company's data is in this repository; run data lives
48
- in folders the operator owns. CI scans the tree and the built bundle for secrets.
48
+ in folders the operator owns. CI scans the tree and the built bundle for secrets. A report published
49
+ from 0.4.0 on carries its fonts inside itself, and opening it contacts no other server.
49
50
 
50
51
  **Where.** [SECURITY.md](SECURITY.md) (the access model), `shared/scope.mjs`, `driver/portal-report.mjs`,
51
- `.gitleaks.toml`, `.github/workflows/ci.yml`.
52
+ `.gitleaks.toml`, `.github/workflows/ci.yml`, `scripts/report-offline-render-check.mjs`.
52
53
 
53
54
  **Not covered.** A company's clearances are kept until the operator deletes them: Clearotron sets no
54
55
  retention period, because how long they are held is the operator's policy.
@@ -57,14 +58,15 @@ retention period, because how long they are held is the operator's policy.
57
58
 
58
59
  **Here.** A compromised npm package, GitHub Action or engine program.
59
60
 
60
- **What Clearotron does.** Dependabot updates the npm packages and the GitHub Actions. Each release is
61
- published to npm through trusted publishing, with provenance that ties the package to the commit and the
62
- build that produced it. A stable release is not published while a code-scanning alert is open. Setup
61
+ **What Clearotron does.** Dependabot updates the npm packages and the GitHub Actions. Every GitHub Action is pinned to a commit
62
+ hash. Each release is published to npm through trusted publishing, with provenance that ties the package
63
+ to the commit and the build that produced it. Each release carries a software bill of materials. On an install from a git
64
+ checkout, `clearotron update` installs with install scripts off. A stable release is not published while a code-scanning alert is open. Setup
63
65
  installs, and `clearotron doctor` accepts, only an engine program at or above the version this release
64
66
  needs.
65
67
 
66
- **Where.** `.github/dependabot.yml`, `.github/workflows/release.yml`,
67
- `scripts/release-code-scanning-check.mjs`, `driver/driver.config.mjs` (`ENGINE_BINARIES`, each program's
68
+ **Where.** `.github/dependabot.yml`, `.github/workflows/`, `.github/workflows/release.yml`,
69
+ `scripts/release-sbom.mjs`, `bin/update.mjs`, `scripts/release-code-scanning-check.mjs`, `driver/driver.config.mjs` (`ENGINE_BINARIES`, each program's
68
70
  `floor`).
69
71
 
70
72
  ### LLM04:2025 Data and Model Poisoning
@@ -99,12 +101,14 @@ The common-law search asks the research service to run a search program, on that
99
101
 
100
102
  **What Clearotron does.** Tools are granted per stage, by name. The case-law stage is the exception: it is granted every tool its two case-law services offer. A Claude
101
103
  stage is offered no command tool.
102
- Register searches beyond the fixed plan go through a proposal that code checks and runs. A key issued for
103
- automation can be limited to named actions. A what-if requested by a company's people is queued for a
104
+ Register searches beyond the fixed plan go through a proposal that code checks and runs. A new key issued for
105
+ automation must name the actions it may take; keys issued before that rule keep working, and each use of
106
+ one is recorded. No key for automation reaches a what-if over the network. A what-if requested by a company's people is queued for a
104
107
  separate process rather than run by the door that received it.
105
108
 
106
109
  **Where.** `driver/engine/mcp/gather-config.mjs`, `driver/engine/anthropic-agent.mjs`,
107
- `driver/engine/mcp/supplemental.mjs`, `shared/scope.mjs`, `driver/whatif-worker.mjs`.
110
+ `driver/engine/mcp/supplemental.mjs`, `shared/scope.mjs`, `driver/whatif-worker.mjs`,
111
+ `mcp-server/mint-token.mjs`, `mcp-server/server.mjs`.
108
112
 
109
113
  **Not covered.** As ASI02: the Claude program also offers a stage built-in tools that act without asking.
110
114
 
@@ -189,7 +193,7 @@ remove them.
189
193
  **What Clearotron does.** The AI program starts with a named list of settings, and the key that signs
190
194
  access keys is not on it. The worker that starts the AI program does not hold that key either, whether
191
195
  the install runs in a terminal or under systemd. Access keys are scoped to a run, a company or named
192
- actions, and one check enforces the scope. On Claude, a stage's file tools can read nothing outside its run folder, its
196
+ actions, and one check enforces the scope. A new key for automation must name its actions. On Claude, a stage's file tools can read nothing outside its run folder, its
193
197
  instruction folders and any folder the machine's own Claude settings add. On Codex with its sandbox on, a stage's commands can read nothing outside its run folder, its instruction folders, the temporary
194
198
  folders, and the system and program files a command needs to run.
195
199
 
@@ -276,10 +280,12 @@ could not run is listed in the report.
276
280
 
277
281
  **What Clearotron does.** No agent outlives its stage. Each stage is one process under a watchdog and a
278
282
  hard time limit, and the driver stops its whole process group. The driver records every program it
279
- starts. On the Claude engine a stage cannot write into the instructions it runs from.
283
+ starts. On the Claude engine a stage cannot write into the instructions it runs from. The systemd units
284
+ an install can run in the background start with `NoNewPrivileges`, so nothing a stage starts there can
285
+ gain more than the install's account holds.
280
286
 
281
287
  **Where.** `driver/engine/anthropic-agent.mjs`, `driver/engine/common.mjs`,
282
- `driver/engine/child-record.mjs`, `driver/engine/deny-authority-write.mjs`.
288
+ `driver/engine/child-record.mjs`, `driver/engine/deny-authority-write.mjs`, `driver/systemd/`.
283
289
 
284
290
  **Not covered.** As LLM04: on Codex, no check refuses a stage's write inside its run folder.
285
291
 
@@ -296,3 +302,5 @@ Measured on 2026-09-23 with the GitHub API.
296
302
  | Private vulnerability reporting | On |
297
303
  | npm provenance | Every release, through trusted publishing |
298
304
  | Required checks on `main` | Six; force pushes and branch deletion refused |
305
+ | Actions pinned to a commit hash | All 42, read from the workflow files on 2026-09-24 |
306
+ | Software bill of materials | CycloneDX, attached to every release, read from the release workflow on 2026-09-24 |
package/docs/SECURITY.md CHANGED
@@ -12,7 +12,7 @@ Each risk on OWASP's two lists for AI systems, and what Clearotron does about it
12
12
 
13
13
  | Surface | Trust | Guard |
14
14
  |---|---|---|
15
- | stdio MCP (`mcp-server/server.mjs`) | local/full ("ops") | OS user boundary — run it AS the operator account; it is the only surface on which `what_if_run` EXECUTES (`visibleTools` keeps what-if out of the HTTP listing for ops, but the CallTool chokepoint gates on `authorize()` alone, which admits it for any ops token not `--verbs`-scoped) |
15
+ | stdio MCP (`mcp-server/server.mjs`) | local/full ("ops") | OS user boundary — run it AS the operator account; it is the only surface on which `what_if_run` EXECUTES (`visibleTools` keeps what-if out of the HTTP listing for ops, and the CallTool chokepoint refuses any tool the listing hides, so no ops token reaches it over HTTP) |
16
16
  | Client MCP (`mcp-server/http-server-client.mjs`) | a company's signed-in person / their access key | `what_if_run` from an `account` principal ENQUEUES rather than executes (ruling 2026-08-27) — it never imports the engine, and `driver/whatif-worker.mjs` spawns the sandbox from an OS service process. A confirmation token is unsigned, so the call must ALSO name its `runId`: the grant check keys on it, and `whatIfEnqueue` refuses a token naming a different run. The `model` argument is refused on this face. |
17
17
  | HTTP MCP (`mcp-server/http-server.mjs`) | authenticated remote | auth-BEFORE-data; fail-closed construction; inner scoped tokens |
18
18
  | Run-bound keys | a report recipient an operator issues one to | a `user` key from `mint-token.mjs`, read-only and bound to one run; the plain-language report tools (`clientSafe`) only |
@@ -98,9 +98,9 @@ with access to everything.
98
98
  on (`get_coverage`, `search`, `diff_artifact`, `run_changes`, what-if), which are denied by the
99
99
  default that an undecided tool is denied.
100
100
  - HMAC-signed tokens (`v1.<payload>.<sig>`, node:crypto only). `authorize()` is the single
101
- enforcement point for every tool call; `visibleTools()` additionally hides what a session may not
102
- call — but it is hygiene on the tool LISTING, not a second gate, and it is the only thing that
103
- reads `local`.
101
+ enforcement point for every tool call; `visibleTools()` decides what a session is offered, and a
102
+ tool it hides is refused by name at the call, after `authorize()`. `authorize()` never sees `local`,
103
+ and `visibleTools()` is the only per-call gate that uses it.
104
104
  - **User tokens cannot**: enumerate runs, cross to another run (explicit mismatching runId refused;
105
105
  omitted runId pinned), reach any write/spend tool, or read internal artifacts — `read_artifact`
106
106
  is name-gated to the report alone (`USER_ARTIFACTS`), and `list_findings` to the curated
@@ -110,13 +110,13 @@ with access to everything.
110
110
  rides inside a delivered PDF and can be forwarded to anyone, and the ruling was about people the
111
111
  operator enrolled. Both sets are gated at the read_artifact tool AND at the Resources surface
112
112
  (`resources/list` / `resources/read`), kind for kind — two doors to the same bytes, one rule.
113
- - **Ops tokens are least-privilege**: an optional `verbs[]` allowlist restricts write tools per
114
- principal (an intake connector physically cannot `stop_run`). `what_if_*` is filtered out of the
115
- HTTP tool LISTING for every OPS scope (`visibleTools`, keyed on `local` — the `local` test governs
116
- the ops branch only, and an `account` principal returns above it). That is hygiene, not a wall:
117
- `authorize()` never sees `local` and treats what-if as an ordinary ops write verb, so an ops token
118
- minted without `--verbs` can still call it over HTTP — which is why every HTTP ops token should be
119
- minted verb-scoped.
113
+ - **Ops tokens are least-privilege**: a new ops token must name its `verbs[]`, the write tools it may
114
+ call (an intake connector physically cannot `stop_run`), and `mint-token` refuses one without
115
+ `--verbs`. Tokens issued before that rule keep working, and the audit line marks every call they
116
+ make. `what_if_plan` and `what_if_run` are left out of the HTTP tool listing for every ops scope
117
+ (`visibleTools`, keyed on `local`), and a tool the listing hides is refused at the call, so no ops
118
+ token plans or runs a what-if over HTTP. An `account` principal returns above that branch: its
119
+ what-if is queued for a separate process, never run by the door.
120
120
 
121
121
  ## Token lifecycle
122
122
 
@@ -127,15 +127,21 @@ what the mechanism guarantees.*
127
127
  principal in every audit line; the `jti` printed at mint time is the revocation handle). Three other callers share the same `mintToken`: `clearotron start` mints the portal's verb-scoped,
128
128
  company-capped ops token in memory at every start, and neither prints nor stores it; `clearotron connect`
129
129
  and the portal's connect screen each mint a person's key for their own assistant.
130
- - **Revocation**: denylist file checked on every verification; missing file = nothing revoked (the
131
- denylist can never take all auth down). **Rotation**: two-secret window, flag-day-free.
130
+ - **Revocation**: a denylist file is checked on every verification, always. With
131
+ `TRADEMARK_MCP_TOKEN_DENYLIST` unset it is `~/.config/clearotron/token-denylist`, the file every revoker
132
+ writes in that case, and that file being absent means nothing has been revoked. A list named in the
133
+ setting that is missing or unreadable, or a default list that exists but cannot be read, refuses every
134
+ key. **Rotation**: two-secret window, flag-day-free.
132
135
  - **Rate limits**: per-identity bucket on every request plus a separate lower per-principal bucket
133
136
  for ops sessions.
134
137
 
135
138
  ## Audit
136
139
 
137
140
  Every HTTP tool call appends one line to an append-only JSONL (`TRADEMARK_MCP_AUDIT_LOG`) when the call
138
- finishes: `{ts, email, sub, method, tool, runId, status, door}`, where `status` is the outcome. It is
141
+ finishes: `{ts, email, sub, kind, method, tool, runId, status, door}`, where `status` is the outcome, plus `query` (its
142
+ first 120 characters) on a search, `uri` on a resource read and `transport` on the local route, and for an
143
+ ops token `namesVerbs`, which says whether the token names its tools (never which tools, and never the
144
+ token). It is
139
145
  written after scope resolution, so it names the principal. A request refused before any tool runs is
140
146
  written too, with the status `refused`. Writing it is best-effort and never blocks a request.
141
147
 
@@ -156,7 +156,8 @@ the attempt row records the id the program reports it served.
156
156
  **The CLI must be new enough for the model.** Each release carries its own list of accepted models,
157
157
  and one that predates a model refuses it outright while the tier alias goes on serving the previous
158
158
  generation — a 400 at the first turn, or a run that quietly used an older model. Setup installs
159
- `2.1.280` or newer for this reason; a copy already on the machine is used as it is, so `doctor`'s
159
+ `2.1.293` or newer for this reason, the first release whose `haiku` reaches Claude Haiku 5.5 (`opus` and
160
+ `sonnet` reached 5.5 from `2.1.284`); a copy already on the machine is used as it is, so `doctor`'s
160
161
  version is the one to read before assuming which model a tier reaches.
161
162
 
162
163
  **Anything else throws.** There is no regex fall-through to sonnet and no cross-provider
@@ -190,7 +191,7 @@ timeout shot, warm patch, backoff), the lane-wedge re-dispatch, and the rate-lim
190
191
  | `CLEAROTRON_AI_BILLING` | `subscription` (default) \| `api-key` \| `cloud` | Billing mode for the selected engine. ONE variable for both, and only the LIVE engine's setting is read — it fills each engine's billing knob, and the engine that is not selected is never consulted. **`anthropic-agent`:** subscription **deletes `ANTHROPIC_API_KEY` from the child env** so `claude -p` uses OAuth subscription credentials (a present key would override them); `api-key` keeps the key — the scale setting and standing fallback; **`cloud`** bills Claude per use through the reader's own cloud account, named by the Claude program's own switch — `CLAUDE_CODE_USE_VERTEX`, `CLAUDE_CODE_USE_FOUNDRY` or `CLAUDE_CODE_USE_BEDROCK`, or `ANTHROPIC_BASE_URL` alone for a gateway (see the credentials table below) — and deletes `ANTHROPIC_API_KEY` as subscription does. **`openai-agent`:** subscription seeds `auth.json` into the per-run `CODEX_HOME` from `CLEAROTRON_OPENAI_AUTH_FILE` (default `~/.codex/auth.json`) and strips API keys; `api-key` keeps `CODEX_API_KEY`; `cloud` is refused. **Fail-loud, never a guess, never the subscription in its place:** `api-key` with no `ANTHROPIC_API_KEY` / `CODEX_API_KEY` throws; on `anthropic-agent`, `cloud` with two switches on, or with none and no `ANTHROPIC_BASE_URL`, throws, and so does a switch left on under `subscription` or `api-key`, because the program bills that cloud whatever this says; a value that is none of the three throws on both engines. `clearotron start` and `clearotron doctor` report each refusal against the settings the services read, naming a value that is not a billing mode by its setting and never quoting it, and the runner refuses the search when it is ordered, before anything is spent. The resolved mode is stamped on every stage telemetry row (`engine`, `authMode`, `apiBilled`, and `cloud`, null when no cloud bills), and the attempt row also carries `providerReported`, the program's own word for who served the turn. |
191
192
  | `CLEAROTRON_CLAUDE_PATH` · `CLEAROTRON_CODEX_PATH` | `claude` / `codex` on `PATH`, then the copy Clearotron installed | The engine binary to spawn, ONE PER ENGINE: `CLEAROTRON_CLAUDE_PATH` under `anthropic-agent`, `CLEAROTRON_CODEX_PATH` under `openai-agent`. Only the live engine's is read, and a machine that runs both sets both — they were one variable until it met a machine needing two different paths. A path set here must be **absolute**: stage subprocesses run with cwd set to the run directory, so a relative path does not resolve there, and `clearotron doctor` refuses one. **Unset**, or set to the bare word `claude` / `codex` (which means the same), the engine uses the program on `PATH` when the machine has one, and otherwise the copy Clearotron installed in `~/.local/share/clearotron/engines`: `clearotron install` offers to put the chosen engine's program there (`@anthropic-ai/claude-code` or `@openai/codex`, this platform's build only), and `clearotron update` refreshes it. Nothing is bundled into the package. A value naming anything else is used as given and never falls back. `clearotron doctor` says which copy it found, and its version when npm installed that copy. |
192
193
  | `CLEAROTRON_OPENAI_AUTH_FILE` | `~/.codex/auth.json` | The credentials file seeded into the per-run `CODEX_HOME` under subscription billing. Was exempted from the August 2026 rename as an `openai-agent` internal rather than an install-surface name; a decision of 2026-09-04 **reversed that exemption** and renamed the whole namespace, so it carries the house prefix like everything else. One spelling, no exceptions. |
193
- | `CLEAROTRON_OPENAI_MODEL_JUDGMENT` / `CLEAROTRON_OPENAI_MODEL_SWEEP` / `CLEAROTRON_OPENAI_MODEL_CHEAP` | `gpt-5.6-sol` / `gpt-5.6-terra` / `gpt-5.6-luna`. | Maps the driver's abstract `opus`/`sonnet`/`haiku` tiers onto the codex ladder, the way the anthropic engine maps onto its own (ruling 2026-09-02, superseding the single-model mapping). A tier that cannot hold its stage contract announces itself at that stage — `missing:negative-results`, `no_coverage_status_row`, `named_band_missing` in `_driver/<stage>.jsonl` — not at delivery. |
194
+ | `CLEAROTRON_OPENAI_MODEL_JUDGMENT` / `CLEAROTRON_OPENAI_MODEL_SWEEP` / `CLEAROTRON_OPENAI_MODEL_CHEAP` | `gpt-6.1-sol` / `gpt-6.1-sol` / `gpt-6-luna` (from 2026-10-09; before that `gpt-5.6-sol` / `gpt-5.6-terra` / `gpt-5.6-luna`). Codex `0.159.0` or newer is required on a ChatGPT sign-in: older releases refuse `gpt-6.1-sol` with a 400. | Maps the driver's abstract `opus`/`sonnet`/`haiku` tiers onto the codex ladder, the way the anthropic engine maps onto its own (ruling 2026-09-02, superseding the single-model mapping). A tier that cannot hold its stage contract announces itself at that stage — `missing:negative-results`, `no_coverage_status_row`, `named_band_missing` in `_driver/<stage>.jsonl` — not at delivery. |
194
195
  | ↳ why they are three, and what the old measurement still says | superseded 2026-09-02, not erased | The three defaulted to `sol` alone on a MEASUREMENT, never a placeholder: on 2026-08-11/12, with `SWEEP=gpt-5.6-terra` / `CHEAP=gpt-5.6-luna`, every codex clearance died on structural-output gates — `missing:negative-results`, `no_coverage_status_row`, `named_band_missing` — over 2 scenarios × 3 stage families, **byte-identically on retry**, so no retry budget rescued it. That measurement stands for the code and the codex CLI **of that date**, and it is why the judgment tier keeps `sol`. Three weeks of stage-contract work and several codex minor versions sit between it and the 2026-09-02 ruling that split the three. **The symptom to match against this cause is unchanged**: those same three gates, as a fail row in`_driver/<stage>.jsonl`, inside the first few dispatches of a clearance and identical on every retry — a model incapable of a contract is not flaky at it. It reads as an engine bug when the only fault is this configuration. A cheap-tier experiment still belongs in the three constants in `driver/engine/openai-agent.mjs` where a reviewer sees it, not in an untraceable env line. |
195
196
  | `CLEAROTRON_DATABASE` | **REQUIRED — no default.** `corsearch` \| `clarivate` \| `signa` \| `euipo` \| `uspto-local` \| `free-tier` | Active register provider — one per run, set in the environment the deploy carries, in every environment including production. There is **no default**: unset resolves to `null` and every use of it throws, so a run refuses at start rather than calling a vendor nobody chose (the removed `corsearch` fallback named a vendor the deployment did not choose). Three are paid global sweeps; `euipo` (EU) and `uspto-local` (US) are free single-office sources, and `free-tier` composes those two as one register. Choosing a free value makes every territory outside its coverage a disclosed *deferred* row. Unknown ids throw loudly, and the gather layer throws at stage time for any provider without a built MCP server. |
196
197
  | `CLEAROTRON_CODEX_SANDBOX_BYPASS` | unset | `1` runs `codex exec` with its own sandbox bypassed, for hosts where that sandbox cannot run (setup and `doctor --probe-engine` report it). It removes a defence: with it set, a stage's commands run with every permission of the account Clearotron runs as, not only in the run folder. Codex has no guard against a stage writing into the run's `_driver/` folder, with the sandbox or without it. Set it because the host forces it, never for convenience. |
@@ -161,7 +161,7 @@ structural, or dev seam); [dev] = dev/test seam, never set in prod.
161
161
  `CLEAROTRON_AI` (anthropic-agent | openai-agent; default anthropic-agent), `CLEAROTRON_AI_BILLING`
162
162
  (subscription|api-key|cloud; cloud is Claude only), `CLEAROTRON_CODEX_PATH`,
163
163
  `CLEAROTRON_OPENAI_AUTH_FILE`, `CLEAROTRON_OPENAI_MODEL_JUDGMENT` / `CLEAROTRON_OPENAI_MODEL_SWEEP` /
164
- `CLEAROTRON_OPENAI_MODEL_CHEAP` (all gpt-5.6-sol),
164
+ `CLEAROTRON_OPENAI_MODEL_CHEAP` (gpt-6.1-sol / gpt-6.1-sol / gpt-6-luna),
165
165
  `CLEAROTRON_CLAUDE_PATH` (claude on PATH, then the copy Clearotron installed),
166
166
  `CLEAROTRON_SYNTHESIS_MODEL` (opus), `CLEAROTRON_KNOCKOUT_MODEL` (opus),
167
167
  `CLEAROTRON_MAX_BUDGET_USD` (unset).
@@ -294,7 +294,7 @@ node mcp-server/mint-token.mjs --scope ops --sub <principal-name> \
294
294
 
295
295
  - `--sub` names the principal, and it rides into every audit line. Two integrations holding ops
296
296
  tokens are then distinguishable in the log; one shared token makes them permanently indistinguishable.
297
- - `--verbs` is a least-privilege allowlist of write tools. A connector minted without `stop_run`
297
+ - `--verbs` is required for an ops token: it lists the write tools the token may call. A connector minted without `stop_run`
298
298
  cannot call it — the check sits at the one chokepoint every tool call passes, not in each tool.
299
299
  - `--accounts` caps the token to a set of company keys, so a trial integration can start demo
300
300
  searches and never a real company's.
@@ -105,9 +105,12 @@ are no root units; everything is `systemd --user`.
105
105
  The API-key door is a fourth process (loopback :18812) with **no Access in front** — the trade is
106
106
  explicit: a mandatory key replaces the browser sign-in, and the mode refuses to start if anything
107
107
  that would weaken that (the dev auth-disable knob, a missing signing secret) is also set.
108
- - The staff unit is systemd-hardened (`NoNewPrivileges`, `ProtectSystem=strict` with only the
109
- audit-log path writable, `PrivateTmp`); both HTTP faces enforce host allowlists
110
- (DNS-rebinding protection), per-identity rate limits, body caps, and auth-before-body-read.
108
+ - The remote deployment's staff unit (`mcp-server/remote/`) is systemd-hardened (`NoNewPrivileges`,
109
+ `ProtectSystem=strict` with only the audit-log path writable, `PrivateTmp`). The portal, worker and two
110
+ assistant-door units an install runs (`driver/systemd/`) set `NoNewPrivileges`, `RestrictSUIDSGID`,
111
+ `LockPersonality` and `UMask=0007`, and nothing that limits where they write: they can write wherever
112
+ the install's account can. Both HTTP faces enforce host allowlists (DNS-rebinding protection),
113
+ per-identity rate limits, body caps, and auth-before-body-read.
111
114
  - **Outbound** traffic is enumerated in [What leaves the machine](#what-leaves-the-machine) below.
112
115
  No outbound service is given filesystem access: register, research, and case-law calls carry query
113
116
  arguments only, and the pool and run dirs are never exposed to them.
@@ -165,17 +168,17 @@ CLI's built-in `WebFetch` (`engine/mcp/gather-config.mjs`), or the driver's stan
165
168
  moment of the call, so no list here can be complete, and a deployment that needs one must enforce it at
166
169
  the network layer.
167
170
 
168
- **A published report reaches two font CDNs.** The report HTML links stylesheets on `api.fontshare.com`
169
- and `fonts.googleapis.com`, with the font files on `cdn.fontshare.com` and `fonts.gstatic.com`
170
- (`publish/render.mjs`, `publish/render-knockout.mjs`; the served CSP admits exactly
171
- those hosts at `portal-static.mjs`). The reader's browser fetches them when the report
172
- opens. The request carries no matter data. Registry links in the report resolve only when a reader
171
+ **A report carries its own fonts.** A report published from 0.4.0 on holds Plus Jakarta Sans and Fira
172
+ Code inside itself (`shared/fonts/`), and opening it contacts no other server, from disk, from an email
173
+ or in the portal. CI records every request a rendered report makes and fails on one that leaves
174
+ (`scripts/report-offline-render-check.mjs`). A report published before 0.4.0 still links its two font
175
+ services when opened from disk. The request carries no matter data. Registry links in the report resolve only when a reader
173
176
  clicks one.
174
177
 
175
178
  **The demo contacts nothing.** `bin/example.mjs` re-publishes artifacts already on disk: no `fetch` in its
176
179
  module graph, no MCP server, no engine binary. Its two subprocesses are a `git rev-parse` build stamp
177
- and the browser opener, and the portal binds `127.0.0.1`. The report it opens loads the two font
178
- stylesheets above.
180
+ and the browser opener, and the portal binds `127.0.0.1`. The report it opens loads nothing from
181
+ another server.
179
182
 
180
183
  ## Secrets
181
184
 
@@ -228,7 +231,9 @@ today — flag for the buyer's compliance review.
228
231
  and the engine-local gather MCP servers add none: each is ~130 lines over a shared 120-line stdio
229
232
  scaffolding (`driver/engine/mcp/stdio-server.mjs`) with no MCP SDK. The artifacts read layer does
230
233
  use the SDK (`@modelcontextprotocol/sdk`, `jose`). Small surface by design.
231
- - Deploy installs with `--omit=dev --ignore-scripts`. If your deploy then rebuilds a native addon,
234
+ - On an install from a git checkout, `clearotron update` installs with `npm ci --ignore-scripts`, so no
235
+ package's install script runs on the host. Development packages are kept there, because the portal is
236
+ rebuilt from them. If your deploy then rebuilds a native addon,
232
237
  make that a named, reviewed step: it is the only place a lifecycle script should run on the host.
233
238
  - Run a skill scanner over the skills tree at deploy time and abort on high/critical findings. Two
234
239
  things to check on any new host: that the scanner is actually installed (a scanner that is absent
@@ -1,5 +1,23 @@
1
1
  # clearotron-driver
2
2
 
3
+ ## 0.4.1-beta.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Fixed: A connection library is updated to close a reported weakness in how sign-in details could be sent.
8
+ - Fixed: A clearance started from a connected assistant under the Generic default now appears on Clearances when you choose Generic. Before, it showed only under All companies.
9
+ - Fixed: A register count or record that could not be fetched no longer shows the program's error message to a client. This covers the knockout workbook's count notes, the knockout report and a clearance finding card. Each shows the line the report already uses for something left open.
10
+ - Fixed: A knockout's completion message and email now give its rating, or the highest rating when several names were screened. They no longer use the term 'worst band'.
11
+ - Fixed: A client whose mark is written in another script, such as Greek, Cyrillic or Chinese, now receives the link to the report. Before, the email arrived without one.
12
+ - Fixed: In rare cases, a report repaired just before delivery listed one conflict as two findings. Its last finding also lost its full write-up. The conflict now stays one finding, and every finding keeps its write-up.
13
+ - Fixed: Reports no longer say a search "did not finish and must be re-run" when a name's non-Latin form was in fact searched. They no longer make a conclusion depend on reviewing records the search had already read in full. Knockout reports now list every territory you ordered.
14
+ - Fixed: The audit workbook no longer warns that the depth rules were applied to nothing on a clearance whose narrative is written in sections. Those rules govern per-finding write-ups, and such a narrative has none.
15
+ - Fixed: An order for the European Union now also searches each member state's own trade mark register, at no extra search cost. A national registration — in France, for example — is now found by an EU search.
16
+ - Before you upgrade: if you installed Codex yourself, update it to 0.159.0 or newer first. Older copies cannot use GPT-6.1 Sol, and searches on the OpenAI engine would stop at their first step.
17
+ - New: Searches now use the latest models: Claude Haiku 5.5, Sonnet 5.5 and Opus 5.5, and OpenAI's GPT-6.1 Sol and GPT-6 Luna. Installing or updating Clearotron brings the Claude and Codex programs up to versions that can use them.
18
+ - Fixed: Two different names written in Greek, Cyrillic or Chinese characters, or differing only in an accented letter, can now be screened in one knockout. Before, they were refused as duplicates.
19
+ - Fixed: Findings rated medium risk or higher are now checked for the owner's actual use in the same way on every company's risk ladder. That same search looks for the owner's record of oppositions and disputes, so "Likely to enforce" carries a rating instead of "not checked".
20
+
3
21
  ## 0.4.0
4
22
 
5
23
  ### Minor Changes
@@ -100,19 +100,39 @@ export function bindingLayersFor(territory) {
100
100
  layers.push({ layer: "national", office: BX_OFFICE,
101
101
  why: `${t} has no separate national register — Benelux (BX) is the national register for it` });
102
102
  } else if (t === WO_OFFICE) {
103
- // The international register ORDERED AS a territory. Same shape as the EU below: there is no
104
- // national layer to miss, because the Madrid register IS the order. Without this a matter listing
103
+ // The international register ORDERED AS a territory. There is no national layer to miss, because
104
+ // the Madrid register IS the order. Without this a matter listing
105
105
  // "International" (territory-codes.mjs normalizes it to WO) discloses a missing national register
106
106
  // that does not exist — a limitation on a register the plan searched, which is the kind a reader
107
107
  // learns to skim.
108
108
  layers.push({ layer: "international", office: WO_OFFICE, why: "the international register, ordered directly" });
109
109
  return Object.freeze(layers.map(Object.freeze));
110
- } else if (t === EU_OFFICE || t === "EU") {
111
- // The EU ordered AS a territory. There is no national layer to miss; the EU register IS the order.
112
- layers.push({ layer: "regional", office: EU_OFFICE, why: "the EU-wide register, ordered directly" });
110
+ } else if (t === EU_OFFICE) {
111
+ // The EU-wide register named by its OFFICE code, which is how a plan that is already resolved
112
+ // carries it. It names that register, not the territory: resolving a France plan's ["FR", "EM",
113
+ // "WO"] again must not turn it into a search of every member state. An EU ORDER arrives as "EU"
114
+ // (normalizeTerritory folds "European Union", EUTM and EUIPO there) and takes the branch below.
115
+ layers.push({ layer: "regional", office: EU_OFFICE, why: "the EU-wide register, named by its office code" });
113
116
  layers.push({ layer: "international", office: WO_OFFICE,
114
117
  why: "international registrations designating the EU bind it without a separate EU filing" });
115
118
  return Object.freeze(layers.map(Object.freeze));
119
+ } else if (t === "EU") {
120
+ // The EU ordered AS a territory: the EU-wide register, EVERY member state's national register, and
121
+ // international registrations designating the EU or a member. A national right blocks use in its own
122
+ // member state without appearing in the EU-wide register, so an EU order that searched only EUIPO and
123
+ // Madrid cleared none of the countries inside it — measured 2026-10-09 on both live providers: an
124
+ // EU-scoped search returns no member state's national filings, and listing the members in the same
125
+ // request reaches them. The owner's ruling of the same day: an EU order searches the national
126
+ // registers too, on every provider. Benelux is one national register for three members, listed once.
127
+ layers.push({ layer: "regional", office: EU_OFFICE, why: "the EU-wide register, ordered directly" });
128
+ for (const office of new Set(EU_MEMBERS.map((m) => (BENELUX_MEMBERS.includes(m) ? BX_OFFICE : m)))) {
129
+ layers.push({ layer: "national", office, why: office === BX_OFFICE
130
+ ? "Benelux (BX) is the national register of three EU members"
131
+ : `a national right in ${office} blocks use there without appearing in the EU-wide register` });
132
+ }
133
+ layers.push({ layer: "international", office: WO_OFFICE,
134
+ why: "an international registration designating the EU or a member state binds it without a separate filing" });
135
+ return Object.freeze(layers.map(Object.freeze));
116
136
  } else {
117
137
  layers.push({ layer: "national", office: t, why: `the national register of ${t}` });
118
138
  }
@@ -215,7 +235,9 @@ export function unsearchedLayerReason(report) {
215
235
  regional: "EU-wide rights",
216
236
  international: "international registrations designating it",
217
237
  };
218
- const missing = report.unsearched.map((u) => names[u.layer] ?? u.layer);
238
+ // One name per KIND of register: an EU order carries a national layer per member state, and a provider
239
+ // that reaches only some of them would otherwise list "the national register" once per member missed.
240
+ const missing = [...new Set(report.unsearched.map((u) => names[u.layer] ?? u.layer))];
219
241
  const list = missing.length === 1 ? missing[0]
220
242
  : `${missing.slice(0, -1).join(", ")} and ${missing[missing.length - 1]}`;
221
243
  return `Not a complete clearance for ${report.territory}: ${list} ${missing.length === 1 ? "was" : "were"} `
@@ -1,5 +1,5 @@
1
1
  {
2
- "citations": 465,
3
- "files": 3308,
4
- "checkable": 129
2
+ "citations": 415,
3
+ "files": 3437,
4
+ "checkable": 186
5
5
  }
@@ -111,3 +111,55 @@ export function recordCoverageStatus(spec, received, { now = () => new Date().to
111
111
  if (refused.length) lines.push("Refused — send these again, corrected:", ...refused.map((x) => `- ${x}`));
112
112
  return { ok: refused.length === 0, text: lines.join("\n") };
113
113
  }
114
+
115
+ /** The status code records for a ledger row whose findings gave none of the three words. */
116
+ export const NOT_STATED = "not stated";
117
+
118
+ /**
119
+ * THE STATUSES A FINDINGS FILE STATES, RECORDED BY CODE (Krzys, 2026-10-01). A seat whose findings carry
120
+ * a coverage ledger but made no `record_coverage_status` call is accepted, not re-run: a re-run for this
121
+ * was a full stage re-run, and nothing is added to what the model reads. Each ledger row's status is read
122
+ * from the row as the findings wrote it — one of the three words, else "not stated" — and recorded in the
123
+ * same file the tool writes, marked `source: "findings"`. A record the seat made through the tool is never
124
+ * overwritten; a record code made earlier is replaced, so it follows the latest findings after a re-run.
125
+ * Returns the record written, or null when nothing was written. Never throws.
126
+ */
127
+ export function recordStatusesFromFindings(findingsPath, { half = null, now = () => new Date().toISOString() } = {}) {
128
+ if (!findingsPath) return null;
129
+ const file = coverageStatusPath(findingsPath);
130
+ let existing = null;
131
+ try { existing = JSON.parse(readFileSync(file, "utf8")); } catch { existing = null; }
132
+ if (existing && existing.source !== "findings") return null; // the seat recorded its own statuses
133
+ let text;
134
+ try { text = readFileSync(String(findingsPath), "utf8"); } catch { return null; }
135
+ const record = { coverage_status: ledgerStatuses(text), half, source: "findings", recorded_at: now() };
136
+ try {
137
+ mkdirSync(dirname(file), { recursive: true });
138
+ writeFileSync(file, JSON.stringify(record, null, 2) + "\n");
139
+ } catch { return null; }
140
+ return record;
141
+ }
142
+
143
+ /**
144
+ * Each coverage-ledger row's unit and status, as the findings file wrote them: the first table under the
145
+ * Coverage ledger heading, its header and separator rows skipped. A row with none of the three words
146
+ * reads "not stated"; a ledger with no rows reads as one unit, "coverage ledger", not stated. PURE.
147
+ */
148
+ export function ledgerStatuses(text) {
149
+ const lines = String(text ?? "").split("\n");
150
+ const at = lines.findIndex((l) => /^#{1,4}\s+[^\n]*coverage[\s-]ledger/i.test(l));
151
+ const isSeparator = (l) => /^\|?(\s*:?-{2,}:?\s*\|)+\s*:?-{0,}:?\s*\|?$/.test(String(l ?? "").trim());
152
+ const out = {};
153
+ if (at >= 0) {
154
+ for (let i = at + 1; i < lines.length; i++) {
155
+ const l = lines[i].trim();
156
+ if (/^#{1,6}\s/.test(l)) break;
157
+ if (!l.startsWith("|")) { if (Object.keys(out).length) break; continue; }
158
+ if (isSeparator(l) || isSeparator(lines[i + 1])) continue; // the separator, and the header above it
159
+ const unit = l.replace(/^\|/, "").split("|")[0].trim();
160
+ if (!unit) continue;
161
+ out[unit] = COMMON_LAW_COVERAGE_STATUSES.find((s) => new RegExp(`(^|[^a-z-])${s}([^a-z-]|$)`, "i").test(l)) ?? NOT_STATED;
162
+ }
163
+ }
164
+ return Object.keys(out).length ? out : { "coverage ledger": NOT_STATED };
165
+ }
@@ -166,7 +166,7 @@ export function countMatrixCells(findingsContent) {
166
166
  /**
167
167
  * The shared shortfall ladder over a `cellsOf(term) → count` accessor. A variant is satisfied by:
168
168
  * its own floor; " / "-packed ALTERNATES each meeting the floor (one manifest cell, two renderings —
169
- * "丝绸与铁 / 席尔克": each alternate is its own search term with its own grid — the copper-conduit worker
169
+ * "缎与钢 / 萨汀": each alternate is its own search term with its own grid — the copper-conduit worker
170
170
  * keyed the split forms with full receipts while the validator demanded the compound key, 2026-06-12);
171
171
  * or WORD-BOUNDARY substring coverage (a fully-receipted variant covers a LONGER variant containing it
172
172
  * as a whole-word phrase — "Foxglade Legends II: Oak & Ember" ⊃ "Oak & Ember"; a marketplace phrase
@@ -300,14 +300,14 @@ export const normText = (s) => foldPunct(String(s ?? "").toLowerCase()).replace(
300
300
 
301
301
  // (2026-08-04, the terminal R2 run of that round) — LANGUAGE EDITIONS OF ONE DOCUMENT ARE ONE RECEIPT.
302
302
  // The recurrence floor demanded the model cite the SAME dictionary lemma twice, under two language editions.
303
- // Attempts 2, 6 and 8 died on https://fr.wiktionary.org/wiki/δελφίς; the model then cited it verbatim; attempt
304
- // 9 died on https://en.wiktionary.org/wiki/δελφίς and the run went terminal — ~37 minutes of model work, no
303
+ // Attempts 2, 6 and 8 died on https://fr.wiktionary.org/wiki/βελτρίν; the model then cited it verbatim; attempt
304
+ // 9 died on https://en.wiktionary.org/wiki/βελτρίν and the run went terminal — ~37 minutes of model work, no
305
305
  // artifact. Both editions cleared the floor independently (en 8 owning queries, fr 5, floor 4) and their
306
306
  // titles diverge before TITLE_PREFIX ("Wiktionary, the free dictionary" / "Wiktionnaire, le dictionnaire
307
307
  // libre"), so citing one PROVABLY could not satisfy the other and no attempt could have converged.
308
308
  // Folding the leading language-edition host label weakens the compare's contract not at all: the contract is
309
- // "the model quoted something it could only have got by opening the receipt", and fr.wiktionary.org/wiki/δελφίς
310
- // proves exactly that for en.wiktionary.org/wiki/δελφίς. BOUNDED two ways — only a leading 2-3 letter (or
309
+ // "the model quoted something it could only have got by opening the receipt", and fr.wiktionary.org/wiki/βελτρίν
310
+ // proves exactly that for en.wiktionary.org/wiki/βελτρίν. BOUNDED two ways — only a leading 2-3 letter (or
311
311
  // xx-yy) label, and only when a PATH follows, because folding a BARE host would let any URL on the domain
312
312
  // satisfy a demand for one specific page. Measured on the evidence receipts: 274 distinct results → 266,
313
313
  // and the load-bearing-by-recurrence set is the same 7, only re-keyed.
@@ -438,7 +438,7 @@ const QUOTE_MIN = 24;
438
438
  // that it could not have been produced without reading the snippet. 24 Latin characters buy that. 24
439
439
  // HANZI buy several times as much, because a Chinese character carries a morpheme where a Latin
440
440
  // character carries a letter — so this constant asked the CJK lane for a passage far longer than the
441
- // rule requires, and a 克罗玛 row failed on exactly that.
441
+ // rule requires, and a 维尔特林 row failed on exactly that.
442
442
  //
443
443
  // IT CUT BOTH WAYS, which is why both sites move together or neither does. The same constant decides
444
444
  // `usableSnippet` — which rows are ELIGIBLE to be quote-required at all — so a CJK snippet under 24
@@ -131,7 +131,7 @@ export const TOOL_ORDER_BACKLOG = [
131
131
  closes: "#865 — same shared file, same split grant." },
132
132
  { stage: "register-digest", tool: "register_batch_screen", site: "driver/skills/clearance-register/digest.md",
133
133
  closes: "#865 — the digest's OWN doctrine, not a shared file: this one is the stage being told to "
134
- + "batch-screen with a tool its grant does not carry. gather-config.mjs:196 already records that "
134
+ + "batch-screen with a tool its grant does not carry. gather-config.mjs:196 RECORDING already records that "
135
135
  + "this stage's prompt once ordered live register checks; the order outlived the note." },
136
136
  { stage: "register-unit", tool: "band_record", site: "driver/skills/clearance-register/SKILL.md",
137
137
  closes: "#865 — same shared file, same split grant." },
@@ -211,7 +211,7 @@ export const E3_BACKLOG = [
211
211
  kind: "literal-json-skeleton",
212
212
  where: "driver/skills/clearance-common-law/SKILL.md:123-170",
213
213
  surface: "skill-file",
214
- evidence: "### Format\\n```markdown\\n# Common-law findings — Dawn: Legends of Thornmantle (2026-05-11)\\n\\n## Summary\\n\\n- Perplexity calls executed: 4 …",
214
+ evidence: "### Format\\n```markdown\\n# Common-law findings — Dusk: Tales of Thornmantle (2026-05-11)\\n\\n## Summary\\n\\n- Perplexity calls executed: 4 …",
215
215
  reparsedBy: "driver/publish/audit-from-spine.mjs:14 parseTables → parseSpineFindingBlocks (audit-from-spine.mjs:123) re-parses the common-law finding tables into audit.md",
216
216
  removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
217
217
  },