clearotron 0.3.2-beta.1 → 0.3.2-beta.3

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 (53) hide show
  1. package/.env.example +29 -0
  2. package/INSTALL.md +2 -0
  3. package/README.md +15 -10
  4. package/bin/onboard.mjs +4 -4
  5. package/build-info.json +2 -2
  6. package/docs/CLIENT-MCP.md +54 -54
  7. package/docs/DELIVERY.md +16 -15
  8. package/docs/E2E.md +8 -8
  9. package/docs/GLOSSARY.md +2 -2
  10. package/docs/INTAKE.md +11 -11
  11. package/docs/ONBOARDING.md +17 -16
  12. package/docs/PORTAL.md +18 -16
  13. package/docs/README.md +10 -10
  14. package/docs/SECURITY.md +20 -20
  15. package/docs/architecture/01-product-overview.md +17 -16
  16. package/docs/architecture/02-architecture.md +6 -6
  17. package/docs/architecture/03-run-lifecycle.md +7 -7
  18. package/docs/architecture/04-configuration-reference.md +15 -13
  19. package/docs/architecture/05-config-governance.md +39 -30
  20. package/docs/architecture/05-customer-profiles.md +35 -35
  21. package/docs/architecture/06-operations-runbook.md +20 -20
  22. package/docs/architecture/07-quality-and-audit.md +17 -17
  23. package/docs/architecture/08-development-guide.md +3 -3
  24. package/docs/architecture/09-security-and-data.md +27 -27
  25. package/docs/architecture/README.md +1 -1
  26. package/docs/branding.md +8 -3
  27. package/docs/configuration.md +18 -18
  28. package/docs/writing-standard.md +3 -3
  29. package/driver/CHANGELOG.md +33 -0
  30. package/driver/driver.config.mjs +19 -3
  31. package/driver/package.json +1 -1
  32. package/driver/portal-service.mjs +34 -4
  33. package/driver/suite-census.json +118 -46
  34. package/mcp-server/CHANGELOG.md +8 -0
  35. package/mcp-server/package.json +1 -1
  36. package/mcp-server/packs/client/CONNECT.md +6 -6
  37. package/package.json +1 -1
  38. package/portal-ui/dist/assets/{index-BsbasHjM.js → index-Bki5jT_N.js} +3376 -2971
  39. package/portal-ui/dist/assets/{index-DNQpLYZF.css → index-De2RFLbT.css} +972 -205
  40. package/portal-ui/dist/index.html +2 -2
  41. package/portal-ui/package.json +1 -1
  42. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  43. package/providers/oauth-mcp-bridge/package.json +1 -1
  44. package/providers/signa/src/core.js +29 -1
  45. package/scripts/ask-ai-render-check.mjs +287 -105
  46. package/scripts/release-note-required.mjs +16 -2
  47. package/scripts/release-pre-gate.mjs +111 -0
  48. package/scripts/settings-render-check.mjs +575 -0
  49. package/shared/brand.mjs +29 -0
  50. package/shared/connect-clients.mjs +99 -47
  51. package/shared/names-in-force.mjs +1 -0
  52. package/shared/stdio-connect.mjs +16 -2
  53. package/shared/writing-standard-classes.mjs +34 -3
@@ -21,8 +21,8 @@ Configuration lives in three layers, resolved in this order:
21
21
  2. **The stage table** → `stages.mjs` — the single source of truth for what each pipeline stage
22
22
  runs: model tier, reasoning effort, timeouts, skills, output file, validator, message. Per-stage
23
23
  behaviour changes happen here, nowhere else.
24
- 3. **Customer profiles** → `profiles/` — per-client knowledge and delivery configuration, frozen
25
- per run. Documented in [05 — Customer profiles](05-customer-profiles.md).
24
+ 3. **Company profiles** → `profiles/` — per-company knowledge and delivery configuration, frozen
25
+ per run. Documented in [05 — Company profiles](05-customer-profiles.md).
26
26
 
27
27
  **Resolution semantics matter.** Every value on the `config` object is a **getter**, read at access
28
28
  time, so all of them honour a mid-process env flip — the file's own NOTE beside `config` says why:
@@ -93,8 +93,8 @@ does after the stage's full retry ladder fails ([03 §5](03-run-lifecycle.md#5--
93
93
  Notes that keep maintainers out of trouble (all verified against code):
94
94
 
95
95
  - **`skillReads` on a stage entry is declarative metadata only.** The live skill reads are the
96
- `reads([...])` call inside each stage's `message()`, which resolves per-customer framework and
97
- worked-examples files via the profile. Do not "reconcile" the two — that breaks per-customer
96
+ `reads([...])` call inside each stage's `message()`, which resolves per-company framework and
97
+ worked-examples files via the profile. Do not "reconcile" the two — that breaks per-company
98
98
  framework selection (guarded by `test/profiles.test.mjs`).
99
99
  - **Stage messages embed verbatim machine contracts** (`ESCALATE:` lines, `- ord:` stamps, closed
100
100
  enums, band `state` values). Code parses these strings; rewording a message can break the parser.
@@ -193,13 +193,13 @@ deployment may override (verify live values per deployment).
193
193
  | `CLEAROTRON_SKILLS_STORE_STRICT` | unset (warn) | Hard mode for the run door's doctrine-store check (`driver/skills-store-provenance.mjs`). The door classifies the store `CLEAROTRON_INSTRUCTIONS_DIR` names into the publication gate's three outcomes — **pass**, **fail** (uncommitted changes under the served path, or a HEAD not contained in the main line), **blocked** (could not be determined: git absent, an unreadable ancestor, no main-line ref, or a store no commit tracks). A store affirmatively outside any checkout passes: the walk to the filesystem root must complete with every step readable, so "no git evidence" can never be inferred from a directory this process cannot see. Unset, a non-pass prints one `[preflight]` line to stderr and stamps a `skills-store` event into the run's `_driver/run.jsonl`, and the run proceeds. Set (`1`; `0`/`off`/`false`/`no`/empty do not arm it), any non-pass **refuses the run before any spend** — `blocked` included, because a store whose state nobody can name is not a store anybody can reproduce. |
194
194
  | `CLEAROTRON_SKILLS_STORE_MAIN_BRANCH` | `main` | Which branch name the check above treats as the main line. A deployment whose config store is on `master` sets this; otherwise that store has no main-line ref to be measured against and every run reports `blocked`. Only the branch name — the check prefers `origin/<name>` and falls back to the local ref. |
195
195
  | `CLEAROTRON_MIN_FREE_DISK_MB` | `500` | Free space the run door requires on the filesystem holding `CLEAROTRON_WORK_DIR`, in MB. Below it a run is **refused** before anything is written — a disk that fills mid-run surfaces as a *missing artifact* at some later stage, which is indistinguishable from an engine or provider fault. Not a sizing estimate: one real delivered run measured 5.85 MB, so 500 MB is the line below which the filesystem is in trouble for reasons of its own.`0` disables the check; a non-numeric value **throws** rather than silently disabling it. A disk that cannot be measured is reported and the run proceeds — never read as room. |
196
- | `CLEAROTRON_REPORTS_DIR` | **none — set it** | Publish pool (web-served). **No default since: unset refuses and names the variable.** It read`/srv/trademark-archive` — a deployed server's real client archive — so a forgotten export published into somebody else's matter, and two entry points already carried hand-written defences against exactly that (`bin/onboard.mjs`, `bin/example.mjs`). Same shape as `CLEAROTRON_DATABASE` and `scripts/purge-runs.mjs`: guessing wrong is expensive, so it does not guess. Read-only surfaces (flag snapshot, status page, MCP options) degrade to "no pool" instead of throwing; anything that writes refuses. `driver/production-pool-guard.mjs` still names `/srv/trademark-archive` on purpose — that constant is a fact about where the archive is, not a default. |
196
+ | `CLEAROTRON_REPORTS_DIR` | **none — set it** | Publish pool (web-served). **No default since: unset refuses and names the variable.** It read`/srv/trademark-archive` — a deployed server's real archive — so a forgotten export published into somebody else's clearances, and two entry points already carried hand-written defences against exactly that (`bin/onboard.mjs`, `bin/example.mjs`). Same shape as `CLEAROTRON_DATABASE` and `scripts/purge-runs.mjs`: guessing wrong is expensive, so it does not guess. Read-only surfaces (flag snapshot, status page, MCP options) degrade to "no pool" instead of throwing; anything that writes refuses. `driver/production-pool-guard.mjs` still names `/srv/trademark-archive` on purpose — that constant is a fact about where the archive is, not a default. |
197
197
  | `CLEAROTRON_REPORTS_URL` | **none — set it** | Pool base URL used in notification links. No placeholder default: unset ⇒ the link is omitted and the runner logs `deployment config MISSING` at activation. It does not gate the queue (a missing hostname costs a link, not the deliverable), so treat that log line as the alarm. |
198
198
  | `CLEAROTRON_ACCESS_DOMAIN` | unset (note omitted) | Identity domain named in the delivery email's access note ("sign in with a `<domain>` account"). Unset ⇒ the note is omitted rather than naming the wrong domain. |
199
199
  | `CLEAROTRON_RUN_LOCK_DIR` | `<workspaceRoot>/prelim-run-locks` | Run-slot lock dir (turn locks under `…/turns`). |
200
200
  | `CLEAROTRON_OUTBOX_DIR` | `<workspaceRoot>/prelim-outbox` | Delivery outbox (`<runId>.pending` wake markers). |
