clearotron 0.4.0 → 0.4.1-beta.1
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/THIRD-PARTY-NOTICES.md +2 -2
- package/bin/README.md +1 -1
- package/bin/clearotron.mjs +2 -0
- package/bin/mcp.mjs +80 -0
- package/bin/start.mjs +14 -1
- package/build-info.json +2 -2
- package/demo/README.md +1 -1
- package/docs/SECURITY-OWASP.md +21 -13
- package/docs/SECURITY.md +20 -14
- package/docs/architecture/04-configuration-reference.md +3 -3
- package/docs/architecture/05-config-governance.md +1 -1
- package/docs/architecture/06-operations-runbook.md +5 -4
- package/docs/architecture/08-development-guide.md +3 -1
- package/docs/architecture/09-security-and-data.md +16 -11
- package/driver/CHANGELOG.md +29 -0
- package/driver/binding-layers.mjs +28 -6
- package/driver/citation-census.json +3 -3
- package/driver/common-law-coverage-status.mjs +52 -0
- package/driver/common-law-receipts.mjs +1 -1
- package/driver/compare.mjs +1 -1
- package/driver/connotation-search.mjs +5 -5
- package/driver/contract-audit.mjs +6 -6
- package/driver/contract-dictation-registry.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +18 -17
- package/driver/contract-vocabulary.mjs +145 -132
- package/driver/declination-call.mjs +5 -1
- package/driver/degraded-parts.mjs +10 -3
- package/driver/dev-portal.mjs +2 -1
- package/driver/dispatch-record.mjs +19 -0
- package/driver/doubt-closure-tool.mjs +2 -2
- package/driver/driver.config.mjs +39 -6
- package/driver/engagement-receipt.mjs +1 -1
- package/driver/engine/CONTRACT.md +41 -1
- package/driver/engine/anthropic-agent.mjs +56 -4
- package/driver/engine/mcp/codex-config.mjs +8 -3
- package/driver/engine/mcp/coverage-server.mjs +1 -1
- package/driver/engine/mcp/gather-config.mjs +1 -1
- package/driver/engine/mcp/recording-server.mjs +32 -5
- package/driver/engine/mcp/stdio-server.mjs +45 -7
- package/driver/engine/mcp/supplemental.mjs +26 -1
- package/driver/engine/openai-agent.mjs +113 -20
- package/driver/engine/session-record.mjs +223 -0
- package/driver/enqueue-schema.mjs +2 -2
- package/driver/findings-model.mjs +4 -4
- package/driver/flag-snapshot.mjs +2 -1
- package/driver/form-neighbourhood.mjs +7 -7
- package/driver/frame-diff-model.mjs +3 -3
- package/driver/framework.mjs +22 -0
- package/driver/gateway.mjs +96 -14
- package/driver/jx.mjs +1 -1
- package/driver/knockout-review-record.mjs +55 -1
- package/driver/named-band.mjs +4 -0
- package/driver/owner-screen.mjs +7 -4
- package/driver/package.json +1 -1
- package/driver/phase0.mjs +13 -2
- package/driver/pipeline-knockout.mjs +15 -13
- package/driver/pipeline.mjs +132 -48
- package/driver/portal-families.mjs +3 -3
- package/driver/portal-service.mjs +6 -5
- package/driver/predelivery-lint.mjs +19 -19
- package/driver/products.mjs +1 -1
- package/driver/profiles.mjs +4 -3
- package/driver/publish/index.mjs +1 -1
- package/driver/publish/knockout.mjs +5 -4
- package/driver/publish/office-record-links.mjs +17 -2
- package/driver/publish/publish-inputs.mjs +3 -3
- package/driver/publish/render-knockout.mjs +17 -5
- package/driver/publish/render.mjs +4 -2
- package/driver/publish/xlsx.mjs +1 -1
- package/driver/record-fold.mjs +177 -0
- package/driver/register-count.mjs +20 -3
- package/driver/register-coverage.mjs +1 -1
- package/driver/register-plan.mjs +83 -18
- package/driver/register-records.mjs +7 -0
- package/driver/registry-fidelity.mjs +2 -2
- package/driver/repairs.mjs +1 -1
- package/driver/report-card-record.mjs +4 -4
- package/driver/result-noun-fields.mjs +3 -3
- package/driver/scope-facts.mjs +11 -3
- package/driver/scope-rules.mjs +2 -2
- package/driver/search-policy.mjs +20 -1
- package/driver/skills/clearance-common-law/SKILL.md +22 -22
- package/driver/skills/clearance-register/digest.md +3 -3
- package/driver/skills/clearance-register/register-recipes.md +11 -11
- package/driver/skills/clearance-register/status-rules.md +1 -1
- package/driver/skills/clearance-register/unit.md +1 -1
- package/driver/skills/clearance-search/firm-wide-reasoning.md +3 -2
- package/driver/skills/clearance-search/phase2-execution.md +6 -5
- package/driver/skills/clearance-search/synthesis-rules.md +4 -4
- package/driver/skills/clearance-search/worked-examples.md +39 -39
- package/driver/skills/clearance-variants/SKILL.md +16 -16
- package/driver/skills/frame-diff/SKILL.md +2 -2
- package/driver/stage-context.mjs +36 -1
- package/driver/stages-knockout.mjs +6 -5
- package/driver/stages.mjs +30 -30
- package/driver/suite-census.json +259 -55
- package/driver/synthesis-record.mjs +5 -0
- package/driver/unit-inventory.mjs +17 -28
- package/driver/verify.mjs +7 -7
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/CONNECT.md +9 -3
- package/mcp-server/demo-mode.mjs +10 -0
- package/mcp-server/lib/evidence.mjs +8 -8
- package/mcp-server/lib/knockout.mjs +1 -1
- package/mcp-server/lib/runs.mjs +7 -1
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/package.json +2 -2
- package/mcp-server/packs/README.md +1 -1
- package/mcp-server/server.mjs +16 -2
- package/package.json +4 -3
- package/portal-ui/package.json +3 -3
- package/providers/_shared/count.mjs +9 -2
- package/providers/_shared/enumerate.mjs +125 -41
- package/providers/_shared/execute-plan.mjs +15 -4
- package/providers/corsearch/src/capabilities.js +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +2 -2
- package/providers/signa/src/capabilities.js +32 -2
- package/providers/signa/src/core.js +262 -36
- package/scripts/band-shape-probe.mjs +2 -2
- package/scripts/deprecate-below.mjs +28 -4
- package/scripts/e2e-first-time.mjs +17 -2
- package/scripts/env-audit.mjs +3 -2
- package/scripts/freeze-example-run.mjs +10 -10
- package/scripts/mcp-registry-listing.mjs +199 -0
- package/scripts/mint-plugin-folder.mjs +65 -0
- package/scripts/release-version.mjs +44 -0
- package/scripts/score.mjs +1 -1
- package/shared/register-floor.mjs +19 -0
- package/shared/scope.mjs +18 -2
- package/shared/trigger-lane.mjs +1 -1
- package/skills/clearotron-ops/SKILL.md +81 -24
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
4
|
+
//
|
|
5
|
+
// mcp-registry-listing.mjs — the MCP Registry lists each stable release npm serves, and nothing else.
|
|
6
|
+
//
|
|
7
|
+
// node scripts/mcp-registry-listing.mjs plan the stables npm serves that the registry does not list
|
|
8
|
+
// node scripts/mcp-registry-listing.mjs publish <publisher> list each of them, oldest first, and read each back
|
|
9
|
+
//
|
|
10
|
+
// The registry (registry.modelcontextprotocol.io) is where MCP clients look a server up. It holds metadata
|
|
11
|
+
// only, about a package npm already serves, and a version's metadata can never change once it is listed.
|
|
12
|
+
// So nothing here reacts to a release: it asks npm which stables it serves and the registry which it
|
|
13
|
+
// lists, and lists what is missing. A run that fails is asked again by the next one, and no release ever
|
|
14
|
+
// waits for this.
|
|
15
|
+
//
|
|
16
|
+
// ONE SOURCE SETS BOTH VERSIONS. Every field that names a version comes from npm's manifest for that exact
|
|
17
|
+
// version, and so does the name: `mcpName` there is the listing's name. The registry reads `mcpName` from
|
|
18
|
+
// npm in turn and refuses a listing that disagrees, so the two cannot drift.
|
|
19
|
+
//
|
|
20
|
+
// A STABLE WITHOUT `mcpName`. The stables published before the field existed are passed by. One published
|
|
21
|
+
// after the first stable that carries it is refused: something stripped it, and listing past it would hide
|
|
22
|
+
// that.
|
|
23
|
+
//
|
|
24
|
+
// A SOURCE THAT CANNOT BE READ IS EXIT 2, never "nothing to list" and never "already listed". The
|
|
25
|
+
// registry's 404 for this exact name is an answer: nothing is listed under it yet.
|
|
26
|
+
//
|
|
27
|
+
// The registry marks the highest version latest whatever order versions arrive in (`registry_service.go`
|
|
28
|
+
// at v1.8.1). Missing stables are listed oldest first all the same, so its record reads in release order.
|
|
29
|
+
//
|
|
30
|
+
// Exit 0: nothing to list, or each one listed and read back.
|
|
31
|
+
// Exit 1: a stable after the first carrying `mcpName` lacks it, or a listing failed or did not read back.
|
|
32
|
+
// Exit 2: could not look.
|
|
33
|
+
import { spawnSync } from "node:child_process";
|
|
34
|
+
import { appendFileSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
35
|
+
import { tmpdir } from "node:os";
|
|
36
|
+
import { join } from "node:path";
|
|
37
|
+
import { isEntrypoint } from "../shared/is-entrypoint.mjs";
|
|
38
|
+
|
|
39
|
+
export const SERVER_NAME = "io.github.CordilleraSarl/clearotron";
|
|
40
|
+
/** The README banner's alt text, a shipped line. The registry allows 100 characters. */
|
|
41
|
+
export const DESCRIPTION = "Clearotron — trademark clearance that shows its work";
|
|
42
|
+
export const SCHEMA = "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json";
|
|
43
|
+
export const REPOSITORY = Object.freeze({ url: "https://github.com/CordilleraSarl/clearotron", source: "github" });
|
|
44
|
+
export const NPM_URL = "https://registry.npmjs.org/clearotron";
|
|
45
|
+
export const LISTED_URL = `https://registry.modelcontextprotocol.io/v0.1/servers/${encodeURIComponent(SERVER_NAME)}/versions`;
|
|
46
|
+
/** The registry's refusal of a version it already holds (`ErrInvalidVersion`, v1.8.1): already listed. */
|
|
47
|
+
const DUPLICATE = /cannot publish duplicate version/;
|
|
48
|
+
|
|
49
|
+
export class CouldNotLook extends Error {}
|
|
50
|
+
|
|
51
|
+
const STABLE = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
52
|
+
export const isStable = (v) => STABLE.test(v);
|
|
53
|
+
const parts = (v) => v.match(STABLE).slice(1).map(Number);
|
|
54
|
+
export function compareStable(a, b) {
|
|
55
|
+
const [x, y] = [parts(a), parts(b)];
|
|
56
|
+
return x[0] - y[0] || x[1] - y[1] || x[2] - y[2];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The listing for one stable npm serves, every field that can come from its manifest taken from it. PURE. */
|
|
60
|
+
export function serverJsonFor(manifest) {
|
|
61
|
+
if (manifest?.mcpName !== SERVER_NAME) {
|
|
62
|
+
throw new Error(`${manifest?.name}@${manifest?.version} carries mcpName ${JSON.stringify(manifest?.mcpName ?? null)}, not ${SERVER_NAME}`);
|
|
63
|
+
}
|
|
64
|
+
if (!isStable(manifest.version)) throw new Error(`${manifest.version} is not a stable version`);
|
|
65
|
+
return {
|
|
66
|
+
$schema: SCHEMA,
|
|
67
|
+
name: manifest.mcpName,
|
|
68
|
+
description: DESCRIPTION,
|
|
69
|
+
repository: { ...REPOSITORY },
|
|
70
|
+
version: manifest.version,
|
|
71
|
+
packages: [{
|
|
72
|
+
registryType: "npm",
|
|
73
|
+
identifier: manifest.name,
|
|
74
|
+
version: manifest.version,
|
|
75
|
+
transport: { type: "stdio" },
|
|
76
|
+
packageArguments: [{ type: "positional", value: "mcp" }],
|
|
77
|
+
}],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Which stables to list. PURE.
|
|
83
|
+
* @param {Record<string, object>} served npm's manifests, by version
|
|
84
|
+
* @param {Set<string>} listed the versions the registry lists under the name
|
|
85
|
+
* @returns {{ list: object[], passedBy: string[], refused: string[] }} `list` oldest first
|
|
86
|
+
*/
|
|
87
|
+
export function plan(served, listed) {
|
|
88
|
+
const stables = Object.keys(served).filter(isStable).sort(compareStable);
|
|
89
|
+
const first = stables.findIndex((v) => served[v]?.mcpName === SERVER_NAME);
|
|
90
|
+
if (first === -1) return { list: [], passedBy: stables, refused: [] };
|
|
91
|
+
const list = [];
|
|
92
|
+
const refused = [];
|
|
93
|
+
for (const v of stables.slice(first)) {
|
|
94
|
+
if (served[v]?.mcpName !== SERVER_NAME) refused.push(v);
|
|
95
|
+
else if (!listed.has(v)) list.push(served[v]);
|
|
96
|
+
}
|
|
97
|
+
return { list, passedBy: stables.slice(0, first), refused };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function readJson(url, fetchImpl, who) {
|
|
101
|
+
let res;
|
|
102
|
+
try {
|
|
103
|
+
res = await fetchImpl(url, { headers: { accept: "application/json" } });
|
|
104
|
+
} catch (e) {
|
|
105
|
+
throw new CouldNotLook(`${who} did not answer: ${e.message}`);
|
|
106
|
+
}
|
|
107
|
+
if (res.status === 404) return null;
|
|
108
|
+
if (res.status !== 200) throw new CouldNotLook(`${who} answered ${res.status}`);
|
|
109
|
+
try {
|
|
110
|
+
return await res.json();
|
|
111
|
+
} catch {
|
|
112
|
+
throw new CouldNotLook(`${who} answered with something that is not JSON`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** npm's manifests for every version it serves. */
|
|
117
|
+
export async function readServed(fetchImpl = fetch) {
|
|
118
|
+
const body = await readJson(NPM_URL, fetchImpl, "npm");
|
|
119
|
+
if (!body?.versions || typeof body.versions !== "object") throw new CouldNotLook("npm's answer names no versions");
|
|
120
|
+
return body.versions;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The versions the registry lists under the exact name: empty when it has never listed it. */
|
|
124
|
+
export async function readListed(fetchImpl = fetch) {
|
|
125
|
+
const body = await readJson(LISTED_URL, fetchImpl, "the MCP Registry");
|
|
126
|
+
if (body === null) return new Set();
|
|
127
|
+
if (!Array.isArray(body?.servers)) throw new CouldNotLook("the MCP Registry's answer lists no servers");
|
|
128
|
+
if (body.metadata?.nextCursor) throw new CouldNotLook("the MCP Registry's answer is paged, and this reads one page");
|
|
129
|
+
const listed = new Set();
|
|
130
|
+
for (const s of body.servers) {
|
|
131
|
+
if (s?.server?.name !== SERVER_NAME) throw new CouldNotLook(`the MCP Registry answered for ${JSON.stringify(s?.server?.name)}`);
|
|
132
|
+
listed.add(s.server.version);
|
|
133
|
+
}
|
|
134
|
+
return listed;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Hand one listing to the publisher. A refusal as a duplicate means it is already there. */
|
|
138
|
+
export function publishOne(manifest, publisher, run = spawnSync) {
|
|
139
|
+
const dir = mkdtempSync(join(tmpdir(), "mcp-registry-"));
|
|
140
|
+
try {
|
|
141
|
+
const file = join(dir, "server.json");
|
|
142
|
+
writeFileSync(file, JSON.stringify(serverJsonFor(manifest), null, 2) + "\n");
|
|
143
|
+
const r = run(publisher, ["publish", file], { encoding: "utf8" });
|
|
144
|
+
const said = `${r.stdout ?? ""}${r.stderr ?? ""}`.trim();
|
|
145
|
+
if (r.status === 0) return { ok: true, said };
|
|
146
|
+
if (DUPLICATE.test(said)) return { ok: true, duplicate: true, said };
|
|
147
|
+
return { ok: false, said: said || String(r.error?.message ?? `exit ${r.status}`) };
|
|
148
|
+
} finally {
|
|
149
|
+
rmSync(dir, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function main(argv, { fetchImpl = fetch, run = spawnSync, output = process.env.GITHUB_OUTPUT, log = console.log, warn = console.error } = {}) {
|
|
154
|
+
const [cmd, publisher] = argv;
|
|
155
|
+
if (!(cmd === "plan" || (cmd === "publish" && publisher))) {
|
|
156
|
+
warn("usage: mcp-registry-listing.mjs plan | publish <publisher>");
|
|
157
|
+
return 2;
|
|
158
|
+
}
|
|
159
|
+
let p;
|
|
160
|
+
try {
|
|
161
|
+
p = plan(await readServed(fetchImpl), await readListed(fetchImpl));
|
|
162
|
+
} catch (e) {
|
|
163
|
+
if (!(e instanceof CouldNotLook)) throw e;
|
|
164
|
+
warn(`mcp-registry: could not look: ${e.message}`);
|
|
165
|
+
return 2;
|
|
166
|
+
}
|
|
167
|
+
if (p.passedBy.length) log(`mcp-registry: passed by, published before mcpName: ${p.passedBy.join(", ")}`);
|
|
168
|
+
if (p.refused.length) {
|
|
169
|
+
warn(`mcp-registry: REFUSED. These stables came after the first that carries mcpName and do not carry it: ${p.refused.join(", ")}`);
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
log(p.list.length ? `mcp-registry: to list, oldest first: ${p.list.map((m) => m.version).join(", ")}` : "mcp-registry: nothing to list");
|
|
173
|
+
if (output) appendFileSync(output, `count=${p.list.length}\n`);
|
|
174
|
+
if (cmd === "plan") return 0;
|
|
175
|
+
|
|
176
|
+
for (const manifest of p.list) {
|
|
177
|
+
const r = publishOne(manifest, publisher, run);
|
|
178
|
+
if (!r.ok) {
|
|
179
|
+
warn(`mcp-registry: ${manifest.version} was not listed: ${r.said}`);
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
let listed;
|
|
183
|
+
try {
|
|
184
|
+
listed = await readListed(fetchImpl);
|
|
185
|
+
} catch (e) {
|
|
186
|
+
if (!(e instanceof CouldNotLook)) throw e;
|
|
187
|
+
warn(`mcp-registry: could not read ${manifest.version} back: ${e.message}`);
|
|
188
|
+
return 2;
|
|
189
|
+
}
|
|
190
|
+
if (!listed.has(manifest.version)) {
|
|
191
|
+
warn(`mcp-registry: the publisher ${r.duplicate ? "called" : "reported"} ${manifest.version} ${r.duplicate ? "a duplicate" : "listed"}, and the registry does not list it`);
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
194
|
+
log(`mcp-registry: ${manifest.version} ${r.duplicate ? "was already listed" : "listed"}, and read back`);
|
|
195
|
+
}
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (isEntrypoint(import.meta.url)) process.exitCode = await main(process.argv.slice(2));
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
4
|
+
//
|
|
5
|
+
// mint-plugin-folder.mjs — the folder Claude's plugin directory installs, its copies derived from the tree.
|
|
6
|
+
//
|
|
7
|
+
// The directory installs one folder, never the repository: a person who installs the plugin gets
|
|
8
|
+
// `plugin/` and nothing else. What the folder needs from the tree is copied into it, and `--check`
|
|
9
|
+
// refuses a folder whose copies have drifted from their sources:
|
|
10
|
+
// - the three connector skills, byte for byte from `skills/`, which stays the one copy the server
|
|
11
|
+
// briefs from and the published package ships;
|
|
12
|
+
// - the licence and its additional terms, byte for byte from the root.
|
|
13
|
+
// Written by hand and not derived: `plugin/README.md`, the manifest, `plugin/scripts/npx.mjs`, the
|
|
14
|
+
// launcher that starts npx on every platform, and `plugin/.mcp.json`, which names the exact stable
|
|
15
|
+
// version the server is started from. A stable cut moves that version and the manifest's together;
|
|
16
|
+
// between stables the package is a beta and the folder still names the last stable.
|
|
17
|
+
//
|
|
18
|
+
// node scripts/mint-plugin-folder.mjs write the copies
|
|
19
|
+
// node scripts/mint-plugin-folder.mjs --check exit 1 if any copy differs from its source, naming it
|
|
20
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { dirname, join, relative } from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { isEntrypoint } from "../shared/is-entrypoint.mjs";
|
|
24
|
+
|
|
25
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
26
|
+
export const PLUGIN_DIR = "plugin";
|
|
27
|
+
export const CONNECTOR_SKILLS = Object.freeze(["clearotron-client", "clearotron-account", "clearotron-ops"]);
|
|
28
|
+
|
|
29
|
+
/** Every copied file of the folder: its path under the root, and its exact bytes. PURE but for reads. */
|
|
30
|
+
export function pluginCopies(root = ROOT) {
|
|
31
|
+
const read = (p) => readFileSync(join(root, p), "utf8");
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const skill of CONNECTOR_SKILLS) {
|
|
34
|
+
for (const f of readdirSync(join(root, "skills", skill)).sort()) {
|
|
35
|
+
out.push({ path: join(PLUGIN_DIR, "skills", skill, f), text: read(join("skills", skill, f)) });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
out.push({ path: join(PLUGIN_DIR, "LICENSE"), text: read("LICENSE") });
|
|
39
|
+
out.push({ path: join(PLUGIN_DIR, "ADDITIONAL-TERMS.md"), text: read("ADDITIONAL-TERMS.md") });
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function main(argv) {
|
|
44
|
+
const check = argv.includes("--check");
|
|
45
|
+
const stale = [];
|
|
46
|
+
for (const { path, text } of pluginCopies()) {
|
|
47
|
+
const at = join(ROOT, path);
|
|
48
|
+
const now = existsSync(at) ? readFileSync(at, "utf8") : null;
|
|
49
|
+
if (now === text) continue;
|
|
50
|
+
if (check) { stale.push(path); continue; }
|
|
51
|
+
mkdirSync(dirname(at), { recursive: true });
|
|
52
|
+
writeFileSync(at, text);
|
|
53
|
+
console.log(`plugin-folder: wrote ${relative(ROOT, at)}`);
|
|
54
|
+
}
|
|
55
|
+
if (check && stale.length) {
|
|
56
|
+
console.error(`plugin-folder: STALE. ${stale.length} copied file(s) differ from their sources:`);
|
|
57
|
+
for (const p of stale) console.error(` ${p}`);
|
|
58
|
+
console.error("Re-mint: node scripts/mint-plugin-folder.mjs");
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
if (check) console.log("plugin-folder: current");
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (isEntrypoint(import.meta.url)) process.exit(main(process.argv.slice(2)));
|
|
@@ -42,6 +42,35 @@ export function alreadyOut({ version, tags = [], published = false }) {
|
|
|
42
42
|
return where;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// ── THE PLUGIN FOLDER FOLLOWS A STABLE, AND ONLY A STABLE ─────────────────────────────────────────────
|
|
46
|
+
//
|
|
47
|
+
// `plugin/` is what Claude's plugin directory installs, and it starts the server from npm at one exact
|
|
48
|
+
// version: the one `"clearotron@X.Y.Z"` argument in its `.mcp.json`, beside the same number in its
|
|
49
|
+
// manifest. A stable cut moves both in the version commit. A beta moves neither, so between stables the
|
|
50
|
+
// folder names the last stable. The pin is found by its prefix, never by its place in the argument list,
|
|
51
|
+
// and a file without exactly one match refuses the cut before anything is stamped.
|
|
52
|
+
export const PLUGIN_SERVER = join("plugin", ".mcp.json");
|
|
53
|
+
export const PLUGIN_MANIFEST = join("plugin", ".claude-plugin", "plugin.json");
|
|
54
|
+
const PIN = /"clearotron@[^"]*"/g;
|
|
55
|
+
const MANIFEST_VERSION = /^ {2}"version": "[^"]*"/gm;
|
|
56
|
+
export const isStableVersion = (v) => /^\d+\.\d+\.\d+$/.test(v);
|
|
57
|
+
|
|
58
|
+
/** PURE. Both plugin files at `version`, or why they cannot be moved. Nothing else in either changes. */
|
|
59
|
+
export function pluginAtVersion({ server, manifest }, version) {
|
|
60
|
+
const pins = server.match(PIN) ?? [];
|
|
61
|
+
if (pins.length !== 1) return { refused: `${PLUGIN_SERVER} carries ${pins.length} "clearotron@" arguments, not one` };
|
|
62
|
+
const versions = manifest.match(MANIFEST_VERSION) ?? [];
|
|
63
|
+
if (versions.length !== 1) return { refused: `${PLUGIN_MANIFEST} carries ${versions.length} top-level versions, not one` };
|
|
64
|
+
const next = {
|
|
65
|
+
server: server.replace(PIN, `"clearotron@${version}"`),
|
|
66
|
+
manifest: manifest.replace(MANIFEST_VERSION, ` "version": "${version}"`),
|
|
67
|
+
};
|
|
68
|
+
for (const [file, text] of [[PLUGIN_SERVER, next.server], [PLUGIN_MANIFEST, next.manifest]]) {
|
|
69
|
+
try { JSON.parse(text); } catch (e) { return { refused: `${file} is not JSON after the move: ${e.message}` }; }
|
|
70
|
+
}
|
|
71
|
+
return next;
|
|
72
|
+
}
|
|
73
|
+
|
|
45
74
|
// ── THE CHANNEL DECIDES THE MODE, AND IT HAS TO BE DECIDED HERE ───────────────────────────────────
|
|
46
75
|
//
|
|
47
76
|
// `changeset version` computes a pre-release number or a stable one depending on whether the tree is in
|
|
@@ -343,12 +372,27 @@ function main() {
|
|
|
343
372
|
process.exitCode = 1;
|
|
344
373
|
return;
|
|
345
374
|
}
|
|
375
|
+
let plugin = null;
|
|
376
|
+
if (isStableVersion(version)) {
|
|
377
|
+
const read = (p) => (existsSync(join(ROOT, p)) ? readFileSync(join(ROOT, p), "utf8") : "");
|
|
378
|
+
plugin = pluginAtVersion({ server: read(PLUGIN_SERVER), manifest: read(PLUGIN_MANIFEST) }, version);
|
|
379
|
+
if (plugin.refused) {
|
|
380
|
+
console.error(`release-version: the plugin folder cannot follow ${version}: ${plugin.refused}. Refusing before anything is stamped.`);
|
|
381
|
+
process.exitCode = 1;
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
346
385
|
const pkgPath = join(ROOT, "package.json");
|
|
347
386
|
const pkg = readJson(pkgPath);
|
|
348
387
|
const was = pkg.version;
|
|
349
388
|
pkg.version = version;
|
|
350
389
|
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
351
390
|
console.log(`release-version: clearotron ${was} -> ${version} (carried from the fixed group)`);
|
|
391
|
+
if (plugin) {
|
|
392
|
+
writeFileSync(join(ROOT, PLUGIN_SERVER), plugin.server);
|
|
393
|
+
writeFileSync(join(ROOT, PLUGIN_MANIFEST), plugin.manifest);
|
|
394
|
+
console.log(`release-version: the plugin folder starts clearotron@${version}, and its manifest says ${version}`);
|
|
395
|
+
}
|
|
352
396
|
|
|
353
397
|
// AND THE LOCKFILE, WHICH NOTHING ELSE IN THIS CUT TOUCHES. `changeset version` rewrites the
|
|
354
398
|
// workspace manifests and the block above rewrites the root's; `package-lock.json` records the same
|
package/scripts/score.mjs
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
// ── which directory to point it at ───────────────────────────────────────────────────────────────────
|
|
34
34
|
//
|
|
35
35
|
// The agent WORKSPACE archive dir, never the published pool dir. The pool keeps report.md and
|
|
36
|
-
// findings.json but not `_driver/` or `_records/` — see
|
|
36
|
+
// findings.json but not `_driver/` or `_records/` — see doRepublish() in pool-admin.mjs. Without
|
|
37
37
|
// `_driver/` there is no band and no reconciliation, so `withheld` cannot be computed at all, and a
|
|
38
38
|
// scorer that silently reported every withheld mark as `lost` would send every fix to the wrong place.
|
|
39
39
|
// So a run dir with no `_driver/` says so and declines to guess.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
// register-floor.mjs — a register's floor, read one way and printed one way, on every path that meets it.
|
|
4
|
+
//
|
|
5
|
+
// A register asked something very broad can answer "at least 10,000" instead of a count: it counted, and
|
|
6
|
+
// gave a floor. That is an answer, never a failure and never a zero (ruled 2026-10-02). The knockout count
|
|
7
|
+
// has printed it as "more than 10,000" since it first met one; the clearance path reads and prints it here
|
|
8
|
+
// with the same words, so the two can never say it two ways.
|
|
9
|
+
|
|
10
|
+
/** A floor as every page prints it: the register's own figure, and no sentence around it. */
|
|
11
|
+
export const moreThan = (floor) => `more than ${floor.toLocaleString("en-US")}`;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The register's floor on a parsed answer that gave one instead of a count, or null. A floor counts only
|
|
15
|
+
* with the register's flag AND the number: a flag with no number says no more than "unknown" does. PURE.
|
|
16
|
+
*/
|
|
17
|
+
export function floorOf(parsed) {
|
|
18
|
+
return parsed?.total_approximate === true && Number.isFinite(parsed?.total_floor) ? parsed.total_floor : null;
|
|
19
|
+
}
|
package/shared/scope.mjs
CHANGED
|
@@ -792,6 +792,12 @@ function tenantStamp(scope, asked) {
|
|
|
792
792
|
throw new Error(`name the organisation whose Generic this is (tenant) — your access covers ${orgs.length}`);
|
|
793
793
|
}
|
|
794
794
|
|
|
795
|
+
// The install's organisation when the grants file holds exactly one; null with several, none, or no file.
|
|
796
|
+
function soleOrganisation() {
|
|
797
|
+
const orgs = Object.keys(loadGrants()?.tenants ?? {});
|
|
798
|
+
return orgs.length === 1 ? orgs[0] : null;
|
|
799
|
+
}
|
|
800
|
+
|
|
795
801
|
// True iff `email`'s domain (the part after the final '@') is one of firmDomains. PURE (no jose), so the HTTP
|
|
796
802
|
// handler can derive firm-staff-ness from the already-verified CF identity without pulling the auth lib. The
|
|
797
803
|
// `internal` (read-all) grant rests on THIS positive check — NOT on which CF app admitted the caller — so a
|
|
@@ -1016,8 +1022,18 @@ export function authorize(scope, toolName, args = {}) {
|
|
|
1016
1022
|
// paid run whose delivery packet routes nowhere. A refusal naming the missing field is the cheaper
|
|
1017
1023
|
// failure by far, and it is not the "accepted then quietly narrower" shape: the preview describes a
|
|
1018
1024
|
// job stamped from the verified identity, and start_run says plainly which field to add.
|
|
1019
|
-
|
|
1020
|
-
|
|
1025
|
+
//
|
|
1026
|
+
// WHICH ORGANISATION'S GENERIC. An ops token names no person, so there is nobody's reach to read the
|
|
1027
|
+
// organisation from — and passed through bare, every Generic run a connector started was filed under
|
|
1028
|
+
// no organisation, where the Generic filter on Clearances can never find it. On an install that holds
|
|
1029
|
+
// exactly one organisation there is nothing to guess, so the job takes it: the same answer the
|
|
1030
|
+
// person-scoped branch gives a person who sees that one organisation. A named tenant passes as it
|
|
1031
|
+
// stands, and with several organisations (or none) the job is left as it was.
|
|
1032
|
+
const placed = (toolName === "start_run" || toolName === "plan_run") && (args?.profileKey ?? "generic") === "generic"
|
|
1033
|
+
&& !(typeof args?.tenant === "string" && args.tenant.trim()) ? soleOrganisation() : null;
|
|
1034
|
+
const routed = placed ? { ...args, tenant: placed } : args;
|
|
1035
|
+
if (toolName === "plan_run") return stampForwarder(scope, routed);
|
|
1036
|
+
return routed;
|
|
1021
1037
|
}
|
|
1022
1038
|
// kind === "account": a signed-in CLIENT, across their OWN account(s). Reads the client layer on any of
|
|
1023
1039
|
// their runs, and drives the run lifecycle for them. Run OWNERSHIP is not checked here — it is enforced
|
package/shared/trigger-lane.mjs
CHANGED
|
@@ -228,7 +228,7 @@ export function triggerLaneVerdict({ url = null, hasToken = false, verbs = null,
|
|
|
228
228
|
// false the moment the door finishes binding, which is seconds later.
|
|
229
229
|
//
|
|
230
230
|
// It is also false as a prediction, and that is the worse half. The submit path makes a FRESH upstream
|
|
231
|
-
// call per request inside its own try/catch (`
|
|
231
|
+
// call per request inside its own try/catch (the `trigger` call inside makePortalService() in portal-service.mjs) and never consults
|
|
232
232
|
// this verdict. So nothing about a boot-time refusal determines what a clearance does.
|
|
233
233
|
//
|
|
234
234
|
// IT COST A DIAGNOSTIC DETOUR during the 0.1.6 production upgrade, and it prints on every reboot. The
|
|
@@ -10,28 +10,75 @@ You are an integrator agent driving a trademark clearance engine over MCP. Intak
|
|
|
10
10
|
call hands the matter to a deterministic pipeline**, and delivery comes back to you as outbox
|
|
11
11
|
events (see `COURIER.md`). You never compose reports and never touch the engine's files.
|
|
12
12
|
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
A search costs real money and takes real time
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
13
|
+
## When a request arrives: confirm it, and start at once
|
|
14
|
+
|
|
15
|
+
A search costs real money and takes real time. Read the request for everything the search needs, send
|
|
16
|
+
the requester one confirmation, and start the search at once; they correct it afterwards. Never wait for
|
|
17
|
+
a yes.
|
|
18
|
+
|
|
19
|
+
**1. Is it a new matter?** The matter is the mark, not the thread. A forwarded reply quotes the original
|
|
20
|
+
request beneath it, which makes a follow-up look like a fresh request, so check `list_runs` for the mark
|
|
21
|
+
(in flight and recently delivered) and your own sent confirmation for the thread first. A bare "proceed"
|
|
22
|
+
or a thank-you never starts a second search: reply once that the search is already running, and stop. A
|
|
23
|
+
different mark in the same thread is a distinct matter and its own search. A changed instruction while
|
|
24
|
+
a run is in flight goes in through `feed_context`. Only the requester's explicit "run it again" starts a
|
|
25
|
+
second search of the same mark, with `dupOverride: true`.
|
|
26
|
+
|
|
27
|
+
**2. Read the brief out of the request.** Ask only for what is both impossible to infer and material.
|
|
28
|
+
- The mark or marks, and what kind each is: a word, a logo, a slogan.
|
|
29
|
+
- The account and the applicant (`profileKey`, `projectKey` and `customer` below). Say the applicant
|
|
30
|
+
together with what it implies: the applicant's own and affiliated marks are not reported as
|
|
31
|
+
conflicts. Never guess the applicant from the mark itself.
|
|
32
|
+
- Prior matters on the same reference or project: ask the engine first (`search_runs`, `list_runs`),
|
|
33
|
+
and say what you found.
|
|
34
|
+
- Goods or classes. Either one suffices: infer the other side, and say that you inferred it
|
|
35
|
+
("sparkling water → class 32, inferred").
|
|
36
|
+
- The deadline and every operational instruction (recipients, billing, the reference), each said back
|
|
37
|
+
with its status: done, will do, or cannot do. Never pass over an instruction in silence.
|
|
38
|
+
- The deliverable as specified (a template, a risk framework, a format), verbatim.
|
|
39
|
+
- How free the client is to change the name, which sets the advice. If the request does not say, state
|
|
40
|
+
the posture you assumed.
|
|
41
|
+
- Prior and intended use. Unless the request says otherwise, a house root is assumed in use and the new
|
|
42
|
+
element unused.
|
|
43
|
+
- Special instructions, verbatim.
|
|
44
|
+
|
|
45
|
+
The reference number and the industry are shown, never asked for.
|
|
46
|
+
|
|
47
|
+
**3. Check it for free with `plan_run`.** It takes exactly the same arguments as `start_run`, spends
|
|
48
|
+
nothing and holds nothing. It reports which search resolves and where that came from (the request, the
|
|
49
|
+
account's default, or a saved search), the territories and marketplaces that would actually be searched,
|
|
50
|
+
the turnaround, and any blockers, as questions to put back. It is a check, not a gate. Put what it
|
|
51
|
+
reports in the confirmation, above all a search or a scope narrower than the words the requester used.
|
|
52
|
+
|
|
53
|
+
**4. Send the confirmation in three groups, in this order.**
|
|
54
|
+
- **Corrections and commitments:** a correction to the mark, the deadline diarised, the recipients, the
|
|
55
|
+
billing, and anything instructed that you will do or cannot do.
|
|
56
|
+
- **Assumed — correct me:** every inferred value with what it does (the applicant and the marks set
|
|
57
|
+
aside with it, inferred classes, the territories, the use posture), and every default applied. A
|
|
58
|
+
default fills silence only; it never narrows an explicit instruction, and a stated "worldwide" stays
|
|
59
|
+
worldwide.
|
|
60
|
+
- **Given:** what the request stated, said back briefly.
|
|
61
|
+
|
|
62
|
+
End with **"Starting now on this understanding — reply to correct anything."**, and call `start_run`
|
|
63
|
+
with the same arguments at once.
|
|
64
|
+
|
|
65
|
+
**5. What holds a search back.** Only two things: a mark that cannot be made out (absent, or with no
|
|
66
|
+
clearly likely reading), and goods and classes both absent. Ask for the missing item, and start when the
|
|
67
|
+
answer arrives. Nothing else holds a search:
|
|
68
|
+
- A mark with one obvious mistyping is searched on the corrected reading, with the correction first in
|
|
69
|
+
the confirmation ("the form wrote NOVAPLUSE; searched as NOVAPULSE — say so if that is wrong").
|
|
70
|
+
- A missing reference never holds a search.
|
|
71
|
+
- An unknown applicant never holds one. Lead the second group with the question, set
|
|
72
|
+
`customerUnknown: true`, and start at once:
|
|
73
|
+
|
|
74
|
+
> Who is the applicant? I've started the search — it doesn't depend on the answer — but until the
|
|
75
|
+
> applicant is named I can't separate their own marks from third-party conflicts: an identical or
|
|
76
|
+
> near-identical hit is reported as an ordinary conflict, carrying a note to disregard it if it's the
|
|
77
|
+
> applicant's own prior filing. Reply as early as you can — I'll fold it in on a best-effort basis
|
|
78
|
+
> while the search is still running.
|
|
79
|
+
|
|
80
|
+
The answer goes in through `feed_context`. Never promise that an answer arriving any time before
|
|
81
|
+
delivery will be folded in: a late answer is best effort.
|
|
35
82
|
|
|
36
83
|
## Starting a search — `start_run`
|
|
37
84
|
|
|
@@ -77,7 +124,14 @@ Extract from the request, faithfully (verbatim beats paraphrase):
|
|
|
77
124
|
storefront that matters to them. These are ADDED to the account's own; you cannot remove theirs.
|
|
78
125
|
Every one widens the grid, so pass only what was actually asked for.
|
|
79
126
|
- **`ref`** (matter reference), **`deadline`** (ISO 8601 — drives deadline arithmetic), **`msgId`**
|
|
80
|
-
(your channel's native message id — the delivery packet threads the reply on it)
|
|
127
|
+
(your channel's native message id — the delivery packet threads the reply on it), and
|
|
128
|
+
**`conversationId`** (your channel's thread key, which the engine's duplicate check reads).
|
|
129
|
+
- **`projectKey`** — the engagement under `profileKey`, from that customer's `projects` in
|
|
130
|
+
`list_profiles`, when the request names one or its list makes one unambiguous. Omit it otherwise.
|
|
131
|
+
- **`customerUnknown: true`** — when the applicant is neither stated nor implied by who sent the request.
|
|
132
|
+
- **`brief`** (the confirmation exactly as you sent it), **`rawRequest`** (the whole request, verbatim),
|
|
133
|
+
**`deliverableSpec`**, **`commercialFlexibility`** and **`priorUse`**, as plain text. Omit any that are
|
|
134
|
+
empty.
|
|
81
135
|
- **`dupOverride: true`** — ONLY when the requester has explicitly confirmed a re-run of a matter
|
|
82
136
|
the dedup gate parked. Never set it on your own initiative; a duplicate submission otherwise gets
|
|
83
137
|
a polite duplicate-skipped notice, not a second spend.
|
|
@@ -87,7 +141,10 @@ The call returns the accepted job (or a validation refusal telling you exactly w
|
|
|
87
141
|
|
|
88
142
|
## While a run is in flight
|
|
89
143
|
|
|
90
|
-
- **`get_run` / `list_runs`** — state, current step, verdict when reached.
|
|
144
|
+
- **`get_run` / `list_runs`** — state, current step, verdict when reached. Answer a requester's "how is
|
|
145
|
+
it going?" from what these return. An engine you cannot reach and a search that has made no progress
|
|
146
|
+
are different facts: say "I could not reach the engine" for the first and "no progress since <time>"
|
|
147
|
+
for the second, and never report one as the other.
|
|
91
148
|
- **`feed_context`** — fold in a mid-run answer (typically the applicant binding). One call; the
|
|
92
149
|
engine acknowledges through the outbox.
|
|
93
150
|
- **`decision_timeline`** — how the verdict evolved, when someone asks "why conditional?".
|