clearotron 0.3.2-beta.12 → 0.3.2-beta.14
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.
- package/CONTRIBUTING.md +6 -5
- package/INSTALL.md +3 -4
- package/README.md +2 -1
- package/bin/clearotron.mjs +7 -1
- package/bin/example.mjs +61 -23
- package/bin/onboard.mjs +16 -2
- package/bin/start.mjs +6 -3
- package/build-info.json +2 -2
- package/docs/CLIENT-MCP.md +6 -6
- package/docs/DELIVERY.md +3 -3
- package/docs/ONBOARDING.md +1 -1
- package/docs/PORTAL.md +3 -3
- package/docs/RELEASES.md +1 -1
- package/docs/SECURITY.md +1 -1
- package/docs/architecture/04-configuration-reference.md +4 -4
- package/docs/architecture/05-config-governance.md +10 -10
- package/docs/architecture/06-operations-runbook.md +3 -3
- package/docs/architecture/07-quality-and-audit.md +1 -1
- package/docs/architecture/08-development-guide.md +2 -2
- package/docs/architecture/09-security-and-data.md +1 -1
- package/docs/decisions/0002-no-dark-functionality.md +1 -1
- package/docs/decisions/0006-what-the-public-repository-carries.md +3 -3
- package/driver/CHANGELOG.md +21 -0
- package/driver/ask-ledger.mjs +1 -1
- package/driver/band-shape.mjs +1 -1
- package/driver/bundled-demos.mjs +2 -2
- package/driver/card-budget.mjs +2 -2
- package/driver/case-law-ledger.mjs +2 -2
- package/driver/connotation-search.mjs +5 -5
- package/driver/contract-e3-backlog.mjs +13 -13
- package/driver/coverage-form.mjs +2 -2
- package/driver/demo-container.mjs +26 -2
- package/driver/disposition-tool.mjs +2 -2
- package/driver/engine/CONTRACT.md +3 -3
- package/driver/engine/mcp/codex-config.mjs +3 -11
- package/driver/engine/mcp/gather-config.mjs +1 -1
- package/driver/engine/openai-agent.mjs +2 -2
- package/driver/findings-model.mjs +21 -6
- package/driver/gateway.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +1 -1
- package/driver/pipeline.mjs +4 -4
- package/driver/placement-union.mjs +1 -1
- package/driver/portal-mcp-client.mjs +1 -1
- package/driver/portal-report.mjs +4 -1
- package/driver/portal-service.mjs +17 -5
- package/driver/predelivery-lint.mjs +9 -4
- package/driver/progress.mjs +37 -1
- package/driver/publish/render-knockout.mjs +31 -6
- package/driver/publish/render.mjs +6 -6
- package/driver/record-discard.mjs +1 -1
- package/driver/register-count.mjs +1 -1
- package/driver/register-digest-record.mjs +1 -1
- package/driver/report-card-record.mjs +2 -2
- package/driver/roster-verdict.mjs +2 -2
- package/driver/search-policy.mjs +1 -1
- package/driver/skeptic-record.mjs +1 -1
- package/driver/stages.mjs +1 -1
- package/driver/suite-census.json +40 -10
- package/driver/systemd/README.md +1 -1
- package/driver/unit-inventory.mjs +2 -2
- package/driver/verify.mjs +1 -1
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/CONNECT.md +8 -8
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/serve.mjs +27 -0
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/free-tier/src/capabilities.js +2 -2
- package/providers/jx/src/core.js +3 -3
- package/providers/jx/src/turn-envelope.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/perplexity/README.md +1 -1
- package/providers/signa/src/capabilities.js +2 -2
- package/providers/signa/src/core.js +2 -2
- package/providers/uspto-local/src/core.js +1 -1
- package/providers/uspto-local/src/sync.js +1 -1
- package/scripts/README.md +2 -6
- package/scripts/citation-anchor-report.mjs +1 -1
- package/scripts/citation-line-check.mjs +3 -3
- package/scripts/e2e.mjs +1 -1
- package/scripts/env-audit.mjs +1 -1
- package/scripts/env-classify.mjs +1 -1
- package/scripts/pack-publishable.mjs +1 -1
- package/scripts/release-artifact-seal.mjs +2 -2
- package/scripts/settings-render-check.mjs +5 -1
- package/scripts/strip-tracker-citations.mjs +4 -4
- package/scripts/test-run.mjs +46 -3
- package/shared/browser-temp-root.mjs +10 -3
- package/shared/client-door.mjs +18 -6
- package/shared/connect-clients.mjs +2 -0
- package/shared/demo-start-args.mjs +10 -0
- package/shared/identifier-scan.mjs +2 -2
- package/shared/invocation.mjs +1 -1
- package/shared/reference-guard-classes.mjs +5 -3
- package/shared/stdio-connect.mjs +80 -28
- package/shared/toml-string.mjs +25 -0
- package/shared/writing-standard-classes.mjs +2 -3
|
@@ -179,6 +179,9 @@ import { accessAudience, audienceLabel } from "../shared/access-audience.mjs";
|
|
|
179
179
|
import { resolveNumericSetting } from "./numeric-setting.mjs"; // — the same table the engine enforces, without the throw a rendering surface must not take
|
|
180
180
|
import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — one entry-point test, all spellings
|
|
181
181
|
import { deploymentBox } from "../shared/deployment-box.mjs"; // — the box names itself; one rule
|
|
182
|
+
import { stageNow } from "./progress.mjs"; // the stage a live run is in now, for the card and the row
|
|
183
|
+
// A run in one of these has finished; its step is the one its terminal write set, not the stage it last entered.
|
|
184
|
+
const TERMINAL_STATES = new Set(["delivered", "failed", "cancelled"]);
|
|
182
185
|
|
|
183
186
|
// The offering the portal menus build from — product-rows.mjs's row, unchanged.
|
|
184
187
|
//
|
|
@@ -384,7 +387,7 @@ export function scanAccountRuns({ poolRoot, workspaceRoot, account = null, gener
|
|
|
384
387
|
stageLabel: stageLabelOf(meta.searchLevel) ?? meta.stageLabel ?? null,
|
|
385
388
|
productName: productNameOf(meta.searchLevel),
|
|
386
389
|
state: "delivered", date: meta.date ?? null,
|
|
387
|
-
// WHEN IT FINISHED, to the second
|
|
390
|
+
// WHEN IT FINISHED, to the second. `date` is DAY precision and always was — it is
|
|
388
391
|
// parsed out of the run directory name. Two reads of one mark delivered on the same day
|
|
389
392
|
// therefore TIE, Array.prototype.sort is stable, and which one the parent row spoke for was
|
|
390
393
|
// decided by readdirSync order rather than by recency.
|
|
@@ -515,7 +518,11 @@ export function scanAccountRuns({ poolRoot, workspaceRoot, account = null, gener
|
|
|
515
518
|
resetsAt: typeof s.resetsAt === "string" ? s.resetsAt : null,
|
|
516
519
|
// Elapsed, so a card can say how long this has been going without inventing a finish time.
|
|
517
520
|
startedAt: typeof s.startedAt === "string" ? s.startedAt : null,
|
|
518
|
-
|
|
521
|
+
// THE STAGE IT IS IN NOW, a step back into an earlier stage included (owner, 2026-09-19): see
|
|
522
|
+
// stageNow. Only while it is live; a run that has finished keeps the step its terminal write set.
|
|
523
|
+
...(TERMINAL_STATES.has(s.state)
|
|
524
|
+
? { step: s.stepLabel ?? null, stepN: s.stepN ?? null, stepTotal: s.stepTotal ?? null }
|
|
525
|
+
: stageNow(s)),
|
|
519
526
|
// — a requested stop is a state the screen shows. Stamped by stop_run,
|
|
520
527
|
// preserved by writeRunStatus's spread-merge, replaced by the terminal when the honour check
|
|
521
528
|
// fires. The UI derives "Stopping…" from this beside a non-terminal state.
|
|
@@ -867,7 +874,7 @@ export const SERVER_ROUTE_HEADS = Object.freeze(["api", "report", "admin", "heal
|
|
|
867
874
|
* `msgId` and `conversationId` sat in this list while `jobFor` hardcoded both to null and the trigger
|
|
868
875
|
* hop stripped them off again. The totality test beside it could not see that: it asks whether every
|
|
869
876
|
* declared field is classified, which a declared-and-nulled field satisfies perfectly. So the sentence
|
|
870
|
-
* above asserted a behaviour and the test next to it checked a different one — the same shape as
|
|
877
|
+
* above asserted a behaviour and the test next to it checked a different one — the same shape as
|
|
871
878
|
* "an invitation nothing honours", one level up.
|
|
872
879
|
*
|
|
873
880
|
* `stamped` exists so the new guard can be DERIVED rather than hand-listed. It splits `carries` into the
|
|
@@ -2928,7 +2935,7 @@ async function connectorDoorKind(url) {
|
|
|
2928
2935
|
// row, and a retired-endpoint row is a different fact from a not-yours row. If an old open tab is
|
|
2929
2936
|
// still clicking Flag, that is a thing worth being able to count.
|
|
2930
2937
|
//
|
|
2931
|
-
// WHAT IS DELIBERATELY STILL BELOW THIS LINE: the whole resolver — the two-lane shape,
|
|
2938
|
+
// WHAT IS DELIBERATELY STILL BELOW THIS LINE: the whole resolver — the two-lane shape, the
|
|
2932
2939
|
// disposition read, the server-side-locator rule. The owner ruled "disable services and any code",
|
|
2933
2940
|
// and the acceptance criterion says re-enabling must be a switch and not a rebuild. Deleting 130
|
|
2934
2941
|
// lines of hard-won doctrine would have made it a rebuild.
|
|
@@ -3759,7 +3766,12 @@ function doctrineStore(now = Date.now) {
|
|
|
3759
3766
|
} catch (e) {
|
|
3760
3767
|
// NOT `{head: null}` alone. A null head with no reason reads as "no store", which is a legal and
|
|
3761
3768
|
// healthy state (`no-overlay`); this is a different thing and has to say so.
|
|
3762
|
-
|
|
3769
|
+
//
|
|
3770
|
+
// THE ERROR'S OWN WORDS GO TO THE SERVICE LOG, NOT THE WIRE. This answer is served by the health
|
|
3771
|
+
// route, and an error message can carry the store's filesystem path. `unreadable` says what the
|
|
3772
|
+
// caller needs; the operator reads why in the log.
|
|
3773
|
+
console.error(`[portal] the instructions store could not be read: ${String(e?.message ?? e).slice(0, 200)}`);
|
|
3774
|
+
value = { head: null, situation: "unreadable", outcome: "blocked" };
|
|
3763
3775
|
}
|
|
3764
3776
|
storeCache = { at: t, value };
|
|
3765
3777
|
return value;
|
|
@@ -30,7 +30,7 @@ import { partyFactSources, partyFactViolations, partyFactMessage, canJudgePartyF
|
|
|
30
30
|
import { writeUpViolations, writeUpMessage } from "./narrative-write-ups.mjs"; //
|
|
31
31
|
|
|
32
32
|
import { findRegistryArithmeticIssues, findRegistryViolations, splitBlocks } from "./registry-fidelity.mjs";
|
|
33
|
-
import { CLIENT_TIER_BY_COMPOSITE, joinFindingToBlock, parseBlockOrd, worstLiveBand, NO_RATED_CONFLICTS, deriveActionConditions, isUnconditionalProceed, verdictStance, joinAskToAnswer, projectAssessmentField, POSITION_REQUIRED_DISPOSITIONS, OFF_FIELD_GROUNDS, FINDINGS_SCHEMA_VERSION, netChainMarkers, STATEMENT_CLAUSE_MAX } from "./findings-model.mjs";
|
|
33
|
+
import { CLIENT_TIER_BY_COMPOSITE, joinFindingToBlock, parseBlockOrd, worstLiveBand, NO_RATED_CONFLICTS, deriveActionConditions, isUnconditionalProceed, verdictStance, statedConditions, joinAskToAnswer, projectAssessmentField, POSITION_REQUIRED_DISPOSITIONS, OFF_FIELD_GROUNDS, FINDINGS_SCHEMA_VERSION, netChainMarkers, STATEMENT_CLAUSE_MAX } from "./findings-model.mjs";
|
|
34
34
|
import { normalizeBand } from "./framework.mjs";
|
|
35
35
|
import { clientConditions, unrenderableConditions, ENGINE_TOKEN_RE } from "./terminal-clamp.mjs"; // the reader's clause per condition, and the token shape it may never carry
|
|
36
36
|
import { knockoutNoteView, REQUEST_NOTE_WORDS, REQUEST_SUBJECT_WORDS } from "./findings-model.mjs"; // one reader for where a note prints
|
|
@@ -87,7 +87,7 @@ const REF_ALLOWLIST = new Set([
|
|
|
87
87
|
const TERRITORY_VOCAB = new Set(Object.values(REGION_NAMES).map((n) => norm(n)));
|
|
88
88
|
|
|
89
89
|
function stripHtml(s) {
|
|
90
|
-
return String(s ?? "").replace(/<[^>]+>/g, " ").replace(/&
|
|
90
|
+
return String(s ?? "").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&");
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
// "April 2014", "MAY 2026" — dates pass the Title-Case-multi-word shape but are never entities.
|
|
@@ -2023,7 +2023,12 @@ export function statementCoherenceChecks({ verdictDoc }) {
|
|
|
2023
2023
|
stanceOk ? "" : `verdict.json stance "${verdictDoc.stance}" does not match the verdict ${v || "(missing)"} (expected "${expected ?? "?"}") — the sidecar fields diverged; re-derive it (writeVerdictSidecar composes both from one record)`);
|
|
2024
2024
|
if (!c3.pass) c3.structural = true;
|
|
2025
2025
|
out.push(c3);
|
|
2026
|
-
|
|
2026
|
+
// The form is owed only where something is stated: a conditional whose every condition was ruled to
|
|
2027
|
+
// the run record carries the band word alone (riskStatement), and a "conditional on:" there would be
|
|
2028
|
+
// the empty line the owner ruled off the page.
|
|
2029
|
+
const stated = statedConditions({ reasons: verdictDoc.reasons, clauses: verdictDoc.clauses });
|
|
2030
|
+
const nothingStated = !stated.cls.length && !stated.conds.length;
|
|
2031
|
+
const formOk = verdictDoc.stance !== "conditional" || nothingStated || /—\s*conditional on:/.test(st);
|
|
2027
2032
|
const c4 = check("statement-conditional-form", "verdict", "report", formOk,
|
|
2028
2033
|
formOk ? "" : `a conditional-stance statement must state what conditions reliance ("<Tier> — conditional on: <facts>"), but reads "${st.slice(0, 80)}" — re-derive the sidecar`);
|
|
2029
2034
|
if (!c4.pass) c4.structural = true;
|
|
@@ -2339,7 +2344,7 @@ export function runLint({ depth, commonLawGrid, matterContext, clientPartyName,
|
|
|
2339
2344
|
// ── The KNOCKOUT lane's applicable subset (2026-07-31) ──────────────────────────────────────────────
|
|
2340
2345
|
// The knockout (Stage 0/0.5) lane wrote NO predelivery-lint.json at all, by a decision recorded in
|
|
2341
2346
|
// docs/DELIVERY.md (2026-07-28). That decision rested on two premises this tranche has since moved:
|
|
2342
|
-
// the lint artifact is now the WORKBOOK's QC record
|
|
2347
|
+
// the lint artifact is now the WORKBOOK's QC record, so a lane that writes none produces an
|
|
2343
2348
|
// EMPTY QC record rather than a deliberately-absent one; and the A10 projects recorded defects onto
|
|
2344
2349
|
// the cover note that reaches the reviewing lawyer, so a knockout defect reached nobody. What has NOT
|
|
2345
2350
|
// moved is the other half of the memo: this lane's deliverable is store-rendered, and most clearance
|
package/driver/progress.mjs
CHANGED
|
@@ -97,6 +97,30 @@ export function stepForStage(rawStageKey) {
|
|
|
97
97
|
return { index, label: DISPLAY_STEPS[index], n: index + 1, total: DISPLAY_STEPS.length };
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* THE STAGE A LIVE RUN IS IN NOW, for the card and the row a person watches (owner, 2026-09-19).
|
|
102
|
+
*
|
|
103
|
+
* status.json keeps two readings. The step fields hold the furthest display step ever reached, for a
|
|
104
|
+
* stepper that never runs backwards. `lastStage` is the stage the run ENTERED last, written at the one
|
|
105
|
+
* choke point every dispatch passes, corrective re-entries included. The card named the first, so a run
|
|
106
|
+
* sent back into synthesis by a correction pass went on reading "Case law & refutation" (measured on a
|
|
107
|
+
* beta, 2026-09-18). This names the stage in hand, a step back included, with its own step number.
|
|
108
|
+
*
|
|
109
|
+
* `currentStep` FIRST, where the run wrote one: the step it is in now, in the stepper's own words, moved by
|
|
110
|
+
* every transition in either direction and left in place through a stage with no display step. A status
|
|
111
|
+
* written before that field existed falls back to mapping `lastStage`, and one with neither keeps the
|
|
112
|
+
* furthest step, which is the best reading there is. PURE.
|
|
113
|
+
*/
|
|
114
|
+
export function stageNow(status) {
|
|
115
|
+
const c = status?.currentStep;
|
|
116
|
+
if (c && typeof c.label === "string" && c.label.trim())
|
|
117
|
+
return { step: c.label, stepN: Number.isFinite(c.n) ? c.n : null, stepTotal: Number.isFinite(c.total) ? c.total : null };
|
|
118
|
+
const now = stepForStage(status?.lastStage);
|
|
119
|
+
return now
|
|
120
|
+
? { step: now.label, stepN: now.n, stepTotal: now.total }
|
|
121
|
+
: { step: status?.stepLabel ?? null, stepN: status?.stepN ?? null, stepTotal: status?.stepTotal ?? null };
|
|
122
|
+
}
|
|
123
|
+
|
|
100
124
|
// Lifecycle honesty (charter P1 §4): the status patch a TERMINAL delivered write must carry. Nothing runs
|
|
101
125
|
// after the report: delivery is a packet and publish is code, so no
|
|
102
126
|
// recordTransition ever advances the stepper past "Drafting the report" — a delivered run (with
|
|
@@ -386,6 +410,7 @@ export function seedRunStatus(ctx, { resume = false } = {}) {
|
|
|
386
410
|
// — the identity is the SHARED rule, not this stepper's. See identitySeed.
|
|
387
411
|
...identitySeed(),
|
|
388
412
|
stepIndex: first.index, stepLabel: first.label, stepN: first.n, stepTotal: first.total,
|
|
413
|
+
currentStep: currentStepOf(first),
|
|
389
414
|
lastStage: null,
|
|
390
415
|
verdict: null,
|
|
391
416
|
url: null,
|
|
@@ -407,15 +432,26 @@ export function seedRunStatus(ctx, { resume = false } = {}) {
|
|
|
407
432
|
// so for the whole of any of them every surface went on naming the PREVIOUS stage, which is the same
|
|
408
433
|
// defect as a stale step wearing a different field. The step fields are still withheld — that part of
|
|
409
434
|
// the early return was right, and an unmapped stage must never touch the displayed step.
|
|
435
|
+
//
|
|
436
|
+
// `currentStep` IS THE STEP THE RUN IS IN NOW, and it moves both ways. The step fields above keep the
|
|
437
|
+
// furthest step reached (writeRunStatus), so a corrective pass that re-enters synthesis after case law
|
|
438
|
+
// left them reading case law while `lastStage` read synthesis (measured 2026-09-18). `lastStage` is the
|
|
439
|
+
// raw key; this is the same moment in the stepper's own words, for a surface that shows where the run is.
|
|
440
|
+
// A stage with no display step leaves it where it was: those stages run inside the step already shown.
|
|
410
441
|
export function recordTransition(ctx, rawStageKey) {
|
|
411
442
|
const step = stepForStage(rawStageKey);
|
|
412
443
|
writeRunStatus(ctx, {
|
|
413
|
-
...(step ? { stepIndex: step.index, stepLabel: step.label, stepN: step.n, stepTotal: step.total } : {}),
|
|
444
|
+
...(step ? { stepIndex: step.index, stepLabel: step.label, stepN: step.n, stepTotal: step.total, currentStep: currentStepOf(step) } : {}),
|
|
414
445
|
lastStage: rawStageKey,
|
|
415
446
|
});
|
|
416
447
|
rollupStatus(ctx?.run?.studioRoot);
|
|
417
448
|
}
|
|
418
449
|
|
|
450
|
+
/** The stepper's own words for one step, as `currentStep` carries it. PURE. */
|
|
451
|
+
function currentStepOf(step) {
|
|
452
|
+
return { index: step.index, label: step.label, n: step.n, total: step.total };
|
|
453
|
+
}
|
|
454
|
+
|
|
419
455
|
// ---- STATUS.md rollup --------------------------------------------------------------------------------
|
|
420
456
|
// The archive lives UNDER studioRoot (studioRoot/archive/...), so a recursive walk of studioRoot finds
|
|
421
457
|
// both in-flight and delivered/failed runs. Cap depth (status.json only sits at the run-dir level) and
|
|
@@ -63,6 +63,7 @@ import { EXPORT_TOGGLE, exportPopover, EXPORT_MENU_JS } from './report-topbar.mj
|
|
|
63
63
|
import { makeClassifyStatus, isAllClass } from '../../providers/_shared/screen.mjs';
|
|
64
64
|
import { saysSomethingNew } from '../../shared/says-something-new.mjs';
|
|
65
65
|
import { isNextStepHeading } from '../knockout-next-step.mjs';
|
|
66
|
+
import OFFERED from '../../shared/offered-territories.json' with { type: 'json' }; // the order form's own territory names
|
|
66
67
|
|
|
67
68
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
68
69
|
|
|
@@ -481,6 +482,14 @@ function territoryName(code, { bare = false } = {}) {
|
|
|
481
482
|
} catch { return ''; }
|
|
482
483
|
}
|
|
483
484
|
|
|
485
|
+
/** A territory the order names, in the order form's own words, or '' when that list has no such code. */
|
|
486
|
+
const ORDER_FORM_NAMES = new Map([...OFFERED.regions, ...OFFERED.countries].map((t) => [t.code, t.name]));
|
|
487
|
+
const orderedTerritoryName = (code) => {
|
|
488
|
+
const c = String(code ?? '').trim().toUpperCase();
|
|
489
|
+
// EM is the European Union register's own code, and some records carry it for an EU order.
|
|
490
|
+
return ORDER_FORM_NAMES.get(c === 'EM' ? 'EU' : c) ?? '';
|
|
491
|
+
};
|
|
492
|
+
|
|
484
493
|
/** "a, b and c" — the reader's list, not a join on commas. */
|
|
485
494
|
function listWords(items) {
|
|
486
495
|
const xs = (items ?? []).filter(Boolean);
|
|
@@ -1244,12 +1253,28 @@ function aboutRequestBlock(scope, requestNotes, depthNote = '', productContext =
|
|
|
1244
1253
|
// the middle of the panel is the thing a reader skips; the count and its source are what they read.
|
|
1245
1254
|
const regions = (registerCounts?.scope?.regions ?? []).filter(Boolean);
|
|
1246
1255
|
const provider = registerCounts?.providerLabel ?? registerCounts?.provider ?? '';
|
|
1247
|
-
// THE
|
|
1248
|
-
//
|
|
1249
|
-
//
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1256
|
+
// THE ORDER'S TERRITORIES, NOT THE PROVIDER'S REGISTERS (owner, 2026-09-19). The row counted the
|
|
1257
|
+
// registers the provider searched, and for a one-country order that is not one: a United States order is
|
|
1258
|
+
// counted over the United States and the international register, because an international filing can
|
|
1259
|
+
// designate the United States. A client who ordered one country read "2 registers" as a mistake. So an
|
|
1260
|
+
// order that names its territories is stated as it named them, in the order form's own names; the
|
|
1261
|
+
// registers actually counted stay in the line under the counts table. A worldwide order keeps the board's
|
|
1262
|
+
// row, "186 registers, on Clarivate Compumark.", because there the count is the scope (the record says
|
|
1263
|
+
// worldwide by carrying no jurisdictions). A territory the provider does not cover was not counted, so it
|
|
1264
|
+
// is not listed here; it is disclosed where deferrals are. A territory the form's list cannot name, or an
|
|
1265
|
+
// office this install could not reach, falls back to the count rather than printing a code or claiming
|
|
1266
|
+
// a territory nothing was counted in.
|
|
1267
|
+
const on = provider ? `, on ${provider}` : '';
|
|
1268
|
+
const sc = registerCounts?.scope ?? {};
|
|
1269
|
+
const deferred = new Set((sc.deferredJurisdictions ?? []).map((c) => String(c ?? '').trim().toUpperCase()));
|
|
1270
|
+
const ordered = [...new Set((sc.jurisdictions ?? [])
|
|
1271
|
+
.filter((c) => !deferred.has(String(c ?? '').trim().toUpperCase())).map(orderedTerritoryName))];
|
|
1272
|
+
const namesTheOrder = sc.worldwide !== true && !(sc.unreachableOffices ?? []).length
|
|
1273
|
+
&& ordered.length > 0 && ordered.every(Boolean);
|
|
1274
|
+
const counted = namesTheOrder ? `${listWords(ordered)}${on}.`
|
|
1275
|
+
: regions.length
|
|
1276
|
+
? `${regions.length} ${regions.length === 1 ? 'register' : 'registers'}${on}.`
|
|
1277
|
+
: (registerCounts ? `Counted worldwide${on}` : '');
|
|
1253
1278
|
if (!asked && !where && !classLine && !stage && !context && !counted) return '';
|
|
1254
1279
|
// ONE PANEL, THE SAME ON BOTH REPORTS (the 2026-09-16 report redesign). The rows carry what was asked for; the
|
|
1255
1280
|
// counts row says what was counted and hides the territory list behind a fold rather than running a
|
|
@@ -90,7 +90,7 @@ let SENIOR_RIGHTS = new Map();
|
|
|
90
90
|
let DISPOSITION_MODE = false;
|
|
91
91
|
// — per-render flag: true when the record declares the v6 findings contract, which is the one that
|
|
92
92
|
// guarantees a position on every negative and a TYPED off-field ground. Gates the grouped
|
|
93
|
-
// reasoned-negative rendering; false ⇒ the pre- section, byte-identical, for every archived run.
|
|
93
|
+
// reasoned-negative rendering; false ⇒ the pre-change section, byte-identical, for every archived run.
|
|
94
94
|
let NEGATIVES_GROUPED = false;
|
|
95
95
|
// doc 50 — the run's FROZEN framework manifest (opts.framework, from _driver/framework.json). When
|
|
96
96
|
// present the report speaks ITS band words: chips/one-liners read f.band, the gauge ticks show its
|
|
@@ -1825,7 +1825,7 @@ function fullDetail(f, card, recordsByUri = new Map()) {
|
|
|
1825
1825
|
// too. It never did — the grouping's own note promises "each member is a <details> whose body is the
|
|
1826
1826
|
// same fullDetail block the compact card carries", and until now that was untrue of the one thing v6
|
|
1827
1827
|
// guarantees on every negative. Suppressing them there would take a conditional whose only job is
|
|
1828
|
-
// reproducing the pre- shape, which is the legacy code path this program forbids.
|
|
1828
|
+
// reproducing the pre-change shape, which is the legacy code path this program forbids.
|
|
1829
1829
|
//
|
|
1830
1830
|
// Absent fields ⇒ both helpers return '' ⇒ zero bytes, and the interpolation is attached to the
|
|
1831
1831
|
// template's opening so it leaves no whitespace-only line (review 2026-07-31, problem 8).
|
|
@@ -1859,7 +1859,7 @@ function fullDetail(f, card, recordsByUri = new Map()) {
|
|
|
1859
1859
|
//
|
|
1860
1860
|
// WHERE THERE IS NO PROSE THE TYPED PAIR IS STILL THE ACCOUNT, and that is most of the surfaces
|
|
1861
1861
|
// fullDetail feeds: a structured-only finding (no card file), a reasoned-negative row, a compact
|
|
1862
|
-
// card whose report-card stage produced nothing.
|
|
1862
|
+
// card whose report-card stage produced nothing. The promise that a negative's drawer carries its
|
|
1863
1863
|
// positions is untouched — a negative has no Full-detail prose to duplicate.
|
|
1864
1864
|
//
|
|
1865
1865
|
// manageableLine is NOT gated: the manageable category is a closed code-owned vocabulary the prose has
|
|
@@ -1988,7 +1988,7 @@ function manageableLine(f) {
|
|
|
1988
1988
|
//
|
|
1989
1989
|
// TRIM-TO-EMPTY IS LOAD-BEARING AND IS PRESERVED VERBATIM. `foldClause` returned '' for null, undefined,
|
|
1990
1990
|
// '' and whitespace-only input, and that empty string is what makes `clause(f.net) || card?.meta?.one ||
|
|
1991
|
-
// oneFallback(...)` fall through to exactly the pre- value on an archived run, which carries no
|
|
1991
|
+
// oneFallback(...)` fall through to exactly the pre-change value on an archived run, which carries no
|
|
1992
1992
|
// `net`. Deleting the function without keeping that behaviour would move every archived card whose
|
|
1993
1993
|
// `net` key exists but is blank. `clause` is `foldClause` with the budget arm removed and nothing else.
|
|
1994
1994
|
const clause = (value) => String(value ?? '').trim();
|
|
@@ -2314,7 +2314,7 @@ function contextNotesBlock(notes = []) {
|
|
|
2314
2314
|
}
|
|
2315
2315
|
|
|
2316
2316
|
// charter ruling 1 (2026-07-30, name-led): the masthead depth strip LEADS with the product's registry
|
|
2317
|
-
// name in bold — the same name
|
|
2317
|
+
// name in bold — the same name the read pills speak, never a rung on our ladder — then the coverage
|
|
2318
2318
|
// clauses (what this search covers / omits, from the run's frozen components via productCoverageNote).
|
|
2319
2319
|
// The seam is the LAST " — ": a registry name may itself carry one ("Preliminary clearance — register
|
|
2320
2320
|
// only") while the coverage clauses never do (they join on ";" and ","). A note with no seam at all
|
|
@@ -2323,7 +2323,7 @@ function contextNotesBlock(notes = []) {
|
|
|
2323
2323
|
/**
|
|
2324
2324
|
* THE HERO VERDICT CAPTION, folded to its first sentence.
|
|
2325
2325
|
*
|
|
2326
|
-
*
|
|
2326
|
+
* The design ruling: above any fold, only a statement, a labelled row, a count or a one-line card —
|
|
2327
2327
|
* "prose never appears until someone opens something, and once opened nothing is ever cut". The finding
|
|
2328
2328
|
* cards were moved onto that rule by `cf8dd43`; the hero caption was named in the same ruling and left
|
|
2329
2329
|
* behind, and this is the bullet that PR recorded as still owed.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// ── why this exists ──────────────────────────────────────────────────────────────────────────────────
|
|
7
7
|
//
|
|
8
|
-
// The shipped trace
|
|
8
|
+
// The shipped trace asked ONE pass to answer a question that spans two moments that never
|
|
9
9
|
// coexist. A record discarded at screening is gone by the time `findings.json` is written; the findings
|
|
10
10
|
// do not exist at the moment the record is discarded. Whichever end a single pass runs at, it guesses
|
|
11
11
|
// about the other — and `deriveRecordCarry` ran inside `register-digest`, which is BEFORE `synthesis`
|
|
@@ -447,7 +447,7 @@ export async function countRegisterHits({
|
|
|
447
447
|
counts,
|
|
448
448
|
// — which registers these figures cover, present ONLY when one was dropped. countLine renders
|
|
449
449
|
// it; the scope block below records it. Both, deliberately: `scope.deferredJurisdictions` has sat
|
|
450
|
-
// on this artifact since it was written and NOTHING reads it, which is the same shape as
|
|
450
|
+
// on this artifact since it was written and NOTHING reads it, which is the same shape as
|
|
451
451
|
// `deferred_coverage` riding the plan with no consumer and shipping a false clean. A field a
|
|
452
452
|
// reader never sees is not a disclosure, so this one lands on the rendered line first.
|
|
453
453
|
...(officeScope ? { officeScope } : {}),
|
|
@@ -366,7 +366,7 @@ export function negativeMarkCell(row) {
|
|
|
366
366
|
const MIN_JUDGED_ROWS = 1;
|
|
367
367
|
|
|
368
368
|
/** A pipe row, cells escaped so a value carrying `|` cannot open a column. */
|
|
369
|
-
const row = (cells) => `| ${cells.map((c) => str(c).replace(/\|/g, "\\|").replace(/\n+/g, " ") || "—").join(" | ")} |`;
|
|
369
|
+
const row = (cells) => `| ${cells.map((c) => str(c).replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\n+/g, " ") || "—").join(" | ")} |`;
|
|
370
370
|
const table = (columns, rows) => [row(columns), `|${columns.map(() => "---").join("|")}|`, ...rows.map(row)].join("\n");
|
|
371
371
|
|
|
372
372
|
/**
|
|
@@ -117,9 +117,9 @@ export function findingsDocFor(runDir, ordinal) {
|
|
|
117
117
|
// the record and under what number. publish/index.mjs quotes the correct shape verbatim in a comment of
|
|
118
118
|
// its own, which is what makes the divergence provable rather than a matter of taste.
|
|
119
119
|
//
|
|
120
|
-
// IT SHIPPED BECAUSE THE CONVERSION WAS NEVER CHECKED AGAINST THE ARTIFACT. asked for exactly that
|
|
120
|
+
// IT SHIPPED BECAUSE THE CONVERSION WAS NEVER CHECKED AGAINST THE ARTIFACT. The issue asked for exactly that
|
|
121
121
|
// check — "recompose the bullet for every card in demo and assert byte-equality with the
|
|
122
|
-
// delivered report.md" — and writing it is what caught this. The guard now lives beside
|
|
122
|
+
// delivered report.md" — and writing it is what caught this. The guard now lives beside the code it checks, and
|
|
123
123
|
// this is the argument for building the guard an issue asks for even when the code already looks done.
|
|
124
124
|
//
|
|
125
125
|
// THE NUMBER comes off the URI's last segment: `/mark/eu/018575624` → `018575624`. That is a parse, and
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
//
|
|
19
19
|
// no configured store
|
|
20
20
|
// → the bundled roster governs, exactly as it always did — derived by driver/bundled-demos.mjs
|
|
21
|
-
// from the directory that ships it, never a list written down. On a test box it is CORRECT
|
|
22
|
-
|
|
21
|
+
// from the directory that ships it, never a list written down. On a test box it is CORRECT;
|
|
22
|
+
// anywhere else it means CLEAROTRON_CUSTOMERS_DIR is not reaching the service.
|
|
23
23
|
//
|
|
24
24
|
// Pure: no fs, no env, no network. Every input is a parameter so a test can state the whole world.
|
|
25
25
|
|
package/driver/search-policy.mjs
CHANGED
|
@@ -152,7 +152,7 @@ export const COMPONENTS = {
|
|
|
152
152
|
// so `basis` under-reports and could not carry the distinction even if we wanted it to.)
|
|
153
153
|
//
|
|
154
154
|
// The one mechanical set that stays load-bearing is the FLOORS — a tier-`identical` floor row, live and
|
|
155
|
-
// in an instructed class, gets a full card in EVERY product. That is an obligation
|
|
155
|
+
// in an instructed class, gets a full card in EVERY product. That is an obligation, not a
|
|
156
156
|
// priority, and the ladder does not grade it.
|
|
157
157
|
//
|
|
158
158
|
// `graded` means: the stage is told what kind of report it is writing and grades ITS OWN written output.
|
|
@@ -51,7 +51,7 @@ export function escalatedAxes(flagsText, axes) {
|
|
|
51
51
|
const flags = String(flagsText ?? "");
|
|
52
52
|
if (!flags) return [];
|
|
53
53
|
return axes.filter((a) =>
|
|
54
|
-
new RegExp(`(^|\\n)\\s*[-*]?\\s*ESCALATE:\\s*${a.replace(/[
|
|
54
|
+
new RegExp(`(^|\\n)\\s*[-*]?\\s*ESCALATE:\\s*${a.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&")}\\b`, "i").test(flags));
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/** Where the call's evidence lives — the driver's own record of what the seat handed it. */
|
package/driver/stages.mjs
CHANGED
|
@@ -2154,7 +2154,7 @@ export const STAGES = {
|
|
|
2154
2154
|
const supplementalLane = !!registerPlan?.contract?.supplemental_lane;
|
|
2155
2155
|
return lines(
|
|
2156
2156
|
`First, read and follow exactly: skills/clearance-register/SKILL.md (the shared spine) then skills/clearance-register/unit.md (MODE A — UNIT). Do NOT read digest.md (digest-mode judgment a unit must never run).`,
|
|
2157
|
-
// WHAT THE KEY ALSO CARRIES — COMPOSED, NOT DOCTRINE
|
|
2157
|
+
// WHAT THE KEY ALSO CARRIES — COMPOSED, NOT DOCTRINE.
|
|
2158
2158
|
// `unit.md` used to name three tools flat, and on a deployment withholding two of them the seat was
|
|
2159
2159
|
// told it holds tools its grant does not carry. The composer derives the list from the same table
|
|
2160
2160
|
// that does the excluding, and returns NULL — not an empty sentence — when the provider cannot be
|
package/driver/suite-census.json
CHANGED
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
},
|
|
66
66
|
"a-browser-run-gets-its-own-temp-root.test.mjs": {
|
|
67
67
|
"tests": 12,
|
|
68
|
-
"asserts":
|
|
68
|
+
"asserts": 31,
|
|
69
69
|
"skips": 0,
|
|
70
70
|
"todos": 0
|
|
71
71
|
},
|
|
@@ -195,9 +195,21 @@
|
|
|
195
195
|
"skips": 0,
|
|
196
196
|
"todos": 0
|
|
197
197
|
},
|
|
198
|
+
"a-conditional-with-nothing-stated-prints-no-conditional-line.test.mjs": {
|
|
199
|
+
"tests": 2,
|
|
200
|
+
"asserts": 6,
|
|
201
|
+
"skips": 0,
|
|
202
|
+
"todos": 0
|
|
203
|
+
},
|
|
204
|
+
"a-connect-line-keeps-every-path-whole.test.mjs": {
|
|
205
|
+
"tests": 8,
|
|
206
|
+
"asserts": 20,
|
|
207
|
+
"skips": 0,
|
|
208
|
+
"todos": 0
|
|
209
|
+
},
|
|
198
210
|
"a-connect-line-says-where-it-runs.test.mjs": {
|
|
199
211
|
"tests": 11,
|
|
200
|
-
"asserts":
|
|
212
|
+
"asserts": 56,
|
|
201
213
|
"skips": 0,
|
|
202
214
|
"todos": 0
|
|
203
215
|
},
|
|
@@ -255,6 +267,12 @@
|
|
|
255
267
|
"skips": 0,
|
|
256
268
|
"todos": 0
|
|
257
269
|
},
|
|
270
|
+
"a-demo-leaves-no-temp-copies.test.mjs": {
|
|
271
|
+
"tests": 2,
|
|
272
|
+
"asserts": 6,
|
|
273
|
+
"skips": 0,
|
|
274
|
+
"todos": 0
|
|
275
|
+
},
|
|
258
276
|
"a-demo-report-declares-itself.test.mjs": {
|
|
259
277
|
"tests": 5,
|
|
260
278
|
"asserts": 15,
|
|
@@ -1528,8 +1546,8 @@
|
|
|
1528
1546
|
"todos": 0
|
|
1529
1547
|
},
|
|
1530
1548
|
"client-door.test.mjs": {
|
|
1531
|
-
"tests":
|
|
1532
|
-
"asserts":
|
|
1549
|
+
"tests": 39,
|
|
1550
|
+
"asserts": 168,
|
|
1533
1551
|
"skips": 1,
|
|
1534
1552
|
"todos": 0
|
|
1535
1553
|
},
|
|
@@ -2537,7 +2555,7 @@
|
|
|
2537
2555
|
},
|
|
2538
2556
|
"findings-model.test.mjs": {
|
|
2539
2557
|
"tests": 68,
|
|
2540
|
-
"asserts":
|
|
2558
|
+
"asserts": 258,
|
|
2541
2559
|
"skips": 0,
|
|
2542
2560
|
"todos": 0
|
|
2543
2561
|
},
|
|
@@ -3028,8 +3046,8 @@
|
|
|
3028
3046
|
"todos": 0
|
|
3029
3047
|
},
|
|
3030
3048
|
"no-test-writes-inside-the-checkout.test.mjs": {
|
|
3031
|
-
"tests":
|
|
3032
|
-
"asserts":
|
|
3049
|
+
"tests": 21,
|
|
3050
|
+
"asserts": 58,
|
|
3033
3051
|
"skips": 0,
|
|
3034
3052
|
"todos": 0
|
|
3035
3053
|
},
|
|
@@ -3364,8 +3382,8 @@
|
|
|
3364
3382
|
"todos": 0
|
|
3365
3383
|
},
|
|
3366
3384
|
"portal-service.test.mjs": {
|
|
3367
|
-
"tests":
|
|
3368
|
-
"asserts":
|
|
3385
|
+
"tests": 131,
|
|
3386
|
+
"asserts": 712,
|
|
3369
3387
|
"skips": 1,
|
|
3370
3388
|
"todos": 0
|
|
3371
3389
|
},
|
|
@@ -4857,6 +4875,12 @@
|
|
|
4857
4875
|
"skips": 0,
|
|
4858
4876
|
"todos": 0
|
|
4859
4877
|
},
|
|
4878
|
+
"the-demo-says-what-it-is-first.test.mjs": {
|
|
4879
|
+
"tests": 3,
|
|
4880
|
+
"asserts": 16,
|
|
4881
|
+
"skips": 0,
|
|
4882
|
+
"todos": 0
|
|
4883
|
+
},
|
|
4860
4884
|
"the-demo-takes-its-folder-with-it.test.mjs": {
|
|
4861
4885
|
"tests": 6,
|
|
4862
4886
|
"asserts": 12,
|
|
@@ -5039,7 +5063,7 @@
|
|
|
5039
5063
|
},
|
|
5040
5064
|
"the-knockout-page-leads-with-the-read.test.mjs": {
|
|
5041
5065
|
"tests": 39,
|
|
5042
|
-
"asserts":
|
|
5066
|
+
"asserts": 125,
|
|
5043
5067
|
"skips": 0,
|
|
5044
5068
|
"todos": 0
|
|
5045
5069
|
},
|
|
@@ -5373,6 +5397,12 @@
|
|
|
5373
5397
|
"skips": 0,
|
|
5374
5398
|
"todos": 0
|
|
5375
5399
|
},
|
|
5400
|
+
"the-status-says-which-step-the-run-is-in-now.test.mjs": {
|
|
5401
|
+
"tests": 3,
|
|
5402
|
+
"asserts": 8,
|
|
5403
|
+
"skips": 0,
|
|
5404
|
+
"todos": 0
|
|
5405
|
+
},
|
|
5376
5406
|
"the-summary-carries-structure.test.mjs": {
|
|
5377
5407
|
"tests": 14,
|
|
5378
5408
|
"asserts": 37,
|
package/driver/systemd/README.md
CHANGED
|
@@ -33,7 +33,7 @@ missing one and writes nothing, because a unit with one placeholder left starts
|
|
|
33
33
|
worse than one that does not start. Which units need it is **derived from the files**, not from a list —
|
|
34
34
|
and `driver/unit-inventory.mjs` declares them, with a test asserting the two agree in both directions.
|
|
35
35
|
|
|
36
|
-
These are for a **server, not a laptop**. On a workstation there is nothing to install: `
|
|
36
|
+
These are for a **server, not a laptop**. On a workstation there is nothing to install: `clearotron start` supervises the
|
|
37
37
|
same loop for you, and`node driver/runner.mjs --watch` is what to run by hand if you started with
|
|
38
38
|
`--no-worker` (`INSTALL.md` §5) — one foreground process that polls for queued jobs and for parked runs whose
|
|
39
39
|
window has elapsed.
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
// docs/architecture/05-config-governance.md, tier 2). Getting that wrong replaces working auth with
|
|
46
46
|
// placeholders that look configured, and it is not work to do beside a running round.
|
|
47
47
|
//
|
|
48
|
-
// So the untracked units are declared as untracked, WITH THE REASON, which is exactly what
|
|
49
|
-
//
|
|
48
|
+
// So the untracked units are declared as untracked, WITH THE REASON, which is exactly what the
|
|
49
|
+
// requirement asks for: "either has a tracked file it is compared against, or is named here with the
|
|
50
50
|
// reason it does not".
|
|
51
51
|
//
|
|
52
52
|
// ── THE CORRECTION THIS FILE NEEDED ITSELF ───────────────────────────────────────────────────────
|
package/driver/verify.mjs
CHANGED
|
@@ -1375,7 +1375,7 @@ function connotationViolations(content, queryCount, opts = {}, dispositionsPath
|
|
|
1375
1375
|
// patch — two families emitting a bare `form_damaged` would be indistinguishable there and the repair
|
|
1376
1376
|
// would rewrite the wrong form.
|
|
1377
1377
|
//
|
|
1378
|
-
// THE SHAPE IS BUILT AROUND THREE CONSUMERS, each of which fails SILENTLY on a mismatch
|
|
1378
|
+
// THE SHAPE IS BUILT AROUND THREE CONSUMERS, each of which fails SILENTLY on a mismatch:
|
|
1379
1379
|
// - The CAUSE CENSUS IS FRONT-LOADED, `no_ruling=<n>`, before any named list: repairs.mjs CENSUS_RE
|
|
1380
1380
|
// sums exactly that, pipeline.mjs shows the failing model `tok.slice(0, 160)`, and a census that fell
|
|
1381
1381
|
// off the end of a slice would read as a missing quantity — i.e. as converged.
|
package/mcp-server/CHANGELOG.md
CHANGED
package/mcp-server/CONNECT.md
CHANGED
|
@@ -5,7 +5,7 @@ MCP-capable app. Two ways in, and they are not variants of each other:
|
|
|
5
5
|
|
|
6
6
|
| | **Local** | **Hosted** |
|
|
7
7
|
|---|---|---|
|
|
8
|
-
| What it is | your app spawns `
|
|
8
|
+
| What it is | your app spawns `serve.mjs` from your own clone | you were given a URL by whoever operates the engine |
|
|
9
9
|
| Who it is for | anyone who cloned this repository | a customer or colleague reading someone else's runs |
|
|
10
10
|
| Setup | copy-paste below, no account, no credential | paste the URL, sign in |
|
|
11
11
|
| Reach | everything, including the write verbs | read-only, scoped to your own matters |
|
|
@@ -46,7 +46,7 @@ One command. Substitute your clone's path and your workspace root:
|
|
|
46
46
|
```sh
|
|
47
47
|
claude mcp add trademark-artifacts --scope user \
|
|
48
48
|
-e CLEAROTRON_WORK_DIR=/path/to/your/workspace \
|
|
49
|
-
-- node /path/to/clearotron/mcp-server/
|
|
49
|
+
-- node /path/to/clearotron/mcp-server/serve.mjs
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
Check it: `claude mcp list` prints `trademark-artifacts: … - √ Connected`.
|
|
@@ -64,7 +64,7 @@ PowerShell 5.1, write the separator as `"--"`: that shell drops a bare `--`. Rem
|
|
|
64
64
|
"mcpServers": {
|
|
65
65
|
"trademark-artifacts": {
|
|
66
66
|
"command": "node",
|
|
67
|
-
"args": ["/path/to/clearotron/mcp-server/
|
|
67
|
+
"args": ["/path/to/clearotron/mcp-server/serve.mjs"],
|
|
68
68
|
"env": { "CLEAROTRON_WORK_DIR": "/path/to/your/workspace" }
|
|
69
69
|
}
|
|
70
70
|
}
|
|
@@ -82,7 +82,7 @@ Codex reads `~/.codex/config.toml`:
|
|
|
82
82
|
```toml
|
|
83
83
|
[mcp_servers.trademark-artifacts]
|
|
84
84
|
command = "node"
|
|
85
|
-
args = ["/path/to/clearotron/mcp-server/
|
|
85
|
+
args = ["/path/to/clearotron/mcp-server/serve.mjs"]
|
|
86
86
|
env = { CLEAROTRON_WORK_DIR = "/path/to/your/workspace" }
|
|
87
87
|
```
|
|
88
88
|
|
|
@@ -94,13 +94,13 @@ is a value written to a file.
|
|
|
94
94
|
|
|
95
95
|
## Any other MCP host
|
|
96
96
|
|
|
97
|
-
The contract is the same three things every time: run `node mcp-server/
|
|
97
|
+
The contract is the same three things every time: run `node mcp-server/serve.mjs`, over stdio, with
|
|
98
98
|
`CLEAROTRON_WORK_DIR` in its environment.
|
|
99
99
|
|
|
100
100
|
```json
|
|
101
101
|
{
|
|
102
102
|
"command": "node",
|
|
103
|
-
"args": ["/path/to/clearotron/mcp-server/
|
|
103
|
+
"args": ["/path/to/clearotron/mcp-server/serve.mjs"],
|
|
104
104
|
"env": { "CLEAROTRON_WORK_DIR": "…", "CLEAROTRON_REPORTS_DIR": "…" }
|
|
105
105
|
}
|
|
106
106
|
```
|
|
@@ -125,7 +125,7 @@ read them. Spawn the server as that user:
|
|
|
125
125
|
```json
|
|
126
126
|
{
|
|
127
127
|
"command": "sudo",
|
|
128
|
-
"args": ["-u", "<operator>", "node", "/path/to/clearotron/mcp-server/
|
|
128
|
+
"args": ["-u", "<operator>", "node", "/path/to/clearotron/mcp-server/serve.mjs"]
|
|
129
129
|
}
|
|
130
130
|
```
|
|
131
131
|
|
|
@@ -133,7 +133,7 @@ That needs a one-time NOPASSWD rule, or the stdio handshake hangs on a password
|
|
|
133
133
|
|
|
134
134
|
```
|
|
135
135
|
# /etc/sudoers.d/trademark-artifacts-mcp (chmod 0440)
|
|
136
|
-
<caller> ALL=(<operator>) NOPASSWD: /usr/bin/node /path/to/clearotron/mcp-server/
|
|
136
|
+
<caller> ALL=(<operator>) NOPASSWD: /usr/bin/node /path/to/clearotron/mcp-server/serve.mjs
|
|
137
137
|
```
|
|
138
138
|
|
|
139
139
|
## What to ask it
|
package/mcp-server/lib/runs.mjs
CHANGED
|
@@ -40,7 +40,7 @@ export function unreadableRunsReason({ workSet, workRoot, workExists, poolSet })
|
|
|
40
40
|
if (workSet || workExists || poolSet) return null;
|
|
41
41
|
return `no searches can be read here: CLEAROTRON_WORK_DIR is unset and ${workRoot} does not exist, `
|
|
42
42
|
+ "and CLEAROTRON_REPORTS_DIR is unset. Set them to the install's directories — `npx clearotron doctor` "
|
|
43
|
-
+ "prints where an install keeps them.";
|
|
43
|
+
+ "prints where an install keeps them, and mcp-server/CONNECT.md says how to connect this server.";
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// Every workspace-<agent>/studio/clearance-search root under the live workspace root.
|