201
201
  | `CLEAROTRON_OAUTH_BRIDGE` | module-relative `providers/oauth-mcp-bridge/bridge.mjs` | Case-law MCP bridge script. (Portable since the module-relative default; set explicitly only for a bridge outside the repo tree.) |
202
- | `CLEAROTRON_REGISTER_CALL_LOG` | `~/trademark/telemetry/register-calls.jsonl`, or the existing file wherever it already is | Billing-grade provider-call ledger, shared by whichever ONE register provider is wired — not a vendor artifact. Every read site derives the default from`homedir()` at call time (2026-07-19: two sites had hardcoded a literal account home, splitting the ledger under any other service account — guarded by `test/deployment-hostnames.test.mjs`). |
202
+ | `CLEAROTRON_REGISTER_CALL_LOG` | `~/trademark/telemetry/register-calls.jsonl`, or the existing file wherever it already is | Billing-grade provider-call ledger, shared by whichever ONE register provider is wired — not a vendor artifact. Every read site derives the default from`homedir()` at call time (2026-07-19: two sites had hardcoded a literal home directory, splitting the ledger under any other service account — guarded by `test/deployment-hostnames.test.mjs`). |
203
203
  | `CLEAROTRON_REGISTER_RECORD_LOG` | **runtime-injected per run**: `<runDir>/_driver/register-record-bodies.jsonl` | Citation-fidelity log: the BODY of every fetched official record. ** moved it INTO the run** — created with the run, unioned into the run's`_records/`, archived and purged with it. There is no retention setting and no cleanup job, because it no longer grows on the box: held globally it reached 432 MB in 61 days on production and needed a rotation timer on every install. **Do not set this by hand** — a fixed value pins every run's bodies to one file and restores the problem. A box upgraded across still holds its old global file; nothing writes or reads it, the driver names it once per process on stderr, and archiving it is one`mv`. An empty log cannot read as verified: the run's successful `record_fetch` rows in the (still global) call ledger are compared against the assembled record set, and a gap is reported as a failure. |
204
204
  | `CORSEARCH_CALL_LOG` / `CORSEARCH_RECORD_LOG` | — | **Deprecated, honoured for one release.** These are the names these two variables carried before the rename. Unset on every deployed box (all three ran the homedir default), so what actually protects an upgrade is the filename fallback: a`corsearch-calls.jsonl` / `corsearch-records.jsonl` already on disk keeps being read where it sits. Resolution order is in `providers/_shared/ledger-path.mjs`. |
205
205
  | `CLEAROTRON_BAND_RUN_DIR` | set per dispatch | The run dir the band MCP server writes into, injected per stage — unset means the server has no run to write to and says so rather than guessing one. |
@@ -219,7 +219,7 @@ deployment may override (verify live values per deployment).
219
219
  | `CLEAROTRON_RUN_LOCK_POLL_MS` | 15000 | Run-slot acquire poll cadence. |
220
220
  | `CLEAROTRON_ADMISSION_BUDGET_MS` | 7200000 (2 h) | Runner stops claiming new jobs after this per activation; leftovers re-trigger a fresh activation. |
221
221
  | `CLEAROTRON_QUEUE_SCAN_MS` | 10000 (min 1000) | Mid-drain re-scan for newly arrived jobs. |
222
- | `CLEAROTRON_WHATIF_MAX_CONCURRENT` | 1 (min 1) | How many client what-ifs the runner drains at once. Deliberately NOT a run-slot: an experiment that took one from `CLEAROTRON_MAX_CONCURRENT_RUNS` could block an admitted paid clearance rather than merely share the box with it. This is a concurrency bound and not a spend control — the owner ruled spend controls out when he opened what-if to clients (2026-08-27), and nothing here refuses a client's next experiment. |
222
+ | `CLEAROTRON_WHATIF_MAX_CONCURRENT` | 1 (min 1) | How many queued what-ifs the runner drains at once. Deliberately NOT a run-slot: an experiment that took one from `CLEAROTRON_MAX_CONCURRENT_RUNS` could block an admitted paid clearance rather than merely share the box with it. This is a concurrency bound and not a spend control — the owner ruled spend controls out when he opened what-if to each company's people (2026-08-27), and nothing here refuses the next experiment. |
223
223
  | `CLEAROTRON_STOP_GRACE_MS` | 60000 | Grace after first SIGTERM before exit(1). |
224
224
  | `CLEAROTRON_MAX_CLAIM_AGE_MS` | 172800000 (48 h; 0 disables) | Hard ceiling on a claim's age (from the `.pid` sidecar mtime) — beyond it, re-claim regardless of liveness. |
