clearotron 0.2.3 → 0.2.4

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 (76) hide show
  1. package/.env.example +36 -37
  2. package/CONTRIBUTING.md +8 -4
  3. package/INSTALL.md +24 -4
  4. package/README.md +7 -6
  5. package/bin/example.mjs +6 -5
  6. package/bin/onboard.mjs +175 -9
  7. package/bin/start.mjs +66 -4
  8. package/build-info.json +2 -2
  9. package/demo/README.md +1 -1
  10. package/docs/GLOSSARY.md +85 -0
  11. package/docs/README.md +1 -0
  12. package/docs/architecture/01-product-overview.md +21 -9
  13. package/docs/architecture/05-config-governance.md +5 -0
  14. package/docs/decisions/0006-what-the-public-repository-carries.md +30 -5
  15. package/driver/CHANGELOG.md +34 -0
  16. package/driver/README.md +25 -6
  17. package/driver/connotation-search.mjs +1 -1
  18. package/driver/contract-audit.mjs +5 -1
  19. package/driver/contract-e3-baseline.json +11 -11
  20. package/driver/doubt-selection.mjs +1 -1
  21. package/driver/drainer-identity.mjs +1 -1
  22. package/driver/effort-model.mjs +2 -2
  23. package/driver/engine/probe.mjs +45 -5
  24. package/driver/gateway.mjs +2 -2
  25. package/driver/outbox-backoff.mjs +1 -1
  26. package/driver/package.json +1 -1
  27. package/driver/pipeline.mjs +206 -101
  28. package/driver/plain-register.mjs +16 -2
  29. package/driver/portal-config-view.mjs +30 -1
  30. package/driver/portal-local-auth.mjs +5 -1
  31. package/driver/portal-service.mjs +53 -2
  32. package/driver/predelivery-lint.mjs +54 -25
  33. package/driver/publish/render.mjs +109 -14
  34. package/driver/search-policy.mjs +1 -1
  35. package/driver/stage-context.mjs +13 -0
  36. package/driver/stages.mjs +51 -4
  37. package/driver/suite-census.json +97 -31
  38. package/driver/systemd/clearotron-worker.service +3 -3
  39. package/driver/tokens.mjs +1 -1
  40. package/driver/unit-inventory.mjs +34 -4
  41. package/mcp-server/CHANGELOG.md +2 -0
  42. package/mcp-server/package.json +1 -1
  43. package/package.json +4 -10
  44. package/portal-ui/dist/assets/{index-CEYvXdqH.js → index-KFAHMgdT.js} +73 -34
  45. package/portal-ui/dist/index.html +1 -1
  46. package/portal-ui/package.json +1 -1
  47. package/providers/oauth-mcp-bridge/CHANGELOG.md +2 -0
  48. package/providers/oauth-mcp-bridge/package.json +1 -1
  49. package/scripts/added-reference-check.mjs +45 -89
  50. package/scripts/e2e.mjs +2 -2
  51. package/scripts/generated-files-are-current.mjs +125 -0
  52. package/scripts/mint-names-in-force.mjs +4 -2
  53. package/scripts/mint-public-residue.mjs +74 -0
  54. package/scripts/mint-reference-strip-backlog.mjs +12 -1
  55. package/scripts/mint-suite-census.mjs +5 -2
  56. package/scripts/render-check.mjs +13 -2
  57. package/scripts/report-frame-check.mjs +8 -1
  58. package/scripts/report-screenshot.mjs +6 -3
  59. package/scripts/revisit-render-check.mjs +6 -3
  60. package/scripts/score.mjs +1 -1
  61. package/scripts/strip-tracker-citations.mjs +9 -1
  62. package/scripts/test-full.mjs +226 -0
  63. package/shared/README.md +2 -1
  64. package/shared/browser-temp-root.mjs +142 -0
  65. package/shared/checkout-move.mjs +1 -1
  66. package/shared/client-door.mjs +4 -3
  67. package/shared/identifier-scan.mjs +1 -1
  68. package/shared/install-auth.mjs +35 -0
  69. package/shared/invocation.mjs +5 -1
  70. package/shared/listen.mjs +55 -3
  71. package/shared/names-in-force.mjs +4 -1
  72. package/shared/os-advice.mjs +91 -0
  73. package/shared/reap-on-exit.mjs +42 -0
  74. package/shared/reference-guard-classes.mjs +351 -0
  75. package/shared/suite-census.mjs +31 -3
  76. package/shared/withheld-paths-access.mjs +36 -18
package/driver/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # clearotron-driver — deterministic driver for the prelim-search trademark workflow
1
+ # clearotron driver — deterministic orchestration for the trademark clearance workflow
2
2
 
3
3
  Orchestration in **code**; the LLM does only judgment leaves. Each pipeline stage is **one blocking
4
4
  engine turn** (default engine: `anthropic-agent`, shelling `claude -p` per stage); fan-out/fan-in/
@@ -18,8 +18,7 @@ the gather MCP servers live.
18
18
  ```
19
19
  integrator ──job JSON (enqueue CLI / start_run / queue file)──┐
20
20
 
21
- systemd .path (watches every queue) ──▶ prelim-driver.service
22
- systemd .timer (~90s fallback re-drain) ─▶ (same oneshot)
21
+ clearotron-worker.service (the built-in worker, watches every queue)
23
22
  └─ node runner.mjs ─▶ for each queue: claim ─▶ pipeline(job, {agent}) ─▶ runStage() ─▶
24
23
  engine turn (`claude -p`, blocking) ─▶ leaf turn
25
24
  ```
@@ -42,8 +41,28 @@ identity ran it. Headless deployments with no per-agent workspaces use one expli
42
41
  delivered report, run failure, intake rejection, duplicate skip, late-bind ack — is a
43
42
  self-contained packet in the outbox (`outbox.mjs`). Integrators consume events over the ops MCP
44
43
  (`list_outbox_events` → `get_delivery_packet` → send → `mark_sent`/`ack_event`) and never touch the
45
- driver's filesystem. `deliver-trigger.sh` + `systemd/prelim-outbox.*` are the reference wake-up for
46
- an agent-based integrator.
44
+ driver's filesystem. `deliver-trigger.sh` is the reference wake-up for an
45
+ agent-based integrator. The outbox units beside it are retired — see the units row below.
46
+
47
+ ## What a file name tells you
48
+
49
+ Eleven suffixes carry a meaning across the whole directory. They are a naming convention rather than
50
+ a framework — nothing enforces them — but they are consistent enough that the suffix tells you what
51
+ kind of thing a module is before you open it.
52
+
53
+ | suffix | what the module is |
54
+ |---|---|
55
+ | `-model` | a pure parser or shape: text or JSON in, a validated structure out, no I/O |
56
+ | `-record` | a recording transport — the typed call a stage makes and the row it writes |
57
+ | `-form` | a driver-written form a stage fills in, with its own validation |
58
+ | `-io` | the reader and writer for a form's file, kept apart from the form's shape |
59
+ | `-union` | the accumulator that merges many writes into one form |
60
+ | `-call` | the model-call half of a pair: what the stage is asked to send |
61
+ | `-tool` | the driver-write half of the same pair: what the driver does with what came back |
62
+ | `-duty` | an obligation check — was something the run owed actually done |
63
+ | `-gate` | a refusal: a condition that stops a run or a stage rather than reporting on it |
64
+ | `-ledger` | an append-only account of what happened, read back for coverage and audit |
65
+ | `-verdict` | a pure function from evidence to a stated answer, with its reason |
47
66
 
48
67
  ## Files
49
68
 
@@ -63,7 +82,7 @@ identity ran it. Headless deployments with no per-agent workspaces use one expli
63
82
  | `coverage-ledger.mjs` | Machine coverage-ledger contract: strict JSON-mirror parser (token-first throws), prose parser, `REGISTER_AXES`/`decideAxes`. |
64
83
  | `enqueue-schema.mjs` | Job-file shape + `validateJob`. |
65
84
  | `dev-portal.mjs` | Loopback-only dev-instance UI (static pool + `/profiles/*` proxy) for dry-run testing — see `../docs/E2E.md` Tier 1b. |