225
225
  | `CLEAROTRON_KNOCKOUT_VARIANT_CAP` | unset (⇒ the lane's own cap) | Ceiling on variants a knockout screens per name. Set only to bound an unusually wide batch; absent means the lane decides. |
@@ -284,8 +284,9 @@ because the rule is about what PRODUCT CODE reads, not about what a run reads.
284
284
  | `DEMO_PORT` | `18900` | Port `npx clearotron demo` serves the replayed report on. `--port` overrides it. |
285
285
  | `CLEAROTRON_DEMO` | unset | `1` puts this install in the DEMO posture. **Ordering is real**: the four products are listed and orderable, the form, the plan and the confirmation are the product's own, and the confirmation resolves to a finished report that already exists rather than dispatching — no engine turn, no register call, no queue entry, no run directory (ruling 2026-08-31, superseding the greyed-control ruling of the same day). A product the demo carries no finished report for refuses and names which one. It also re-aims two boot warnings written for an operator of a real deployment at the visitor who is not one, from one place (`driver/demo-posture.mjs`). **Set by `npx clearotron demo`, not by an operator** — it is passed explicitly to the two processes that have a reason to know (the portal and the MCP door; the worker is not told, because a demo never queues anything for it to drain), and those run with `CLEAROTRON_NO_ENV_FILE=1`, so a stray `.env` can neither put a live install into demo mode nor take a demo out of one. Anything but the literal `1` is not a demo. Replaces `PORTAL_DEMO`, which named only one of the processes that has to know. |
286
286
  | `CLEAROTRON_TEST_FIXTURE_PROFILES` | unset | `1` makes the profile loader return the three suite fixtures, which are refused from every roster otherwise. Set by `scripts/test-run.mjs`, never by an operator; an explicit `includeTestFixtures` argument beats it. Effect class `harness`; the full contract is its row in `.env.example`. |
287
- | `CLEAROTRON_DEMO_PROFILES` | unset | `1` makes the profile loader return the bundled demo account, which a fresh install does not resolve (ruling 2026-09-08). Set by `clearotron demo`, `start --demo` and the suite runner, never by an operator. The gate is on the bundled layer, so a deployment's own configured store keeps its `demoData` accounts either way. Effect class `harness`; the full contract is its row in `.env.example`. |
287
+ | `CLEAROTRON_DEMO_PROFILES` | unset | `1` makes the profile loader return the bundled demo company, which a fresh install does not resolve (ruling 2026-09-08). Set by `clearotron demo`, `start --demo` and the suite runner, never by an operator. The gate is on the bundled layer, so a deployment's own configured store keeps its `demoData` companies either way. Effect class `harness`; the full contract is its row in `.env.example`. |
288
288
  | `CLEAROTRON_ORGANISATION_NAME` | unset | Your organisation's name, written quoted by `npx clearotron install`, which asks for it and not for a sign-in address. The first `clearotron start` files it as the first organisation in the grants file (`CLEAROTRON_ACCESS_FILE`) when that file holds none; from then on the grants file holds the name and renaming is an edit there. The portal also reads it, to name the organisation on its top bar and on its sign-in refusal page. Unset ⇒ no organisation is invented. `clearotron start --organisation <name>` supplies it for one start, and a demo never reads it. Effect class `deployment`; the full contract is its row in `.env.example`. |
289
+ | `CLEAROTRON_ADMINISTRATOR_CONTACT` | unset | Who a signed-in person contacts to change the address, the permissions or the companies on their sign-in: a mail address (with or without `mailto:`) or an http(s) address. The portal reads it where it reads the organisation name and sends it on the same `/me` answer; Preferences then links the words "Clearotron administrator" to it. Unset, or any value that is neither a mail nor a web address, ⇒ the words are plain text. Effect class `deployment`; the full contract is its row in `.env.example`. |
289
290
  | `PORTAL_LOCAL_CREDENTIAL` | `~/.cordillera/portal-local-credential.json` | Where local sign-in keeps its passphrase DIGEST. `clearotron start` points it inside the install's own base directory for a new install, so it mints its own passphrase instead of adopting a digest another install left; an install that has been signing in with the shared file keeps it. `npx clearotron demo` always points it inside the demo's own base directory and mints a new passphrase there on every start, so a demo never inherits a digest minted for another address, and removing the demo stays one `rm -rf`. |
290
291
  | `PORTAL_LOCAL_PASSPHRASE` | unset | **NEVER set this in a file.** An internal one-shot handoff, not an operator control: on a first FOREGROUND start the supervisor mints the passphrase and hands it to the portal it spawns *at the spawn call*, so the closing summary can print the value beside the address rather than sending a first-time reader back into eleven startup log lines for the one value in this product that cannot be read back. It is deliberately absent from the composed child environments, because that composition is what `--background` writes into the units' env file — a passphrase there would be a permanent plaintext copy on disk and the product's own sentence, "it is stored only as a digest", would stop being true. Setting it in any env file recreates exactly that. Lost passphrase: `clearotron passphrase --reset`. |
291
292
  | `PORTAL_URL` | `http://127.0.0.1:18802`, or built from `PORTAL_SERVICE_HOST`/`PORTAL_SERVICE_PORT` | Where the deploy tick's live-surface check expects to reach the portal. |
@@ -296,7 +297,8 @@ because the rule is about what PRODUCT CODE reads, not about what a run reads.
296
297
  | `CLEAROTRON_UPDATER_STAMP` | `_updater-identity.json` beside the update script, in the directory the updater runs from | The full path of the file in which the updater that deploys this box records which copy of itself ran. The updater writes it and deploy health reads it under this one name, so a box that moves the stamp sets it once for both. On a box with no updater unit, setting it says an updater exists elsewhere and is to be judged. Effect class `deployment`. |
297
298
  | `CLEAROTRON_CUT_REF` | `HEAD` | Which ref the cut decision reads the version from. **Read only by the release workflow, never set on a deployment.** The jobs that ask about `main` set it to `origin/main` explicitly, because their checkout is pinned to the run's own ref and `HEAD` there is that ref rather than the branch they are deciding about. A job that asks the wrong subject gets a confident wrong answer. |
298
299
  | `CLEAROTRON_RELEASE_WAIT_MS` | 25 minutes | How long a requested cut waits for the version pull request to merge itself before giving up. **Read only by the release workflow.** A rehearsal sets it to `0` so the wiring is exercised without holding a runner. Giving up is a quiet success by design — the scheduled run underneath catches a cut whose wait expired — so this budget failing shows up as a slow job rather than a red one. |
299
- | `CLEAROTRON_AGENT_MCP_URL` | unset (⇒ `null`) | The API-key MCP door advertised to a signed-in client. Null until that door is deployed, and the UI keeps its honest empty state rather than inventing a URL. |
300
+ | `ACTIONS_APPROVE_TOKEN` | unset | A GitHub token with Actions read and write, used to approve the version pull request's parked CI run so a cut does not wait for a person. **Read only by the release workflow, never set on a deployment.** The built-in token cannot approve a run — GitHub blocks self-approval — so this is a second, separate credential. Unset is the ordinary case and not an error: the script names the absent token and exits successfully, and the version run waits for a person as it did before. Actions write is broader than approval alone — it also dispatches workflows, cancels any run in the repository and deletes run logs. |
301
+ | `CLEAROTRON_AGENT_MCP_URL` | unset (⇒ `null`) | The API-key MCP door advertised to a signed-in person. Null until that door is deployed, and the UI keeps its honest empty state rather than inventing a URL. |
300
302
  | `CLEAROTRON_AGENTS` | derived | Comma-separated agent ids for `scripts/purge-runs.mjs` to sweep. |
301
303
  | `CLEAROTRON_E2E_DIR` | **none — the script refuses without it** | The config repo's `e2e/` directory. There is one suite and it is not in this repo (ruling 2026-08-07), so the comparison script names the variable rather than defaulting anywhere. |
302
304
  | `CLEAROTRON_E2E_EXPECT_DEMO_ROSTER` | unset | `1` makes the live-surface check REQUIRE the bundled demo roster. For a box that is meant to ship the demos; off elsewhere, so a real deployment is not failed for lacking them. |
@@ -323,14 +325,14 @@ cannot be read as one list.
323
325
  | `PERPLEXITY_API_KEY` | Marketplace grid research server. |
324
326
  | `EUIPO_CLIENT_ID` / `EUIPO_CLIENT_SECRET` / `EUIPO_ENVIRONMENT` | EUIPO OAuth2 client-credentials; environment defaults to **sandbox** — production requires an explicit set. |
325
327
  | `CLEAROTRON_MCP_URL` | Staff "Ask your AI" connector base, baked into the report at render — there is one report, and `portal-report.mjs` strips the block for non-staff readers at serve time. Fails closed → link omitted when unset. |
326
- | `CLEAROTRON_CLIENT_MCP_URL` | Client "Ask your AI" connector base, served LIVE by the portal's `/portal/api/mcp-access` (the Use-your-AI screen) — set it on the portal unit, or the screen shows its (correct) empty state. The render no longer reads it: the baked block is the staff connector above. Fails closed → `{url:null}` when unset. **Never pin a placeholder value:** the fail-closed branch keys on the var being EMPTY, so a placeholder host defeats the guard and hands out a dead connector address. |
327
- | `CLIENT_MCP_ACCOUNT_ACCESS` | `1` admits the signed-in CLIENT principal (kind `account`) on the client MCP surface: no token, scoped to the accounts the CF-verified email is granted. Off by default. Set WITHOUT `CLEAROTRON_ACCESS_FILE` ⇒ fatal start (`accountsForEmail` answers `"*"` with no guest list, and an unscoped wildcard must never be admitted). See `docs/CLIENT-MCP.md` — enabling this for an account with no `runCaps.dailyRuns` lets it start paid searches all day. |
328
+ | `CLEAROTRON_CLIENT_MCP_URL` | The client connector's "Ask your AI" base address, served LIVE by the portal's `/portal/api/mcp-access` (the Use-your-AI screen) — set it on the portal unit, or the screen shows its (correct) empty state. The render no longer reads it: the baked block is the staff connector above. Fails closed → `{url:null}` when unset. **Never pin a placeholder value:** the fail-closed branch keys on the var being EMPTY, so a placeholder host defeats the guard and hands out a dead connector address. |
329
+ | `CLIENT_MCP_ACCOUNT_ACCESS` | `1` admits the signed-in principal from outside your staff (kind `account`) on the client MCP surface: no token, scoped to the companies the CF-verified email is granted. Off by default. Set WITHOUT `CLEAROTRON_ACCESS_FILE` ⇒ fatal start (`accountsForEmail` answers `"*"` with no guest list, and an unscoped wildcard must never be admitted). See `docs/CLIENT-MCP.md` — enabling this for a company with no `runCaps.dailyRuns` lets it start paid searches all day. |
328
330
 
329
331
  ### Scrub guard
330
332
 
331
333
  | Var | Default | Meaning |
332
334
  |---|---|---|
333
- | `CLEAROTRON_IDENTIFIER_BLOCKLIST` | unset (⇒ sentinels) | Path to `identifier-blocklist.json` in the customer-config store: every customer and mark this repo has carried, paired with the demo twin that replaced it. Read by the client-identifier guard and the publication scan, never on a run path. **Unset is a supported mode**, not a degraded one — the guard runs on synthetic sentinels built into it, which exercise every branch of the matcher and identify nobody; that is how the public repository runs it. Set but unreadable, malformed, or below the size floor ⇒ **throws** (a truncated table reads as a smaller blocklist, and a smaller blocklist reads as a cleaner repo). The table is kept out of the product repo because the list of names *is* the thing the guard protects. |
335
+ | `CLEAROTRON_IDENTIFIER_BLOCKLIST` | unset (⇒ sentinels) | Path to `identifier-blocklist.json` in the config store: every real company and mark this repo has carried, paired with the demo twin that replaced it. Read by the identifier guard and the publication scan, never on a run path. **Unset is a supported mode**, not a degraded one — the guard runs on synthetic sentinels built into it, which exercise every branch of the matcher and identify nobody; that is how the public repository runs it. Set but unreadable, malformed, or below the size floor ⇒ **throws** (a truncated table reads as a smaller blocklist, and a smaller blocklist reads as a cleaner repo). The table is kept out of the product repo because the list of names *is* the thing the guard protects. |
334
336
 
335
337
  ### A/B, telemetry, debug
336
338
 
@@ -341,7 +343,7 @@ cannot be read as one list.
341
343
  | `CLEAROTRON_STAGE_THINKING` | unset (⇒ each stage's declared tier) | Per-stage thinking-tier override, `<stage>=<tier>[,…]` (e.g. `register-digest=high`) — the A/B instrument, so a suite arm needs no code fork or redeploy between runs. Thinking only: models are deliberately not overridable here, so one arm can never move two variables. **The env override is a dev/test instrument**; a permanent change edits the tier in `stages.mjs` and ships. Unknown stage or tier **throws** — `effortFor()` falls back to `medium`, so a typo would otherwise run a stage at a tier nobody chose and every number measured against it would be wrong. Read per call, so an arm can flip mid-process. |
342
344
  | `CLEAROTRON_AZURE_MODEL` | `azure-openai/gpt-5.4` | Target of the `azure` alias — a legacy catalogue entry no engine can run (see model tiers above). |
343
345
  | `CLEAROTRON_DUMP_JSON` | unset | Dump each attempt's raw engine envelope to `_driver/<stage>.attempt<N>.rawjson.json`. Opt-in: any value except `0`/`off`/`false`/`no`/empty arms it. |
344
- | `CLEAROTRON_DISPATCH_RECORD` | **on** | Write the verbatim message of every stage dispatch to `_driver/<stage>.attempt<N>[.repair<M>].dispatch.txt`, with `{file, sha, bytes, chars, kind}` on the attempt row. **Default ON** — `0`/`off`/`false`/`no` disarms it. Unlike `CLEAROTRON_DUMP_JSON` beside it, this is opt-OUT: the question it answers ("was the model given this?") is asked *after* the run that raised it, so a flag someone had to remember would be off on exactly the run that needed it. The files carry client identity verbatim and are deliberately not in the artifact table. |
346
+ | `CLEAROTRON_DISPATCH_RECORD` | **on** | Write the verbatim message of every stage dispatch to `_driver/<stage>.attempt<N>[.repair<M>].dispatch.txt`, with `{file, sha, bytes, chars, kind}` on the attempt row. **Default ON** — `0`/`off`/`false`/`no` disarms it. Unlike `CLEAROTRON_DUMP_JSON` beside it, this is opt-OUT: the question it answers ("was the model given this?") is asked *after* the run that raised it, so a flag someone had to remember would be off on exactly the run that needed it. The files carry the company's identity verbatim and are deliberately not in the artifact table. |
345
347
  | `CLEAROTRON_GATHER_SESSION_KEY` / `CLEAROTRON_GATHER_AGENT` / `CLEAROTRON_GATHER_SESSION_ID` | set per stage | Telemetry attribution into the provider-call ledger (set by the gather config; not operator-set). |
346
348
  | `CLEAROTRON_RECORD_AXIS` | set per dispatch (unset ⇒ the stage is not fanned out) | Binds one fan-out turn of a recording stage to the single member it may write. `stageOnce` suffixes a fan-out stage's label with its axis, the gather config resolves `<stage>:<axis>` back to the base stage's tool group, and this carries the axis to the recording server. A call whose payload names a different member than the turn is bound to is REFUSED, so a seat cannot write into a sibling's file — without the binding every turn of the fan-out would record over member one. Set by the driver; not operator-set. |
347
349
  | `PORTAL_READ_MODEL` | `claude-sonnet-5` | The model the portal's own compose-read turn uses. Distinct from the pipeline's tiers: this is a portal surface, not a stage. |
@@ -22,16 +22,16 @@
22
22
 
23
23
  | Tier | Who changes it | Through what | Examples |
24
24
  |---|---|---|---|
25
- | **T1 — Client** | The client (self-service) | Agent conversation → job spec; client portal | mark, classes, deadline, own profile fields, flags |
26
- | **T2 — Staff/ops UI** | Firm staff | the portal's brand screens, profile-service | customer profiles, project overlays, saved searches, run curation |
25
+ | **T1 — Company** | A company's own people (self-service) | Agent conversation → job spec; the portal | mark, classes, deadline, own profile fields, flags |
26
+ | **T2 — Staff/ops UI** | The operator's staff | the portal's company settings screens, profile-service | company profiles, project overlays, saved searches, run curation |
27
27
  | **T3 — Operator env** | Ops, on the box | the EnvironmentFile + restart/next-activation | hostnames, models, caps, timeouts, feature gates |
28
28
  | **T4 — Backend-only** | Ops, deliberately | env secrets, unit files, code defaults | credentials, token secrets, data-plane paths, dev seams |
29
- | **T5 — Edge auth** | Ops, in the edge console | the auth proxy's own console, off-box (reference: a Cloudflare Zero-Trust tenant) | ingress routes, access apps + policies |
29
+ | **T5 — Edge auth** | Ops, in the edge console | the auth proxy's own console, off-box (reference: Cloudflare Zero Trust) | ingress routes, access apps + policies |
30
30
 
31
- The UI mapping, as the `portal-ui` nav is actually structured: T2 → **`brand.profile`** ("Brand
32
- profile", everything specific to one brand owner) + **`brand.projects`** + **`brand.searches`**
33
- ("Custom searches"), plus **`admin.access`** ("People & access") for who may sign in; T3 →
34
- **read-only** in **`admin.config`** ("Global config"): which engine is running the searches and who is
31
+ The UI mapping, as the `portal-ui` nav is actually structured: T2 → **`brand.profile`** ("Profile",
32
+ everything specific to one company) + **`brand.projects`** + **`brand.searches`**
33
+ ("Search templates"), plus **`admin.access`** ("People & access") for who may sign in; T3 →
34
+ **read-only** in **`admin.config`** ("Installation settings"): which engine is running the searches and who is
35
35
  billed for them, and every provider a search depends on with a configured-or-missing state — secrets,
36
36
  paths and switch names deliberately excluded, visible to staff, changed only on the backend.
37
37
 
@@ -55,8 +55,8 @@ recorded.
55
55
  |---|---|---|---|
56
56
  | 1 | The EnvironmentFile (`%h/.env`) | secrets + T3 operator env for the generic units | ops, by hand. **Keep it dedicated to this product where possible** — sharing one env file with another stack couples rotation and backup blast-radius across products |
57
57
  | 2 | systemd user units (`driver/systemd/`, `mcp-server/remote/`) | ports, CF Access mirrors, paths. Two kinds — **generic** (defer to the EnvironmentFile; safe to sync verbatim) vs **template** (placeholders in-repo, real identity-edge values in the live copies; merge by hand after a diff — banner-marked). Copying a template over a live unit replaces working auth with placeholders that *look* configured, which is why the two kinds are distinguished at all | this repo's deploy |
58
- | 3 | The config store (separate git repo) | customer profiles, project overlays, frameworks/skills | profile-service + client portal git auto-commit; frameworks by git edit (deliberate) |
59
- | 4 | Client allowlist file (`CLIENT_ACCESS_MAP` JSON) | client per-email → customer grants | git + PR (today it lives in the integrator's repo — a candidate to move into the config store so the product owns all its config) |
58
+ | 3 | The config store (separate git repo) | company profiles, project overlays, frameworks/skills | profile-service + portal git auto-commit; frameworks by git edit (deliberate) |
59
+ | 4 | The allowlist file (`CLIENT_ACCESS_MAP` JSON) | per-email → company grants | git + PR (today it lives in the integrator's repo — a candidate to move into the config store so the product owns all its config) |
60
60
  | 5 | Web-server / edge routing (reverse-proxy block or tunnel ingress; this deployment: Caddy + Cloudflare) | which hostname reaches which loopback port; the pool's file-server root | ops |
61
61
  | 6 | Data roots (`CLEAROTRON_REPORTS_DIR`, shares) | published reports, quality flags | services |
62
62
  | 7 | **Edge console** (off-box; reference: Cloudflare) | ingress routes + access apps and who they admit | ops — **not recoverable from the box**; keep an exported note of apps/AUDs in the deployment's ops repo |
@@ -75,7 +75,7 @@ fills the same three roles, and the product requires none of them by name.
75
75
  `MCP_ALLOWED_EMAIL_DOMAINS` appear in unit files as **mirrors of the dashboard**, not a second
76
76
  setup. They must match the edge; rotation touches both. The CF-gated loopback services
77
77
  deliberately do NOT load the shared EnvironmentFile so one app's AUD can't shadow another's
78
- (client-access/client-mcp refuse to start if client AUD == staff AUD).
78
+ (`client-access`/`client-mcp` refuse to start if the client AUD == the staff AUD).
79
79
  3. **Rendered links (T3, env):** `CLEAROTRON_REPORTS_URL`, `CLEAROTRON_MCP_URL`, `CLEAROTRON_CLIENT_MCP_URL`,
80
80
  `CLEAROTRON_ACCESS_DOMAIN` are neither of the above — they are what gets *printed into reports and
81
81
  emails*. Wrong values render dead links; unset values are omitted.
@@ -88,18 +88,18 @@ product doc.
88
88
 
89
89
  | Surface | Tier | Managed via | Storage | State today |
90
90
  |---|---|---|---|---|
91
- | **Job spec** (per matter): id, forwarder(+email/domain), markName/marks[], classes\|goods/use, ref, profileKey, projectKey, searchLevel/recipeKey, deliveryRoute, customer(+Unknown), deliverableSpec, commercialFlexibility, priorUse, dupOverride, deadline, brief | T1 | agent conversation → `start_run` MCP verb (ops token) → queue; validated by `enqueue-schema.mjs` | queue → run dir | LIVE (conversational; portal `run/plan`+`run` API exists) |
92
- | **Customer profile** (17 keys — identity/rating/provenance: name, matchDomains, selfExclusionOwners, frameworkPath, workedExamplesPath, allowedRecipes, jxPolicy, runCaps, demoData (`true` marks the record as demo data; a real clearance is refused at the runner's admission wall); overlayable: platforms, defaultClasses, defaultJurisdictions, marketplaceDensity, delivery, riskAppetite, industry, defaultProduct) | T2 (staff) + T1 (client edits own via portal §C) | profile-service UI (staff, `/profiles/*`); client-access UI (own profile) | config store, git auto-commit | LIVE. Merge law: project **replaces** every overlayable key except `platforms`, which **unions** (client floor never subtractable) |
93
- | **Project overlays** (8 overlayable keys) | T2 | profile-service UI | config store `profiles/projects/<cust>/` | LIVE. The project form deliberately withholds `defaultProduct` and both `delivery` sub-keys — for the first the sparse save path has no `""` ⇒ clear branch, so the control could only ever be turned on; for the second the engine replaces `delivery` wholesale, so a partial overlay would silently drop the customer's other sub-keys. Both are customer-level controls until the server side changes |
91
+ | **Job spec** (per matter): id, forwarder(+email/domain), markName/marks[], classes\|goods/use, ref, profileKey, projectKey, searchLevel/recipeKey, deliveryRoute, `customer`(+Unknown), deliverableSpec, commercialFlexibility, priorUse, dupOverride, deadline, brief | T1 | agent conversation → `start_run` MCP verb (ops token) → queue; validated by `enqueue-schema.mjs` | queue → run dir | LIVE (conversational; portal `run/plan`+`run` API exists) |
92
+ | **Company profile** (17 keys — identity/rating/provenance: name, matchDomains, selfExclusionOwners, frameworkPath, workedExamplesPath, allowedRecipes, jxPolicy, runCaps, demoData (`true` marks the record as demo data; a real clearance is refused at the runner's admission wall); overlayable: platforms, defaultClasses, defaultJurisdictions, marketplaceDensity, delivery, riskAppetite, industry, defaultProduct) | T2 (staff) + T1 (a company's people edit its own via portal §C) | profile-service UI (staff, `/profiles/*`); the portal (own profile) | config store, git auto-commit | LIVE. Merge law: project **replaces** every overlayable key except `platforms`, which **unions** (the company's floor is never subtractable) |
93
+ | **Project overlays** (8 overlayable keys) | T2 | profile-service UI | config store `profiles/projects/<cust>/` | LIVE. The project form deliberately withholds `defaultProduct` and both `delivery` sub-keys — for the first the sparse save path has no `""` ⇒ clear branch, so the control could only ever be turned on; for the second the engine replaces `delivery` wholesale, so a partial overlay would silently drop the company's other sub-keys. Both are company-level controls until the server side changes |
94
94
  | **Frameworks / skills** (risk-framework-<key>.md + .manifest.json, worked examples, SKILL.md) | T2 (senior-lawyer content) | git edits in the config store (deliberate — the prose deck is the rating authority) | config store `skills/prelim-search/` | LIVE via git; no UI by design |
95
95
  | **Recipes / saved searches** (base level + component toggles + emailTable/defaultDeadlineDays/standingInstructions) | T2 | recipe-service UI | `<recipesDir>/<cust>/<slug>.json`, git | **DARK** — code complete, no unit deployed. A saved search is honoured wherever it resolves (the `CLEAROTRON_RECIPES_MODE` door was retired 2026-07-27) |
96
96
  | **Run curation** (archive folds, republish, index regen) | T2 | `pool-admin.mjs` CLI only | pool `archive-tags.json` | LIVE, CLI-only |
97
- | **Client allowlist** (`{version, grants:[{email, customer}]}`) | T2 | git + PR on the `CLIENT_ACCESS_MAP` file | see §2 row 4 | LIVE, file-only; surfaced read-only at `admin.access` |
98
- | **Ops tokens** (scope ops/user, verbs, accounts, TTL) | T4 | `mint-token.mjs` CLI; jti denylist file | operator-held tokens | LIVE, CLI |
97
+ | **Allowlist** (`{version, grants:[{email, customer}]}`) | T2 | git + PR on the `CLIENT_ACCESS_MAP` file | see §2 row 4 | LIVE, file-only; surfaced read-only at `admin.access` |
98
+ | **Ops tokens** (scope ops/user, verbs, companies, TTL) | T4 | `mint-token.mjs` CLI; jti denylist file | operator-held tokens | LIVE, CLI |
99
99
 
100
100
  ## 4b. The install surface names ()
101
101
 
102
- The variables a **customer or installer** ever types carry the product’s own prefix. They are listed
102
+ The variables a **user or installer** ever types carry the product’s own prefix. They are listed
103
103
  by name in §5 below and in the upgrade table in INSTALL.md.
104
104
  `CLEAROTRON` is the internal codename of the first product this engine shipped and means nothing to a reader
105
105
  who has not read the code. Vendor credentials keep the vendor’s name (`SIGNA_API_KEY`,
@@ -152,8 +152,9 @@ structural, or dev seam); [dev] = dev/test seam, never set in prod.
152
152
  | `CLEAROTRON_MCP_URL` | fail-closed omit | Staff "Ask your AI" connector base |
153
153
  | `CLEAROTRON_CLIENT_MCP_URL` | fail-closed omit | Client connector base |
154
154
  | `CLEAROTRON_ACCESS_DOMAIN` | omit note | Identity domain in the delivery email access note |
155
- | `CLEAROTRON_BOX` | none — required (unset or unrecognised ⇒ the unit-inventory line fails and names this variable) | Which deployment this is (`prod` \| `test`), for `scripts/live-surface-check.mjs`'s unit inventory. Self-declared, never inferred from the account name. Without a recognised value, the half that looks for a unit declared here and not running cannot run, because a guess would report every other deployment's units missing; so the line fails instead of passing with that half unrun |
156
- | `CLEAROTRON_BRAND_NAME` / `CLEAROTRON_BRAND_TAGLINE` / `CLEAROTRON_BRAND_PRODUCT` | reference-tenant literals in `shared/brand.mjs` | Tenant brand seam (single-sourced) |
155
+ | `CLEAROTRON_BOX` | none — required (unset or unrecognised ⇒ the unit-inventory line fails and names this variable) | Which deployment this is (`prod` \| `test`), for `scripts/live-surface-check.mjs`'s unit inventory. Self-declared, never inferred from the login name. Without a recognised value, the half that looks for a unit declared here and not running cannot run, because a guess would report every other deployment's units missing; so the line fails instead of passing with that half unrun |
156
+ | `CLEAROTRON_BRAND_NAME` / `CLEAROTRON_BRAND_TAGLINE` / `CLEAROTRON_BRAND_PRODUCT` | reference-deployment literals in `shared/brand.mjs` | Installation brand seam (single-sourced) |
157
+ | `CLEAROTRON_ADMINISTRATOR_CONTACT` | unset ⇒ plain text, no link | The administrator contact Preferences links "Clearotron administrator" to — a mail or http(s) address, read in `shared/brand.mjs`; any other value reads as unset |
157
158
 
158
159
  ### 5.2 Engine & models — T3
159
160
 
@@ -230,7 +231,7 @@ Per-lane `CLEAROTRON_NATIVE_LANGUAGE_<XX>` — `zh`, `ja` and `ko` all work (`LA
230
231
  `driver/jx-lanes.mjs`); default on, set `0` to kill one lane — plus `CLEAROTRON_JX_SERP_DEADLINE_MS`.
231
232
  `CLEAROTRON_JX_SERP_GRID`, `CLEAROTRON_JX_NATIVEREAD` and `CLEAROTRON_JX_CONSUME` were **deleted by item 8**
232
233
  under ADR-0002: each was off here and on in production, so the shipped default described a
233
- configuration nobody ran. The slices now run on conditions a client can already see.
234
+ configuration nobody ran. The slices now run on conditions a requester can already see.
234
235
 
235
236
  `CLEAROTRON_KNOCKOUT_MODE`, `CLEAROTRON_JX_LANES` and `CLEAROTRON_RECIPES_MODE` were **RETIRED 2026-07-27** and
236
237
  have no reader. Depth availability is decided by `BUILT` (`driver/search-policy.mjs`) and the wired
@@ -260,13 +261,13 @@ bodies to one file and restores the unbounded growth the move removed),
260
261
  `PROFILE_REPO_ROOT`, `RECIPE_REPO_ROOT`, `CLEAROTRON_OAUTH_BRIDGE` (module-relative),
261
262
  `OAUTH_BRIDGE_CREDS_DIR`, `OAUTH_BRIDGE_CLIENT_NAME`.
262
263
 
263
- `PROFILE_DIR` **left this list.** The customer store is named once, by
264
+ `PROFILE_DIR` **left this list.** The company store is named once, by
264
265
  `CLEAROTRON_CUSTOMERS_DIR`, and resolved through `shared/customer-store.mjs` for the settings
265
266
  surface and the runs alike. It is not accepted as a fallback: a box setting only the retired name
266
267
  would otherwise pull the settings surface onto a second store, which is the split that issue
267
268
  closed. It still appears in `env-set-in-production.txt` because production is measured, not
268
269
  edited — that box runs pre-rebuild code and genuinely still sets it.
269
- | `CLEAROTRON_JX_SUBCLASS_DB` | unset ⇒ the similar-group lookup REFUSES by name | Path to `similar-groups.db`, built by `node providers/jx-subclass/load-public.mjs` from the committed `public/` tables. Not committed (the office sources permit redistributing the data, not their prose), so a deployment builds it. Unset or missing is a refusal, never an empty answer: `node:sqlite` creates an empty file on open and every lookup over it would read as "no similar groups" — a false clear in the offices a Western client can least check |
270
+ | `CLEAROTRON_JX_SUBCLASS_DB` | unset ⇒ the similar-group lookup REFUSES by name | Path to `similar-groups.db`, built by `node providers/jx-subclass/load-public.mjs` from the committed `public/` tables. Not committed (the office sources permit redistributing the data, not their prose), so a deployment builds it. Unset or missing is a refusal, never an empty answer: `node:sqlite` creates an empty file on open and every lookup over it would read as "no similar groups" — a false clear in the offices a Western company can least check |
270
271
 
271
272
  ### 5.7 Delivery & comms — T3/T4
272
273
 
@@ -300,10 +301,10 @@ set), `TRADEMARK_MCP_TOKEN_SECRET` (+`TRADEMARK_MCP_TOKEN_SECRET_PREVIOUS` rotat
300
301
  only**; the portal refuses to start without it — see [docs/SECURITY.md](../SECURITY.md)).
301
302
 
302
303
  **Scrub guard (test-time only — never read on a run path, and outside the five tiers above).**
303
- `CLEAROTRON_IDENTIFIER_BLOCKLIST` points at `identifier-blocklist.json` in the customer-config store: the
304
- retired customer/mark roster the scrub guard matches on, kept out of the product repo because the list
305
- of names *is* what the guard protects. Unset is a SUPPORTED mode, not a degraded one — the guard runs
306
- on synthetic sentinels built into the guard, which is how the public repository runs
304
+ `CLEAROTRON_IDENTIFIER_BLOCKLIST` points at `identifier-blocklist.json` in the config store: the
305
+ retired roster of real companies and marks the scrub guard matches on, kept out of the product repo
306
+ because the list of names *is* what the guard protects. Unset is a SUPPORTED mode, not a degraded one —
307
+ the guard runs on synthetic sentinels built into the guard, which is how the public repository runs
307
308
  it. Set-but-unreadable, malformed, or below the size floor **throws**: a truncated table reads as a
308
309
  smaller blocklist, and a smaller blocklist reads as a cleaner repo. **Whether the real table is required
309
310
  is the CALLER's declaration, never the environment's**: `publication-scan.mjs` asks for it in its own
@@ -386,7 +387,7 @@ to be written out.
386
387
  | recipe service | `RECIPE_OIDC_ISSUER` | `RECIPE_JWKS_URL` | `RECIPE_EMAIL_CLAIM` | `RECIPE_AUTH_HEADER` |
387
388
  | client MCP origin | `CLIENT_MCP_OIDC_ISSUER` | `CLIENT_MCP_JWKS_URL` | `CLIENT_MCP_EMAIL_CLAIM` | `CLIENT_MCP_AUTH_HEADER` |
388
389
 
389
- Each is T4, each is optional, and each unset falls back exactly as the two older sets do — the client
390
+ Each is T4, each is optional, and each unset falls back exactly as the two older sets do — the client MCP
390
391
  row to the staff face's matching value first (`TRADEMARK_MCP_OIDC_ISSUER`, `TRADEMARK_MCP_JWKS_URL`,
391
392
  `TRADEMARK_MCP_EMAIL_CLAIM`, `TRADEMARK_MCP_AUTH_HEADER`), then to the shapes derived from
392
393
  `CF_ACCESS_TEAM` for whichever JWT-fronting proxy the deployment runs.
@@ -419,7 +420,7 @@ replayed as the other. First start in local mode mints a passphrase and prints i
419
420
  byte-for-byte the behaviour that shipped before the name existed, dev bypass included; `token` requires
420
421
  a valid HMAC-signed scoped access key on **every** request and runs no auth proxy at all. An
421
422
  unrecognised value is fatal. `token` is the far end of the local install's Start button — the portal
422
- holds a verb-scoped, account-capped ops key and this door demands it — and it is **not** a bypass:
423
+ holds a verb-scoped, company-capped ops key and this door demands it — and it is **not** a bypass:
423
424
  `lib/http-handler.mjs` refuses outright to build it alongside `TRADEMARK_MCP_AUTH_DISABLED`, because a
424
425
  synthetic identity would answer before the mandatory key ever ran. The mode is loopback-only (an ops key
425
426
  travels in a header or the query string), requires `TRADEMARK_MCP_ALLOWED_HOSTS` exactly as the
@@ -495,7 +496,15 @@ origin; all portal config lives server-side in portal-service.
495
496
 
496
497
  These are read by the release workflow and by nothing a deployment runs. They are listed here because a
497
498
  name absent from this register is a name nobody can look up, not because an operator has any reason to set
498
- one — and setting either on a box does nothing at all.
499
+ one — and setting any of them on a box does nothing at all.
500
+
501
+ `ACTIONS_APPROVE_TOKEN` (unset) — a GitHub token with Actions read and write, used to approve the
502
+ version pull request's parked CI run so that a cut does not wait for someone to click. That run is
503
+ authored by the repository's own Actions bot and GitHub parks bot-authored runs; the built-in token
504
+ cannot release one, because self-approval is blocked deliberately. Unset is the ordinary case: the
505
+ script names the absent token and exits successfully, and the run waits for a person as before.
506
+ Actions write is wider than approving — it also dispatches workflows, cancels any run in the
507
+ repository and deletes run logs — so it is worth rotating on the same schedule as a deploy key.
499
508
 
500
509
  `CLEAROTRON_CUT_REF` (default `HEAD`) — which ref the cut decision reads the version from. The jobs that
501
510
  ask about `main` set it to `origin/main` explicitly, because their checkout is pinned to the run's own ref
@@ -515,8 +524,8 @@ change. The product's known mirror classes:
515
524
 
516
525
  | Value class | Copies (by design) | Break mode if they diverge |
517
526
  |---|---|---|
518
- | CF Access team / AUD / domain gates | edge (dashboard) + each fronted unit's inline env | staff or client lockout, service by service |
519
- | `TRADEMARK_MCP_TOKEN_SECRET` | EnvironmentFile + any integrator-hosted artifacts-MCP env block | run-bound client tokens fail verification |
527
+ | CF Access team / AUD / domain gates | edge (dashboard) + each fronted unit's inline env | staff or company lockout, service by service |
528
+ | `TRADEMARK_MCP_TOKEN_SECRET` | EnvironmentFile + any integrator-hosted artifacts-MCP env block | run-bound `user` tokens fail verification |
520
529
  | Register-provider credentials | EnvironmentFile + integrator plugin config (when both consume the provider) | one consumer silently unauthenticated |
521
530
  | Profiles/skills store paths | EnvironmentFile + service unit files (+ integrator MCP env) | roster-mismatch class (the PR #14 incident) |
522
531
  | Pool root | code default + units + web-server file root (+ integrator MCP env) | reports publish where nothing serves |
@@ -1,22 +1,22 @@
1
- # 05 — Customer Profiles
1
+ # 05 — Company Profiles
2
2
 
3
3
  > Part of the architecture pack (`docs/architecture/`). The driver's module tree and the headless
4
4
  > integrator contract are in [`driver/README.md`](../../driver/README.md).
5
- > This chapter describes the profile *mechanism*. Real customer bundles load from an external store
6
- > via `CLEAROTRON_CUSTOMERS_DIR` and no customer is named here; the package ships `generic` and the demo
7
- > brand owner, and the repository holds three further synthetic profiles for the test suite — see
5
+ > This chapter describes the profile *mechanism*. Real company bundles load from an external store
6
+ > via `CLEAROTRON_CUSTOMERS_DIR` and no real company is named here; the package ships `generic` and the
7
+ > demo company, and the repository holds three further synthetic profiles for the test suite — see
8
8
  > [`driver/profiles/`](../../driver/profiles/).
9
9
 
10
- One engine, never forked — three layers. The reasoning core is shared by every client; the client
10
+ One engine, never forked — three layers. The reasoning core is shared by every company; the company's
11
11
  context sharpens judgment without ever overriding it; the delivery layer shapes presentation only.
12
- The profile system is where that principle is *enforced*, not just stated: every per-client knob is
12
+ The profile system is where that principle is *enforced*, not just stated: every per-company knob is
13
13
  either consumed by named code or rejected at load, and configuration that attempts to move a legal
14
14
  rating is refused by pattern guards and by stage-level firewalls.
15
15
 
16
16
  ## The bundle
17
17
 
18
- A customer = one git-owned JSON file `profiles/<key>.json`, plus optionally: a prose context pack
19
- (`<key>.context.md`), a per-customer rating framework pair in `skills/prelim-search/`
18
+ A company = one git-owned JSON file `profiles/<key>.json`, plus optionally: a prose context pack
19
+ (`<key>.context.md`), a per-company rating framework pair in `skills/prelim-search/`
20
20
  (`risk-framework-<key>.md` + its `.manifest.json`, plus worked examples), and per-engagement
21
21
  project overlays under `profiles/projects/<key>/`.
22
22
 
@@ -27,25 +27,25 @@ that reads it in `FIELD_CONSUMERS`, and CI asserts the manifest and the key list
27
27
  | Key | Layer | What it drives |
28
28
  |---|---|---|
29
29
  | `name` (required) | identity | Anchor for the self-exclusion gate (`applicantMatchesProfile` — word-boundary containment, so "Company" matches "Company Corporation" but not "Companyish Ltd") |
30
- | `matchDomains[]` | identity | Forwarder-domain fallback resolution; cross-file overlap is a load error (directory order must never decide a customer) |
30
+ | `matchDomains[]` | identity | Forwarder-domain fallback resolution; cross-file overlap is a load error (directory order must never decide a company) |
31
31
  | `platforms[]` (required) | structural | The store domains the marketplace grid sweeps, verbatim — dictated into the worker spec and enforced by the receipts join. Bare domains only, no `"web"` (the general-web cell is implicit), no duplicates: a one-character slip bricks every run under the profile, so load guards are strict |
32
32
  | `defaultClasses[]` / `defaultJurisdictions[]` | structural | Matter-frame defaults when the request names none |
33
- | `selfExclusionOwners[]` | structural | Never flag the client against itself — seeded only when the job's applicant *is* the profile customer |
33
+ | `selfExclusionOwners[]` | structural | Never flag the company against itself — seeded only when the job's applicant *is* the profile's company |
34
34
  | `marketplaceDensity` | structural | `sparse` (default) or `dense` — selects the grid cell budget (98 vs 16 cells/call, both calibrated from real truncation incidents) |
35
35
  | `industry` | context | Sector context in the matter frame — context, never a rule |
36
36
  | `riskAppetite` | context | Prose posture only; reaches **curation stages only**, never synthesis |
37
37
  | `delivery` | delivery | `{email: "summary", privileged: bool, style: prose, template: closed-enum}` — presentation only. `email` is inert as of 2026-07-28 (every run gets a cover note); `"table"` is accepted-but-retired so stored profiles keep loading |
38
- | `frameworkPath` | rating authority | The customer's own risk framework deck; absent ⇒ the Generic default rates the matter ("nothing in between") |
39
- | `workedExamplesPath` | context | Per-customer synthesis depth target |
38
+ | `frameworkPath` | rating authority | The company's own risk framework deck; absent ⇒ the Generic default rates the matter ("nothing in between") |
39
+ | `workedExamplesPath` | context | Per-company synthesis depth target |
40
40
  | `defaultProduct` | entitlement | Which search runs when a request names none (`search-policy.mjs`). May be unset — a clearance that names no product is then named by its own resolved territories |
41
- | `allowedRecipes[]` | entitlement | The closed menu of searches this account may trigger; absent ⇒ everything allowed. Non-empty when present |
41
+ | `allowedRecipes[]` | entitlement | The closed menu of searches this company may trigger; absent ⇒ everything allowed. Non-empty when present |
42
42
  | `jxPolicy` | entitlement | The native-language deepening posture (declared lanes, escalation, provider stance) — policy, never capability. Frozen into the run sidecar so a resume keeps it |
43
- | `runCaps` | admission | Per-account admission caps `{maxQueued?, dailyRuns?, monthlyRuns?}`, integers 1–10000, at least one set. Enforced at the runner's claim chokepoint for **both** intake doors (email and portal) — the run-slot cap does not bound a client that can trigger its own searches. **Two behaviours it is worth knowing before you rely on a cap** — see below |
43
+ | `runCaps` | admission | Per-company admission caps `{maxQueued?, dailyRuns?, monthlyRuns?}`, integers 1–10000, at least one set. Enforced at the runner's claim chokepoint for **both** intake doors (email and portal) — the run-slot cap does not bound people who can trigger their own searches. **Two behaviours it is worth knowing before you rely on a cap** — see below |
44
44
 
45
45
  **Two `runCaps` behaviours to know before relying on a cap** (`checkRunCaps`, `driver/runner.mjs`):
46
46
  **a cap written onto `generic` does nothing** — it is exempt by design, so a long-lived credential
47
- pointed there spends without limit; and a client run with no `dailyRuns` gets
48
- `DEFAULT_CLIENT_DAILY_RUNS`, not unlimited.
47
+ pointed there spends without limit; and a run a company's own people start with no
48
+ `dailyRuns` gets `DEFAULT_CLIENT_DAILY_RUNS`, not unlimited.
49
49
 
50
50
  **Derived, never stored** (storing either is its own louder load error): the grid floor
51
51
  `minCellsPerVariant = platforms.length + 1` and `batchSize = gridCellBudget / floor`.
@@ -56,7 +56,7 @@ never sees it): curated background facts, standing concerns, prior-matter learni
56
56
  conditionals are rejected at load; the fix is to rephrase the rule as a concern or question. It
57
57
  feeds `report-overview` as context that "NEVER changes a band" and never reaches synthesis.
58
58
 
59
- **The framework layer** (`framework.mjs`) is where per-client rating vocabulary lives. The
59
+ **The framework layer** (`framework.mjs`) is where per-company rating vocabulary lives. The
60
60
  framework itself is a prose deck the model reasons *with*; the code-side manifest
61
61
  (`<framework>.manifest.json`) carries **vocabulary and order only** — 2–8 ordered bands, each
62
62
  `{label, tone}`, no digits in labels ("a numbered band is a score in disguise"), and structurally
@@ -64,8 +64,8 @@ framework itself is a prose deck the model reasons *with*; the code-side manifes
64
64
  CI-linted, every profile's framework selection must resolve to a manifest, and bands-shaped decks
65
65
  are checked to carry no legacy scoring machinery (`test/framework-lint.test.mjs`).
66
66
 
67
- > **Per-customer decks legitimately diverge, and no test forbids it.** There is no
68
- > "doctrine-lockstep" check keeping rating tables byte-identical across customer frameworks: the
67
+ > **Per-company decks legitimately diverge, and no test forbids it.** There is no
68
+ > "doctrine-lockstep" check keeping rating tables byte-identical across company frameworks: the
69
69
  > framework in force is what *rates* the matter, so divergence is the point. The integrity guarantee
70
70
  > is elsewhere — closed manifests (vocabulary only), the anti-rule guards below, and the
71
71
  > framework-lint suite (`test/framework-lint.test.mjs`). Code comments in `stages.mjs` still
@@ -76,19 +76,19 @@ split 8/8 and every future field must choose a side (`PROJECT_KEYS ∪ CUSTOMER_
76
76
  KNOWN_PROFILE_KEYS`, asserted by test). **Overlayable (8):** platforms, defaultClasses,
77
77
  defaultJurisdictions, marketplaceDensity, delivery, riskAppetite, industry, defaultProduct — a
78
78
  distinct engagement legitimately runs a different product and different marketplaces.
79
- **Customer-only (8):** name, matchDomains, selfExclusionOwners, frameworkPath, workedExamplesPath,
80
- allowedRecipes, jxPolicy, runCaps — identity, rating authority and entitlement stay whole-customer,
81
- and a project that could widen its own caps would hollow out the customer's. Effective resolution
82
- merges project → customer → generic with a per-field `origins` map frozen into the run.
79
+ **Company-only (8):** name, matchDomains, selfExclusionOwners, frameworkPath, workedExamplesPath,
80
+ allowedRecipes, jxPolicy, runCaps — identity, rating authority and entitlement stay whole-company,
81
+ and a project that could widen its own caps would hollow out the company's. Effective resolution
82
+ merges project → company → generic with a per-field `origins` map frozen into the run.
83
83
 
84
84
  ## Resolution and the frozen sidecar
85
85
 
86
86
  ```mermaid
87
87
  flowchart TD
88
88
  J["job arrives (queue file)"] --> PK{"job.profileKey<br/>names a roster key?"}
89
- PK -- yes --> P1["that customer wins<br/>(intake AI resolved it)"]
89
+ PK -- yes --> P1["that company wins<br/>(intake AI resolved it)"]
90
90
  PK -- no --> FD{"forwarderDomain matches<br/>a profile's matchDomains?"}
91
- FD -- yes --> P2["domain-matched customer"]
91
+ FD -- yes --> P2["domain-matched company"]
92
92
  FD -- no --> P3["generic (required fallback)"]
93
93
  P1 --> PROJ{"job.projectKey?"}
94
94
  P2 --> PROJ
@@ -110,10 +110,10 @@ Three rules make this trustworthy:
110
110
  profiles/ edit mid-run can never change a live run's floor or platforms; resume/experiment
111
111
  and every validator read the same immutable file; a corrupt sidecar crashes loudly. `profileSha`
112
112
  (canonical-JSON sha256) makes "which profile/framework rated this run" verifiable by recompute.
113
- A mid-run late-bind of the customer re-classifies findings only — it never re-resolves the
113
+ A mid-run late-bind of the company re-classifies findings only — it never re-resolves the
114
114
  profile.
115
- - **An unbound run can never be presented customer-framed.** No key + no domain ⇒ generic ⇒ neutral
116
- delivery (no client table, no privileged header) — pinned by test.
115
+ - **An unbound run can never be presented as any company's.** No key + no domain ⇒ generic ⇒ neutral
116
+ delivery (no review table, no privileged header) — pinned by test.
117
117
 
118
118
  ## The guardrails
119
119
 
@@ -124,7 +124,7 @@ Three rules make this trustworthy:
124
124
  | **Anti-threshold guard** | `riskAppetite` (and context pack, and delivery style) reject numeric/threshold/rule shapes at load — percentages, comparison operators, "threshold", level/composite cutoffs, imperative ratings | `profiles.mjs` |
125
125
  | **The D1 firewall** | Stage messages decide which stage *sees* what: synthesis gets framework + worked examples and **no** appetite/pack/style; the three curation stages get them labelled "emphasis only / NEVER changes a band" | `stages.mjs` |
126
126
  | **Framework manifest constraints** | Vocabulary + order only; no digit labels; closed keys; 2–8 bands; deck⇄manifest lint | `framework.mjs`, `test/framework-lint.test.mjs` |
127
- | **Freeze completeness** | A configured field must be carried by `freezeProfile` or it is silently never applied — the exact 2026-06-19 bug (frameworkPath dropped from the freeze quietly rated two customers under the Generic default); regression-pinned | `pipeline.mjs`, `test/framework-freeze.test.mjs` |
127
+ | **Freeze completeness** | A configured field must be carried by `freezeProfile` or it is silently never applied — the exact 2026-06-19 bug (frameworkPath dropped from the freeze quietly rated two companies under the Generic default); regression-pinned | `pipeline.mjs`, `test/framework-freeze.test.mjs` |
128
128
 
129
129
  Honest boundary: the regex guards are conservative-reject and *necessary, not sufficient* — a
130
130
  pure-prose rule can pass them; the evaluation layer (reference library / review) is the catch for
@@ -137,7 +137,7 @@ A small loopback HTTP service (`profile-service.mjs`, systemd user unit, `PROFIL
137
137
  which carries the deployment's own directories and is therefore part of a deployment rather than of
138
138
  this source tree.
139
139
 
140
- - **Endpoints**: roster list, per-customer view (config + derived values + framework box), validate
140
+ - **Endpoints**: roster list, per-company view (config + derived values + framework box), validate
141
141
  (server-side dry run), save (validated **auto-commit** — creates or updates the JSON + context
142
142
  pack and commits authored as the signed-in identity), health (unauthenticated liveness), plus the
143
143
  project endpoints.
@@ -150,13 +150,13 @@ this source tree.
150
150
  (a body-supplied author is ignored); every write re-runs the *same* load-time validators the
151
151
  driver uses, so the UI can never persist a profile the driver would reject; and
152
152
  `frameworkPath`/`workedExamplesPath` are **code-owned** — the on-disk value always wins because a
153
- 2026-07-04 UI save once silently stripped both fields and flipped two customers to the Generic default
153
+ 2026-07-04 UI save once silently stripped both fields and flipped two companies to the Generic default
154
154
  framework (`preserveCodeOwned`, `profile-service.mjs`).
155
155
  - **Governance posture**: UI saves are validated auto-commits with no PR gate. Recovery is
156
156
  `git revert` plus the audit log (`profiles/_audit.log`, git-tracked). In-flight runs are
157
157
  unaffected (sidecar freeze); changes ship to the next run.
158
158
 
159
- ## Onboarding a new client — the runbook
159
+ ## Onboarding a new company — the runbook
160
160
 
161
161
  **Path A — git PR** (the bespoke-engagement default):
162
162
 
@@ -173,7 +173,7 @@ this source tree.
173
173
  report reads right.
174
174
  5. Merge + deploy.
175
175
 
176
- **Path B — the config UI**: sign in through CF Access → "+ New customer" → fill the form →
176
+ **Path B — the config UI**: sign in through CF Access → "+ New company" → fill the form →
177
177
  "Check first" (dry-run validate) → "Save" (validated auto-commit + audit line).
178
178
 
179
179
  **Either path, afterwards:**
@@ -181,9 +181,9 @@ this source tree.
181
181
  - **Routing**: intake stamps `job.profileKey` (the primary selector); optionally add
182
182
  `matchDomains` for forwarder-based fallback. A profile with empty `matchDomains` is reachable by
183
183
  profileKey only.
184
- - **Per-customer framework** (optional; git-only, legal-team work — the UI cannot set it): add the
184
+ - **Per-company framework** (optional; git-only, legal-team work — the UI cannot set it): add the
185
185
  deck + manifest + worked examples under `skills/prelim-search/`, set the two paths in the profile
186
- JSON via git. Until then the customer rates under the Generic default.
186
+ JSON via git. Until then the company rates under the Generic default.
187
187
  - **Per-engagement overlay** (optional): `profiles/projects/<key>/<slug>.json` with the 8
188
188
  overlayable keys; intake stamps `job.projectKey` to select it.
189
189
 
@@ -85,7 +85,7 @@ properties below, each of which was learned the expensive way:
85
85
  them; silent auto-promotion twice resurrected intentionally deleted files. A skill relocated into
86
86
  the driver tree is relocation, not drift.
87
87
  4. **Driver block** — install dependencies, copy the seven unit files + `daemon-reload`,
88
- install/enable/restart the sibling services (profile-service, client access/MCP), and
88
+ install/enable/restart the sibling services (`profile-service`, `client-access`, `client-mcp`), and
89
89
  **pre-create the queue dirs and the outbox**: an inotify `.path` watch on a directory that does
90
90
  not exist never fires.
91
91
  5. **Restart the gateway last**, with no live run and the triggers stopped, then re-enable the four
@@ -260,37 +260,37 @@ pre-change telemetry directory), because the resolver reads the file that is the
260
260
  ## Access control and instance isolation
261
261
 
262
262
  Three capabilities you meet once the engine produces reports other people want to see. Each is off,
263
- or single-tenant, until you configure it.
263
+ or shared by everyone, until you configure it.
264
264
 
265
- ### Tenant grants — who may see which runs
265
+ ### Organisation grants — who may see which runs
266
266
 
267
- `CLEAROTRON_ACCESS_FILE` names one JSON file: the guest list. It maps **tenants** (an organisation) to the
268
- **accounts** they may see, and users within a tenant to a subset of it. An account key is a
269
- `profileKey` — the customer bundle a run froze at start (§4) — so a grant is expressed in the same
267
+ `CLEAROTRON_ACCESS_FILE` names one JSON file: the guest list. It maps **organisations** (`tenants`) to the
268
+ **companies** (`accounts`) they may see, and people within an organisation to a subset of it. A company
269
+ key is a `profileKey` — the company bundle a run froze at start (§4) — so a grant is expressed in the same
270
270
  vocabulary as the runs it filters.
271
271
 
272
272
  **Unset means enforcement is off for the MCP faces** — whoever gets through your door sees every run.
273
273
  **It is not a valid state for the portal, which refuses to start without a guest list**; the access
274
- model is stated once, in [docs/SECURITY.md](../SECURITY.md). Setting the variable turns account scoping
274
+ model is stated once, in [docs/SECURITY.md](../SECURITY.md). Setting the variable turns company scoping
275
275
  on for **every face at once** — the portal, the MCP read face, the client face, and ops tokens. There is
276
276
  no half-enforced state and no per-surface switch to forget.
277
277
 
278
- Resolution: the signed-in email is matched against each tenant's `users` map, by exact address or by
279
- a `*@domain` wildcard; a user value of `"*"` means the tenant's whole grant; an address appearing in
280
- several tenants gets the union. An authenticated address matching nothing is granted nothing — it
281
- signs in and sees an empty world. That is deliberate. Signing in is not being enrolled.
278
+ Resolution: the signed-in email is matched against each organisation's `users` map, by exact address or
279
+ by a `*@domain` wildcard; a user value of `"*"` means the organisation's whole grant; an address
280
+ appearing in several organisations gets the union. An authenticated address matching nothing is granted
281
+ nothing — it signs in and sees an empty world. That is deliberate. Signing in is not being enrolled.
282
282
 
283
283
  **A file that is set but unreadable throws rather than failing open.** A missing path, malformed JSON,
284
284
  or a file with no `tenants` object stops the process and names the reason. Keep that in mind when you
285
285
  move the file: a guest list you configured and then broke must never resolve to "admit everybody".
286
286
 
287
- `examples/grants.example.json` is a working file over the synthetic demo customers this repo ships.
287
+ `examples/grants.example.json` is a working file over the synthetic demo companies this repo ships.
288
288
  Copy it, point `CLEAROTRON_ACCESS_FILE` at your copy, sign in as one of its addresses, and you see
289
- exactly that tenant's accounts. Then delete it and write your own — it names nobody real, which also
289
+ exactly that organisation's companies. Then delete it and write your own — it names nobody real, which also
290
290
  means it grants nothing you have.
291
291
 
292
292
  `npx clearotron start` (§6) writes an empty roster (`{"tenants": {}}`) into its state directory: your own staff
293
- address is admitted, no client is enrolled, and enforcement is already on.
293
+ address is admitted, nobody else is enrolled, and enforcement is already on.
294
294
 
295
295
  ### Ops tokens — a credential for the verbs that spend
296
296
 
@@ -308,8 +308,8 @@ node mcp-server/mint-token.mjs --scope ops --sub <principal-name> \
308
308
  tokens are then distinguishable in the log; one shared token makes them permanently indistinguishable.
309
309
  - `--verbs` is a least-privilege allowlist of write tools. A connector minted without `stop_run`
310
310
  cannot call it — the check sits at the one chokepoint every tool call passes, not in each tool.
311
- - `--accounts` caps the token to a set of account keys, so a trial integration can start demo
312
- searches and never a real customer's.
311
+ - `--accounts` caps the token to a set of company keys, so a trial integration can start demo
312
+ searches and never a real company's.
313
313
  - The token is printed once and stored nowhere. Losing it means minting another.
314
314
 
315
315
  **Revoking one.** Every mint prints a `jti`. Write that line into the file named by
@@ -338,11 +338,11 @@ That makes the environment the whole isolation boundary, so set it to make a tes
338
338
  | Axis | Variables | Why it is structural |
339
339
  |---|---|---|
340
340
  | Data plane | `CLEAROTRON_REPORTS_DIR`, `CLEAROTRON_WORK_DIR`, `CLEAROTRON_QUEUE_DIR`, `CLEAROTRON_OUTBOX_DIR` | separate directories mean a test run cannot publish into the live archive even by mistake |
341
- | Customer configs | `CLEAROTRON_CUSTOMERS_DIR`, `CLEAROTRON_INSTRUCTIONS_DIR` **unset** on the test instance | it then resolves the repo's synthetic demo customers and cannot read a real bundle |
341
+ | Company configs | `CLEAROTRON_CUSTOMERS_DIR`, `CLEAROTRON_INSTRUCTIONS_DIR` **unset** on the test instance | it then resolves the repo's synthetic demo companies and cannot read a real bundle |
342
342
  | Engine | the running engine's binary variable (`CLEAROTRON_CLAUDE_PATH` / `CLEAROTRON_CODEX_PATH`) pointed at `driver/test/mock-claude.mjs` | the mock engine needs no provider credentials, so a test instance can hold none — the strongest form of "spends nothing" |
343
343
  | Ports | give the test instance its own for the portal, MCP face and profile service | a default that collides with a live service turns a dry run into a probe of the live one, and it looks like it worked |
344
344
  | Auth | `TRADEMARK_MCP_AUTH_DISABLED=1` with `TRADEMARK_MCP_DEV=1` is **loopback-only, enforced in code** | the switch cannot be used to open a remote face |
345
345
 
346
- The strongest isolation is still two instances with their own pool and their own config store. Tenant
347
- grants above are for the case where one instance must be shared safely — demos, trials, per-user
348
- scoping inside one firm — not a substitute for this.
346
+ The strongest isolation is still two instances with their own pool and their own config store. Organisation
347
+ grants above are for the case where one instance must be shared safely — demos, trials, per-person
348
+ scoping inside one organisation — not a substitute for this.