66
- | `systemd/*` | Reference units: `prelim-driver.path` (watches the queues) + `prelim-driver.timer` (fallback re-drain) `prelim-driver.service` (oneshot drain); `prelim-outbox.*` (outbox wake). |
85
+ | `systemd/*` | `clearotron-worker.service` is the live worker and what a deployment runs. The path/timer/oneshot drain units and the outbox-wake units beside them are RETIRED: the built-in worker replaced that posture. Their files stay until the production rebuild — `unit-inventory.mjs` records the ruling and the reason, and reports a retired unit's absence as expected rather than as a fault. They keep their original names because a deployed box still holds them under those names. |
67
86
 
68
87
  ## Key engine facts
69
88
 
@@ -769,7 +769,7 @@ export function segmentBinding({ segment_index, fragment } = {}, candidate) {
769
769
  //
770
770
  // SO THE MEASUREMENT SURVIVES THE ENFORCEMENT. A fragment that arrives is still weighed and still
771
771
  // matched, and the verdict rides out on `fragmentState` for the receipts histogram — which is how the
772
- // transcription-quality signal scruffy used to settle this stays measurable after the duty that
772
+ // transcription-quality signal testing used to settle this stays measurable after the duty that
773
773
  // produced it is gone. Never charge for it: an unbound fragment is a fact about the model, and the
774
774
  // seat's ruling is not worse for it.
775
775
  const frag = String(fragment ?? "").trim();
@@ -276,7 +276,11 @@ export const E3_BACKLOG_KINDS = [...E3_KINDS, "other"];
276
276
 
277
277
  const E3_PATTERNS = [
278
278
  { kind: "literal-json-skeleton", re: /```json|\{\s*"[a-z_]+"\s*:/i },
279
- { kind: "exactly-these-keys", re: /EXACTLY (?:these|one of|the)|keys EXACTLY|EXACTLY \{|closed enum|key set is closed|additionalProperties/i },
279
+ // `EXACTLY the` USED TO STAND ALONE, and it is ordinary English: "puts its noise on exactly the
280
+ // report that matters most" is prose about a report, not a dictated key set, and the pattern is
281
+ // case-insensitive so the lower-case form matched too. The alternative now names what may follow it,
282
+ // which is the only form that dictates a set. Everything else in this pattern is byte-identical.
283
+ { kind: "exactly-these-keys", re: /EXACTLY (?:these|one of)|EXACTLY the (?:following|keys|fields)|keys EXACTLY|EXACTLY \{|closed enum|key set is closed|additionalProperties/i },
280
284
  // A quoted or backticked template carrying a <placeholder>, close to an instruction to emit it. The
281
285
  // placeholder class is deliberately loose (`<comma-separated DOMAINS>`, `<N>`, `<one record URI>`) —
282
286
  // the tell is a shape dictated for a parser to read back, not the casing inside the angle brackets.
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "common-law": {
19
19
  "literal-json-skeleton": 0,
20
- "exactly-these-keys": 3,
20
+ "exactly-these-keys": 2,
21
21
  "dictated-line-shape": 0
22
22
  },
23
23
  "common-law-half": {
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "synthesis": {
54
54
  "literal-json-skeleton": 10,
55
- "exactly-these-keys": 9,
55
+ "exactly-these-keys": 8,
56
56
  "dictated-line-shape": 5
57
57
  },
58
58
  "case-law": {
@@ -114,7 +114,7 @@
114
114
  },
115
115
  "skills/prelim-common-law/SKILL.md": {
116
116
  "literal-json-skeleton": 0,
117
- "exactly-these-keys": 2,
117
+ "exactly-these-keys": 1,
118
118
  "dictated-line-shape": 2
119
119
  },
120
120
  "skills/prelim-register/SKILL.md": {
@@ -129,7 +129,7 @@
129
129
  },
130
130
  "skills/placement-inquiry/SKILL.md": {
131
131
  "literal-json-skeleton": 1,
132
- "exactly-these-keys": 4,
132
+ "exactly-these-keys": 3,
133
133
  "dictated-line-shape": 1
134
134
  },
135
135
  "skills/prelim-register/digest.md": {
@@ -139,7 +139,7 @@
139
139
  },
140
140
  "skills/prelim-search/phase2-execution.md": {
141
141
  "literal-json-skeleton": 0,
142
- "exactly-these-keys": 2,
142
+ "exactly-these-keys": 0,
143
143
  "dictated-line-shape": 3
144
144
  },
145
145
  "skills/frame-diff/SKILL.md": {
@@ -149,12 +149,12 @@
149
149
  },
150
150
  "skills/prelim-search/synthesis-rules.md": {
151
151
  "literal-json-skeleton": 1,
152
- "exactly-these-keys": 7,
152
+ "exactly-these-keys": 1,
153
153
  "dictated-line-shape": 2
154
154
  },
155
155
  "skills/prelim-search/firm-wide-reasoning.md": {
156
156
  "literal-json-skeleton": 0,
157
- "exactly-these-keys": 2,
157
+ "exactly-these-keys": 0,
158
158
  "dictated-line-shape": 0
159
159
  },
160
160
  "skills/prelim-search/risk-framework.md": {
@@ -164,7 +164,7 @@
164
164
  },
165
165
  "skills/prelim-search/worked-examples.md": {
166
166
  "literal-json-skeleton": 0,
167
- "exactly-these-keys": 1,
167
+ "exactly-these-keys": 0,
168
168
  "dictated-line-shape": 1
169
169
  },
170
170
  "skills/prelim-search/report-prose.md": {
@@ -179,7 +179,7 @@
179
179
  },
180
180
  "skills/narrative-refutation/SKILL.md": {
181
181
  "literal-json-skeleton": 0,
182
- "exactly-these-keys": 3,
182
+ "exactly-these-keys": 2,
183
183
  "dictated-line-shape": 1
184
184
  },
185
185
  "skills/prelim-search/delivery-contract.md": {
@@ -189,7 +189,7 @@
189
189
  }
190
190
  },
191
191
  "totals": {
192
- "stages": 47,
193
- "files": 78
192
+ "stages": 45,
193
+ "files": 64
194
194
  }
195
195
  }
@@ -100,7 +100,7 @@ export function doubtsForClosure({ doubts = null, doubtClosure = null } = {}) {
100
100
  // The event exists to answer whether a cut is WORTH giving a value, before any row has one: if most
101
101
  // doubts carry no tier the cut saves nothing. A count written only when the cut is live needs the cut
102
102
  // to exist in order to measure whether the cut should exist — the circularity pipeline.mjs's own
103
- // comment says this event prevents. Found by role-e2e eggie on the R2 artifacts.
103
+ // comment says this event prevents. Found in review on the R2 artifacts.
104
104
  //
105
105
  // `keepSet` null means "keep everything": nothing is ever dropped, `ids` is discarded below, and the
106
106
  // dispatch stays byte-identical. Only the COUNTS change.
@@ -136,7 +136,7 @@ export function drainerVerdict({ stamp, headCommit, isAlive, processes, ppidOf =
136
136
  : "";
137
137
 
138
138
  if (!alive) {
139
- // ── THE ONE BRANCH THE POSTURE DECIDES (tracker issue 206, ruled by overwatch 2026-09-06).
139
+ // ── THE ONE BRANCH THE POSTURE DECIDES (decided 2026-09-06).
140
140
  //
141
141
  // The same empty answer means two opposite things. Where a worker holds the queue open, a gone
142
142
  // drainer is the outage: nothing is executing runs and nothing will start one. Where the box drains
@@ -218,7 +218,7 @@ export const costBand = (i) => Math.max(1, Math.min(5, Math.ceil(effortUnits(i)
218
218
  // range is the thing this section was rebuilt to remove; do not reintroduce one without a new ruling.
219
219
 
220
220
  /**
221
- * THE QUOTED BOUNDS. ONE SOURCE — owner ruling, 2026-08-23, relayed by overwatch.
221
+ * THE QUOTED BOUNDS. ONE SOURCE — owner ruling, 2026-08-23.
222
222
  *
223
223
  * WHAT THIS REPLACED, and why a constant beat a model. The old quote was a base plus one adder per lane:
224
224
  * 1.5h, +0.5 for case law, +0.5 for a native-language lane, +0.5 for a single territory. It missed the
@@ -264,7 +264,7 @@ export const quoteBoundsFor = (l) => TURNAROUND_QUOTE[deriveMode(l) === "knockou
264
264
  /**
265
265
  * The bounds for THIS job. The ruled range, and NOTHING is done to it.
266
266
  *
267
- * — OWNER RULING, 2026-08-26, relayed by overwatch: "No compute. We just say 1.5–2.5 hours for
267
+ * — OWNER RULING, 2026-08-26: "No compute. We just say 1.5–2.5 hours for
268
268
  * big reports, period. Keep it simple." The question put to him was which run-slot cap the quote should
269
269
  * divide by, and he removed the division instead of answering it.
270
270
  *
@@ -123,7 +123,13 @@ export function classifyProbe({ engine, tuple = null, error = null, timeoutSec =
123
123
  if (!tuple) return v("failed", "none", `${id} returned nothing`, "The adapter settled no tuple — this is a driver bug, not a configuration one.");
124
124
  const s = tuple.signals ?? {};
125
125
  const text = `${tuple.stderr ?? ""}\n${tuple.stdout ?? ""}`;
126
- const detail = tail(tuple.stderr);
126
+ // READ FROM THE SAME STREAMS THE CLASSIFICATION IS. Every branch below matches over `text`, which is
127
+ // stderr AND stdout, while this carried stderr alone — so a CLI that reports its failure on stdout
128
+ // produced a verdict with its evidence dropped, and the reader got a headline with nothing under it.
129
+ // stderr stays FIRST because that is where a diagnostic belongs and where these CLIs put theirs; the
130
+ // fallback exists so that "the engine said nothing" is a claim about the engine rather than about
131
+ // which pipe this happened to look at.
132
+ const detail = tail(tuple.stderr) ?? tail(tuple.stdout);
127
133
 
128
134
  if (tuple.code === 0) return { ok: true, engine: id, mode: "ok", basis: "completed-turn", headline: `${id} completed a turn`, fix: null, detail: null };
129
135
 
@@ -249,13 +255,47 @@ export function probeWeatherWarning(verdict) {
249
255
  * whatever the operator's shell happens to have set and report a pass for the wrong engine — the same
250
256
  * frozen-provider trap `preflightCandidate` works around one file over.
251
257
  *
252
- * ONLY the engine-selection keys are applied. Credentials and billing-mode variables are deliberately
253
- * NOT copied: the probe must bill exactly the way a run on this box would, and moving a spend variable
254
- * to make a probe pass is the one thing this must never do.
258
+ * THE SPEND VARIABLES ARE APPLIED TOO, and the rule this replaces had it backwards. It said they were
259
+ * deliberately not copied, so that "the probe must bill exactly the way a run on this machine would"
260
+ * the aim is right and withholding them defeated it. The caller supplies the lane and the credential
261
+ * precisely because they are not in the environment yet; not copying them left the probe billing the
262
+ * way the operator's shell happened to be set, while the run that followed billed the way the reader
263
+ * had just chosen. The probe proved a lane nobody was going to use. Measured: an API-key install failed
264
+ * as "not signed in" because the adapter removes the key under any mode that is not the API-key one,
265
+ * and the mode it read was the unset one.
266
+ *
267
+ * WHAT IS STILL NEVER DONE, which is the sentence that was worth keeping: nothing here defaults,
268
+ * repairs, invents or falls back. It copies what the caller passed and nothing else, so a probe cannot
269
+ * pass on a credential a run would not have — the caller is the wizard about to write these very values
270
+ * to disk, or a command reading the values already there.
271
+ *
272
+ * AND IT PUTS EVERYTHING BACK. Widening what this writes onto the process widens what it must remove
273
+ * again; a credential outliving the check that borrowed it would be a worse defect than the one this
274
+ * fixed, and a check drives the restore rather than trusting it.
255
275
  */
256
276
  function applyEngineEnv(env) {
257
277
  if (!env || env === process.env) return () => {};
258
- const keys = ["CLEAROTRON_AI", ...Object.values(ENGINE_BINARIES).map((s) => s.env)];
278
+ // EVERY VARIABLE THAT DECIDES WHAT THE SPAWN IS, not only the ones that decide WHICH BINARY.
279
+ //
280
+ // This list used to carry CLEAROTRON_AI and the binary paths. It did not carry the billing mode or
281
+ // the API key — so a caller could hand this function a correct api-key environment, watch
282
+ // `resolveAuthMode` accept it, and then have `runTurn` read `process.env`, where neither had arrived.
283
+ // `anthropic-agent.spawnEnv` deletes ANTHROPIC_API_KEY under any mode that is not api-key, and the
284
+ // mode it read was unset, so it deleted the key the reader had just typed. The turn then failed as
285
+ // "not signed in" — correctly, about an environment nobody had asked for.
286
+ //
287
+ // Reported from a real WSL2 install, 2026-09-09: the setup wizard's api-key lane, a valid key, and a
288
+ // sign-in failure the reader could only get past by exporting the key into their own shell.
289
+ //
290
+ // A SET, AND THE DUPLICATE IS NOT HYPOTHETICAL. Both engines name the same billing variable, so this
291
+ // list contains CLEAROTRON_AI_BILLING twice — and the save below is `saved.set(k, process.env[k])`
292
+ // inside the same loop that writes. On the second visit it saved the value the FIRST visit had just
293
+ // written, so the restore put that back instead of deleting it, and the mode leaked into the rest of
294
+ // the process. Caught by the arm that exists to prove the restore, which is the only reason widening
295
+ // this list was safe to do at all.
296
+ const keys = [...new Set(["CLEAROTRON_AI", ...Object.values(ENGINE_BINARIES)
297
+ .flatMap((s) => [s.env, s.authEnv, s.apiKeyEnv, s.headless?.tokenEnv])
298
+ .filter(Boolean)])];
259
299
  const saved = new Map();
260
300
  for (const k of keys) {
261
301
  saved.set(k, process.env[k]);
@@ -909,8 +909,8 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
909
909
  // until the wall. ONE free rung per ladder is the concession; a second zero-usage warm turn in the
910
910
  // same ladder is charged, and the record says so rather than leaving the difference to be inferred.
911
911
  //
912
- // PROVISIONAL, and deliberately one line to change: the number below is a routed parameter (2026-08-22,
913
- // overwatch), not a measured optimum. Nobody has yet counted how often a second free rung would have
912
+ // PROVISIONAL, and deliberately one line to change: the number below is a routed parameter (2026-08-22),
913
+ // not a measured optimum. Nobody has yet counted how often a second free rung would have
914
914
  // converted, and until someone does, 1 is the smallest concession that closes the defect.
915
915
  //
916
916
  // THE CHARGING BRANCH CANNOT FIRE TODAY, and that is stated rather than left to be discovered. `warm`
@@ -274,7 +274,7 @@ export function settleProgress(agent, now = Date.now()) {
274
274
  // disposed of on the box.
275
275
  //
276
276
  // **Owner ruling, 2026-08-24, SUPERSEDING THIS SWEEP'S HALF OF IT: failed runs' notification packets get
277
- // the same re-drop cover as delivered ones.** (Relayed by role-overwatch.)
277
+ // the same re-drop cover as delivered ones.**
278
278
  //
279
279
  // BOTH ARE KEPT BECAUSE BOTH ARE STILL TRUE, and reading them as a reversal is the mistake to avoid. A
280
280
  // failed run owes the CLIENT no report — that is 2026-08-22, and `scripts/e2e.mjs`'s delivery assertion
@@ -2,7 +2,7 @@
2
2
  "name": "clearotron-driver",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.2.3",
5
+ "version": "0.2.4",
6
6
  "license": "AGPL-3.0-only",
7
7
  "description": "Deterministic driver for the trademark clearance workflow: orchestration in code (fan-out, fan-in barrier, gating, retries); the model does judgment leaves only, through a reasoning CLI spawned per stage.",
8
8
  "engines": {