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
|
@@ -69,7 +69,14 @@ const readJsonl = (p) => {
|
|
|
69
69
|
} catch { return []; }
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* The stage's own last attempt row, or null when the stage wrote none.
|
|
74
|
+
*
|
|
75
|
+
* A PART IS DEGRADED WHEN ITS STAGE FAILED, and that is the row's `fail`, never its `ok`. The two said the
|
|
76
|
+
* same thing until the attempt row learned to record a session that reached an error result and was
|
|
77
|
+
* recovered (engine/session-record.mjs): such an attempt is not ok, and its stage still delivered. Whether
|
|
78
|
+
* that should reach a client's report is a ruling nobody has made, so this reads what it always meant.
|
|
79
|
+
*/
|
|
73
80
|
export function lastAttempt(runDir, stage) {
|
|
74
81
|
const rows = readJsonl(driverDir(runDir, `${stage}.jsonl`))
|
|
75
82
|
.filter((r) => Number.isFinite(Number(r?.attempt)) && typeof r.ok === "boolean");
|
|
@@ -93,7 +100,7 @@ function courtDecisions(runDir, log) {
|
|
|
93
100
|
if (lastEvent(log, ["case-law-decision"])?.run !== true) return null;
|
|
94
101
|
const last = lastAttempt(runDir, "case-law");
|
|
95
102
|
const part = { part: "court-decisions", name: PART_NAMES.courtDecisions };
|
|
96
|
-
if (last?.
|
|
103
|
+
if (last?.fail) return { ...part, reason: reasonFor(last.fail), cause: `the case-law stage ended failed: ${last.fail}` };
|
|
97
104
|
let text;
|
|
98
105
|
try { text = readFileSync(join(runDir, "case-law-findings.md"), "utf8"); } catch { text = null; }
|
|
99
106
|
if (text === null) return { ...part, reason: NOT_COMPLETED, cause: "case-law-findings.md is absent" };
|
|
@@ -119,7 +126,7 @@ const failedStep = (log, ok, failed) => {
|
|
|
119
126
|
};
|
|
120
127
|
const stageEnded = (runDir, stage) => {
|
|
121
128
|
const last = lastAttempt(runDir, stage);
|
|
122
|
-
return last?.
|
|
129
|
+
return last?.fail ? last : null;
|
|
123
130
|
};
|
|
124
131
|
|
|
125
132
|
// The register's own steps, each of which the run survives without: the band's shape (the floors and the
|
package/driver/dev-portal.mjs
CHANGED
|
@@ -380,7 +380,8 @@ export function startPortal({ poolRoot = null, port = 18899, host = "127.0.0.1",
|
|
|
380
380
|
// "knockout" enqueued a job naming no product at all and ran whatever the account default or the
|
|
381
381
|
// request's own territories named — a silent product substitution at a live door, from a control
|
|
382
382
|
// that said otherwise. The menu is gone; refusing the field as well is what stops a stale tab, a
|
|
383
|
-
// bookmarked script or a copied curl from doing it quietly.
|
|
383
|
+
// bookmarked script or a copied curl from doing it quietly.
|
|
384
|
+
// SEARCH_LEVEL_NOT_A_REQUEST declared in products.mjs states the rule.
|
|
384
385
|
//
|
|
385
386
|
// THE SENTENCE IS NO LONGER WRITTEN HERE. This door had the only searchLevel refusal in the
|
|
386
387
|
// system, in its own words — so the same request was refused here and accepted at the other
|
|
@@ -46,6 +46,25 @@ export function dispatchFileName(stage, attempt, repair = 0) {
|
|
|
46
46
|
return `${stage}.attempt${attempt}${repair ? `.repair${repair}` : ""}.${DISPATCH_SUFFIX}`;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export const STREAMS_DIR = "streams";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `_driver/streams/<label>.attempt<N>.jsonl` (or `…repair<M>…`): the raw stream of the session that
|
|
53
|
+
* dispatch started, named as its dispatch record is. The engines write it (engine/session-record.mjs,
|
|
54
|
+
* `streamSink`); this is the one place the name is made. Absolute, through `driverDir`, so a label's
|
|
55
|
+
* colon is written the way the platform can hold it.
|
|
56
|
+
*
|
|
57
|
+
* IN ITS OWN FOLDER, NEVER BESIDE THE JOURNALS. Every `*.jsonl` directly under `_driver/` is read as a
|
|
58
|
+
* stage's attempt journal by name: the token rollup sums its usage rows, the degraded-parts check reads
|
|
59
|
+
* its label off the file name, the integrity check leaves it out as append-only. A raw stream there
|
|
60
|
+
* would be counted as a stage and its usage summed twice. One folder down, no reader of `_driver/`
|
|
61
|
+
* lists it, and the integrity snapshot reads files only.
|
|
62
|
+
*/
|
|
63
|
+
export function streamFilePath(runDir, stage, attempt, repair = 0) {
|
|
64
|
+
if (!runDir) return null;
|
|
65
|
+
return join(driverDir(runDir), STREAMS_DIR, driverFileName(`${stage}.attempt${attempt}${repair ? `.repair${repair}` : ""}.jsonl`));
|
|
66
|
+
}
|
|
67
|
+
|
|
49
68
|
const shaOf = (buf) => createHash("sha256").update(buf).digest("hex").slice(0, 12);
|
|
50
69
|
|
|
51
70
|
// ── — WHAT THE STAGE WAS GRANTED, BESIDE THE SHA THAT PROVES WHAT IT WAS ASKED ────────────────
|
|
@@ -119,8 +119,8 @@ export function captureCall(runDir, seq, received, { now = () => new Date().toIS
|
|
|
119
119
|
//
|
|
120
120
|
// THAT PREMISE IS WHY THIS IS SAFE, SO CHECK IT BEFORE MOVING THE SPEC WRITE. Five of the six
|
|
121
121
|
// doubt-minting sites build an id from an ordinal over a FILTERED list
|
|
122
|
-
// (`presence-reconciliation.mjs:182` mints `doubt:presence:<sheet>:<n>` after three `continue` filters,
|
|
123
|
-
// with `status: "open"` hardcoded beside it); `doubt-ledger.mjs:232` is the exception and the template,
|
|
122
|
+
// (`presence-reconciliation.mjs:182 mintPresenceDoubts` mints `doubt:presence:<sheet>:<n>` after three `continue` filters,
|
|
123
|
+
// with `status: "open"` hardcoded beside it); `doubt-ledger.mjs:232 mintContradictionDoubts` is the exception and the template,
|
|
124
124
|
// putting the candidate name in the id and using the ordinal only as a tiebreaker. If the spec is ever
|
|
125
125
|
// minted per attempt instead of per stage, those ordinals can shift between attempts and this serve
|
|
126
126
|
// starts handing back a verdict about a different subject. Nothing else in this file would notice.
|
package/driver/driver.config.mjs
CHANGED
|
@@ -1337,6 +1337,10 @@ export const PROVIDERS = {
|
|
|
1337
1337
|
return { ok: true,
|
|
1338
1338
|
total: Number.isFinite(p.total_hits) ? p.total_hits : null,
|
|
1339
1339
|
...(p.total_approximate === true ? { approximate: true, floor: Number.isFinite(p.total_floor) ? p.total_floor : null } : {}),
|
|
1340
|
+
// The register's own notes on this answer, carried whole as the clearance sweep carries them —
|
|
1341
|
+
// `expanded_fallback` among them, which says the total counts records rather than marks. The
|
|
1342
|
+
// knockout listing dropped them, so a knockout run could not show what a clearance run shows.
|
|
1343
|
+
...(Array.isArray(p.warnings) && p.warnings.length ? { warnings: p.warnings } : {}),
|
|
1340
1344
|
records: (Array.isArray(p.results) ? p.results : []).map((row) => {
|
|
1341
1345
|
const rec = row?.raw && typeof row.raw === "object" ? core.normalizeRecord(row.raw, row.office || null) : null;
|
|
1342
1346
|
return {
|
|
@@ -1355,6 +1359,9 @@ export const PROVIDERS = {
|
|
|
1355
1359
|
filing_route: rec?.filingRoute ?? null,
|
|
1356
1360
|
// No page per record at Signa (hasPublicRecordUrl: false above), and none is made up here.
|
|
1357
1361
|
record_url: null,
|
|
1362
|
+
// The office's own page as the register gives it, for publish to fall back on where it cannot
|
|
1363
|
+
// address the office's page from the numbers above (office-record-links.mjs).
|
|
1364
|
+
office_url: rec?.officeUrl ?? null,
|
|
1358
1365
|
};
|
|
1359
1366
|
}) };
|
|
1360
1367
|
} catch (e) { return { ok: false, records: null, reason: `listRecords threw: ${e.message}` }; }
|
|
@@ -2079,12 +2086,31 @@ export const ENGINE_BINARIES = {
|
|
|
2079
2086
|
// and a pinned id was refused outright ("version 2.1.280 or newer is required"), measured 2026-09-22.
|
|
2080
2087
|
// A floor that only asks for a program that starts is a floor that passes a machine this engine
|
|
2081
2088
|
// cannot run the current generation on.
|
|
2082
|
-
|
|
2089
|
+
//
|
|
2090
|
+
// THE HAIKU TIER CROSSED LATER, when Claude Haiku 5.5 joined the generation. Measured 2026-10-09 by a
|
|
2091
|
+
// one-word `-p` turn per version on the test box's own sign-in, reading the model the turn reports in
|
|
2092
|
+
// `modelUsage`:
|
|
2093
|
+
//
|
|
2094
|
+
// version --model haiku
|
|
2095
|
+
// 2.1.285 claude-haiku-4-5-20251001
|
|
2096
|
+
// 2.1.287 claude-haiku-4-5-20251001
|
|
2097
|
+
// 2.1.288 claude-haiku-4-5-20251001
|
|
2098
|
+
// 2.1.289 claude-haiku-4-5-20251001
|
|
2099
|
+
// 2.1.290 claude-haiku-4-5-20251001
|
|
2100
|
+
// 2.1.292 claude-haiku-4-5-20251001
|
|
2101
|
+
// 2.1.293 claude-haiku-5-5
|
|
2102
|
+
// 2.1.294 claude-haiku-5-5
|
|
2103
|
+
// 2.1.295 claude-haiku-5-5
|
|
2104
|
+
//
|
|
2105
|
+
// 2.1.295 still serves claude-opus-5-5 and claude-sonnet-5-5. The floor moves to 2.1.293 for the reason
|
|
2106
|
+
// it moved to 2.1.284: a stage that asks for haiku is asking for the current haiku.
|
|
2107
|
+
package: "@anthropic-ai/claude-code", floor: "2.1.293",
|
|
2083
2108
|
// WHAT THE INSTALL TAKES ON DISK, in MB, which setup states before it asks to install. MEASURED, not
|
|
2084
2109
|
// declared by the vendor: the engines folder after a fresh install of this package into an empty
|
|
2085
|
-
// folder
|
|
2086
|
-
//
|
|
2087
|
-
|
|
2110
|
+
// folder. Re-measured when the floor moved to 2.1.293: 242 MB for a fresh install of 2.1.293 itself,
|
|
2111
|
+
// on npm 10.9.9, 2026-10-09. A later release can be larger or smaller, so setup says "about".
|
|
2112
|
+
// Re-measure when the floor moves.
|
|
2113
|
+
installMB: 242,
|
|
2088
2114
|
// The licence setup states wherever it tells a reader what they are about to install or use, as the
|
|
2089
2115
|
// vendor's package declares it: "SEE LICENSE IN README.md", Anthropic's own terms.
|
|
2090
2116
|
licence: "proprietary third-party software",
|
|
@@ -2110,8 +2136,15 @@ export const ENGINE_BINARIES = {
|
|
|
2110
2136
|
vendor: "OpenAI",
|
|
2111
2137
|
product: "Codex",
|
|
2112
2138
|
env: "CLEAROTRON_CODEX_PATH", fallback: "codex",
|
|
2113
|
-
|
|
2114
|
-
|
|
2139
|
+
// THE FLOOR IS THE OLDEST RELEASE THAT ACCEPTS THE JUDGMENT TIER'S MODEL on a ChatGPT sign-in, which
|
|
2140
|
+
// is how a subscription bills. Measured 2026-10-09 with a one-word `codex exec` turn per version on the
|
|
2141
|
+
// test box: `gpt-6.1-sol` is refused with a 400 on 0.156.1, 0.157.0 and 0.158.0 ("not supported when
|
|
2142
|
+
// using Codex with a ChatGPT account") and accepted on 0.159.0, 0.160.0, 0.161.0 and 0.162.0;
|
|
2143
|
+
// `gpt-6-luna` is accepted from 0.156.1. Below the floor every clearance stops at its first
|
|
2144
|
+
// judgment-tier stage (the tiers are in engine/openai-agent.mjs). The old floor, 0.154.0, was the
|
|
2145
|
+
// permissions feature's (engine/mcp/codex-config.mjs).
|
|
2146
|
+
package: "@openai/codex", floor: "0.159.0",
|
|
2147
|
+
installMB: 424, // re-measured the same way as Claude's, above: a fresh install of 0.159.0 itself, 2026-10-09
|
|
2115
2148
|
licence: "third-party software under the Apache-2.0 licence", // the package's own "license" field
|
|
2116
2149
|
label: "OpenAI — each stage runs as a headless `codex exec` turn",
|
|
2117
2150
|
module: "engine/openai-agent.mjs", adapter: "openaiAgentEngine",
|
|
@@ -37,7 +37,7 @@ export function isFinding(heading, block) {
|
|
|
37
37
|
|
|
38
38
|
// Distinctive word anchors (length ≥4, non-stopword) from the owner / element / goods / sector /
|
|
39
39
|
// jurisdiction / mark vocabulary; multi-word anchors contribute their significant words (so a finding
|
|
40
|
-
// mentioning "
|
|
40
|
+
// mentioning "Varnholt" anchors against owner "Varnholt Advokatpartnerselskab").
|
|
41
41
|
function anchorTerms(anchors = {}) {
|
|
42
42
|
const raw = [
|
|
43
43
|
...(anchors.owners ?? []), ...(anchors.dominantElements ?? []), ...(anchors.goodsServices ?? []),
|
|
@@ -36,7 +36,9 @@ ladder consumes it without knowing which engine produced it ([gateway.mjs](../ga
|
|
|
36
36
|
laneWaitMs: number,
|
|
37
37
|
json: object | null, // synthesized envelope in the classifier's shape; every downstream
|
|
38
38
|
// classifier (payloadText, status, embedded-fallback, timeout,
|
|
39
|
-
// lane-wedge) reads this and works unchanged across engines
|
|
39
|
+
// lane-wedge) reads this and works unchanged across engines.
|
|
40
|
+
// It also carries how the session ended: terminalReason,
|
|
41
|
+
// apiErrorStatus and errorText (see `session` below)
|
|
40
42
|
usage: Usage | null, // canonical shape below; null = no tokens accounted (e.g. stall)
|
|
41
43
|
sessionRef: string | null, // opaque resume handle (claude session_id | codex thread_id)
|
|
42
44
|
modelWire: string | null, // MODEL GAUGE — the served model id this turn observed (§3);
|
|
@@ -57,9 +59,47 @@ ladder consumes it without knowing which engine produced it ([gateway.mjs](../ga
|
|
|
57
59
|
// opened; [] = recorded "read nothing"; key absent = engine cannot
|
|
58
60
|
// observe reads (openai-agent) → gateway journals `reads: null`
|
|
59
61
|
readsTruncated: boolean | absent, // true = the path cap dropped at least one further distinct file
|
|
62
|
+
toolCallsByName: object | null, // the turn's tool calls by tool name; an MCP call is
|
|
63
|
+
// `mcp__<server>__<tool>` on both engines; null = no session ran
|
|
64
|
+
session: Session | null, // what the session went through (below); null = no session ran
|
|
65
|
+
stream: { file, present, bytes, reason? } | null,
|
|
66
|
+
// the raw stream, written to the file the caller named
|
|
67
|
+
// (the gateway names _driver/streams/<label>.attempt<N>.jsonl)
|
|
60
68
|
}
|
|
61
69
|
```
|
|
62
70
|
|
|
71
|
+
### `session` — what the session went through
|
|
72
|
+
|
|
73
|
+
Both engines used to keep only the LAST result of a session, so a session the vendor's classifier cut
|
|
74
|
+
off mid-answer, or refused and a restart recovered, ended in a successful result and was recorded as
|
|
75
|
+
clean. `session` keeps every event of that kind, as the program wrote it, with milliseconds since the
|
|
76
|
+
spawn ([engine/session-record.mjs](session-record.mjs)):
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
Session { interrupted, starts, results: [{atMs, index, subtype, isError, stopReason, terminalReason, origin}],
|
|
80
|
+
classifierCuts: [{atMs, synthetic}], refusals: [{atMs, category, model}], refusalStops }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
It carries kinds, codes and times, never the program's words. The retry ladder still reads the last
|
|
84
|
+
result alone; the gateway journals the attempt as `ok: false` when any result carried `is_error`, while
|
|
85
|
+
`fail` — what every reader of "did the stage fail" reads — is unchanged. Codex states no cut or refusal
|
|
86
|
+
on its stream, so on that engine `results` lists every `turn.completed`, `turn.failed` and `error`.
|
|
87
|
+
|
|
88
|
+
How the session ended rides the envelope, and the gateway writes it on every attempt and repair row,
|
|
89
|
+
null where the engine said nothing:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
terminalReason the kind of ending as the program states it (claude `terminal_reason`); null on codex
|
|
93
|
+
apiErrorStatus the vendor's status when it gave one (claude `api_error_status`); null on codex
|
|
94
|
+
errorText the program's own words for the error, only when the session ended in one, cut to
|
|
95
|
+
300 characters: claude's result text when that result has `is_error`; codex's
|
|
96
|
+
`turn.failed` message, or its stream error when the turn never completed
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
These are the one place the record holds the program's words: an expired sign-in otherwise reads as
|
|
100
|
+
`stop_sequence` and nothing else. `stopReason` stays verbatim beside them, and none of the three moves a
|
|
101
|
+
verdict.
|
|
102
|
+
|
|
63
103
|
`runStage()` folds that into what the pipeline consumes — `{ok, json, attempts, text, sessionKey, …}` —
|
|
64
104
|
and journals usage/wall/status on the way past.
|
|
65
105
|
|
|
@@ -26,6 +26,7 @@ import { authorityTrees } from "../authority-trees.mjs";
|
|
|
26
26
|
import { recordEngineChild, clearEngineChild } from "./child-record.mjs"; //
|
|
27
27
|
import { engineEnv } from "./engine-env.mjs"; // — the program's environment, by list (see spawnEnv)
|
|
28
28
|
import { engineSpawn, spawnsDetached, killWindowsTreeNow } from "./engine-spawn.mjs"; // one answer to how a turn starts and ends, on every platform
|
|
29
|
+
import { newSessionRecord, noteClaudeEvent, sessionSummary, countTool, streamSink, claudeEnding } from "./session-record.mjs"; // what the session went through, beside how it ended
|
|
29
30
|
|
|
30
31
|
// Read per-call (not module-level) so tests can drive a short stall timeout / a mock binary.
|
|
31
32
|
// ONE place knows how to find the program (driver.config.mjs resolveEngineProgram): the explicit setting,
|
|
@@ -514,6 +515,9 @@ export function synthesizeEnvelope({ resultEvent, usage, killed }) {
|
|
|
514
515
|
status: ok ? "ok" : (killed ? "timeout" : "error"),
|
|
515
516
|
result: { meta: { agentMeta: { usage } }, payloads: [{ text }] },
|
|
516
517
|
summary: r?.subtype, runId: r?.session_id, stopReason: r?.stop_reason,
|
|
518
|
+
// …and beside the stop reason, what ended the session and, on an error, the program's own words for it
|
|
519
|
+
// (session-record.mjs, claudeEnding). Recorded only: `ok` above reads exactly what it read before.
|
|
520
|
+
...claudeEnding(r),
|
|
517
521
|
};
|
|
518
522
|
}
|
|
519
523
|
|
|
@@ -524,7 +528,7 @@ export const anthropicAgentEngine = {
|
|
|
524
528
|
// doctrine tree, the profile store and `<runDir>/_driver/` at the moment of the write. That is a real
|
|
525
529
|
// boundary and this engine has it.
|
|
526
530
|
writeBoundary: "enforced",
|
|
527
|
-
async runTurn({ message, model, thinking, timeoutSec, resumeRef, mcpConfig, allowedTools, cwd, skillsDir, skillsGrantRoots, profilesDir, resolveSkill, runDir, seatWrites = null, grantDispatch = null, stallSec, progressFiles } = {}) {
|
|
531
|
+
async runTurn({ message, model, thinking, timeoutSec, resumeRef, mcpConfig, allowedTools, cwd, skillsDir, skillsGrantRoots, profilesDir, resolveSkill, runDir, seatWrites = null, grantDispatch = null, stallSec, progressFiles, streamFile = null } = {}) {
|
|
528
532
|
if (!message) throw new Error("anthropic-agent.runTurn: message is required");
|
|
529
533
|
const { args, input, grantNote } = buildClaudeArgs({ message, model, thinking, resumeRef, mcpConfig, allowedTools, cwd, skillsDir, skillsGrantRoots, profilesDir, resolveSkill, runDir, seatWrites, grantDispatch });
|
|
530
534
|
// the cross-check speaks. Driver stderr is what an operator reads, and it is where the other
|
|
@@ -690,6 +694,24 @@ export const anthropicAgentEngine = {
|
|
|
690
694
|
// ("a completed tool result = the agent loop advanced", below). The gap between them is the turn
|
|
691
695
|
// waiting on tools rather than generating.
|
|
692
696
|
let toolCalls = 0;
|
|
697
|
+
// …AND THE SAME CALLS BY TOOL NAME, so a tool server's own log can be set against what the model
|
|
698
|
+
// asked for. A name is the program's identifier for a tool (`Read`, `mcp__band__band_lookup`), never
|
|
699
|
+
// an input, so it carries no client text. The values sum to `toolCalls` by construction: one fold
|
|
700
|
+
// per tool_use block, at the one site that counts them.
|
|
701
|
+
const toolCallsByName = Object.create(null);
|
|
702
|
+
// …AND WHICH OF THEM A TOOL SERVER ANSWERED WITH AN ERROR. A refused call the model then re-made used to
|
|
703
|
+
// leave the stage row reading ok on one attempt, as though nothing had happened. Each ask's id names its
|
|
704
|
+
// tool, and its result says whether it came back as an error; nothing else is kept.
|
|
705
|
+
const toolNameById = new Map();
|
|
706
|
+
const toolErrorIds = new Set();
|
|
707
|
+
// WHAT THE SESSION WENT THROUGH, beside how it ended: every result event, every classifier cut,
|
|
708
|
+
// every refusal and every restart, each with when it arrived. See session-record.mjs; the retry
|
|
709
|
+
// policy below still reads the LAST result alone, so this records and decides nothing.
|
|
710
|
+
const session = newSessionRecord();
|
|
711
|
+
// The raw stream, one file per session beside its dispatch (the caller names it). Opened before the
|
|
712
|
+
// first byte can arrive; closed in settle(). A sink that could not open is a null writer whose
|
|
713
|
+
// close() says why.
|
|
714
|
+
const sink = streamSink(streamFile);
|
|
693
715
|
// HOW MANY OF THOSE ASKED FOR A COMMAND TOOL. The command tools are removed from every stage
|
|
694
716
|
// (COMMAND_TOOLS), so the number a test round expects here is zero, and a count is what proves it: a
|
|
695
717
|
// flag in the argv shows what was asked of the program, this shows what the model did. Still a count
|
|
@@ -814,6 +836,7 @@ export const anthropicAgentEngine = {
|
|
|
814
836
|
let ev;
|
|
815
837
|
try { ev = JSON.parse(line); } catch { return; /* partial/non-json line */ }
|
|
816
838
|
sawAnyEvent = true;
|
|
839
|
+
noteClaudeEvent(session, ev, Date.now() - t0);
|
|
817
840
|
if (ev.type === "result") resultEvent = ev;
|
|
818
841
|
else if (ev.type === "rate_limit_event") rateLimitEvent = ev;
|
|
819
842
|
else if (ev.type === "system" && ev.subtype === "init") {
|
|
@@ -839,7 +862,8 @@ export const anthropicAgentEngine = {
|
|
|
839
862
|
// classification and no recovery park, or the turn never settles with its watchdog already gone.
|
|
840
863
|
// Same belt-and-braces as the thinking gauge's `?.some?.()` on the line above.
|
|
841
864
|
for (const b of Array.isArray(ev.message?.content) ? ev.message.content : []) {
|
|
842
|
-
if (b?.type === "tool_use") toolCalls++; // #1111 — every tool, not only Read;
|
|
865
|
+
if (b?.type === "tool_use") { toolCalls++; countTool(toolCallsByName, b?.name); } // #1111 — every tool, not only Read; the count and its split by name
|
|
866
|
+
if (b?.type === "tool_use" && b?.id != null) toolNameById.set(String(b.id), String(b?.name ?? "?"));
|
|
843
867
|
if (b?.type === "tool_use" && COMMAND_TOOLS.includes(b?.name)) commandToolCalls++;
|
|
844
868
|
if (b?.type === "tool_use" && FILE_WRITE_TOOLS.includes(b?.name) && b?.id != null) writeAsks.add(String(b.id));
|
|
845
869
|
if (b?.type === "tool_use" && b?.name === "Read" && typeof b?.input?.file_path === "string") {
|
|
@@ -873,8 +897,10 @@ export const anthropicAgentEngine = {
|
|
|
873
897
|
progress();
|
|
874
898
|
}
|
|
875
899
|
else if (ev.type === "user") {
|
|
876
|
-
for (const b of Array.isArray(ev.message?.content) ? ev.message.content : [])
|
|
900
|
+
for (const b of Array.isArray(ev.message?.content) ? ev.message.content : []) {
|
|
877
901
|
if (b?.type === "tool_result" && b?.is_error === true && writeAsks.has(String(b?.tool_use_id))) writesFailed++;
|
|
902
|
+
if (b?.type === "tool_result" && b?.is_error === true && b?.tool_use_id != null) toolErrorIds.add(String(b.tool_use_id));
|
|
903
|
+
}
|
|
878
904
|
// the result of the ask above. Only counted when an ask is outstanding, so a `user`
|
|
879
905
|
// event with no preceding tool_use adds nothing rather than charging the turn for a gap it
|
|
880
906
|
// never spent waiting.
|
|
@@ -1052,6 +1078,7 @@ export const anthropicAgentEngine = {
|
|
|
1052
1078
|
chunkGapMs = prevChunkAt === null ? 0 : Date.now() - prevChunkAt;
|
|
1053
1079
|
prevChunkAt = Date.now();
|
|
1054
1080
|
lastMove = Date.now(); // ANY streamed byte = liveness → resets the stall clock
|
|
1081
|
+
sink.write(d); // the raw stream, byte for byte, before anything parses it
|
|
1055
1082
|
buf += d.toString();
|
|
1056
1083
|
let nl;
|
|
1057
1084
|
while ((nl = buf.indexOf("\n")) >= 0) { parseLine(buf.slice(0, nl)); buf = buf.slice(nl + 1); }
|
|
@@ -1074,7 +1101,7 @@ export const anthropicAgentEngine = {
|
|
|
1074
1101
|
if (stderr.length > maxBuffer) { stderr = stderr.slice(0, maxBuffer); overflowKill(); }
|
|
1075
1102
|
});
|
|
1076
1103
|
// error + close can BOTH fire for a single failure (Node) → settle EXACTLY once (no double clearInterval/resolve).
|
|
1077
|
-
child.on("error", (e) => { if (settled) return; settled = true; clearInterval(watchdog); resolve(errResult(t0, e, resumeRef)); });
|
|
1104
|
+
child.on("error", (e) => { if (settled) return; settled = true; clearInterval(watchdog); sink.close(); resolve(errResult(t0, e, resumeRef)); });
|
|
1078
1105
|
child.on("close", (code) => settle(code));
|
|
1079
1106
|
function settle(code) {
|
|
1080
1107
|
// the turn is over; the record must not outlive it, and is cleared only if
|
|
@@ -1087,6 +1114,7 @@ export const anthropicAgentEngine = {
|
|
|
1087
1114
|
try { child.stdout.destroy(); } catch { /* stream gone */ }
|
|
1088
1115
|
try { child.stderr.destroy(); } catch { /* stream gone */ }
|
|
1089
1116
|
if (!overflow) { parseLine(buf); } // B1: flush the final, un-terminated line (the result event) — but NEVER the truncated overflow tail
|
|
1117
|
+
const stream = sink.close(); // after the flush: every byte the program wrote is in it, or the record says why not
|
|
1090
1118
|
buf = "";
|
|
1091
1119
|
// ONE SAMPLE OF THE CLOCK, and both quantities below are derived from it. Two `Date.now()` calls
|
|
1092
1120
|
// a few milliseconds apart are enough to break the identity on the very turn it matters for: on a
|
|
@@ -1176,11 +1204,18 @@ export const anthropicAgentEngine = {
|
|
|
1176
1204
|
// from this list is unreliable and the consumer must downgrade it to "not observed").
|
|
1177
1205
|
reads: [...reads], readsTruncated,
|
|
1178
1206
|
toolCalls, toolWaitMs: settledToolWaitMs, // #1111 — two integers, no content
|
|
1207
|
+
// The same calls by tool name (values sum to toolCalls), and what the session went through: every
|
|
1208
|
+
// result, cut, refusal and restart (session-record.mjs). `stream` is the raw stream's file and size,
|
|
1209
|
+
// or why it was not kept. Recording only — `failed` above still reads the last result alone.
|
|
1210
|
+
toolCallsByName: { ...toolCallsByName }, session: sessionSummary(session), stream,
|
|
1179
1211
|
// The two counts a test round reads per stage: calls to a command tool, and calls the program
|
|
1180
1212
|
// refused. The refusals are the result event's own `permission_denials`, counted; null when the
|
|
1181
1213
|
// turn settled with no result event, so "not reported" never reads as "none refused".
|
|
1182
1214
|
commandToolCalls, writesFailed,
|
|
1183
1215
|
toolCallsRefused: Array.isArray(resultEvent?.permission_denials) ? resultEvent.permission_denials.length : null,
|
|
1216
|
+
// The tool-server calls that came back as an error, by tool name: the program sent them and the
|
|
1217
|
+
// server refused or failed them. A call the program itself refused is counted above and not here.
|
|
1218
|
+
toolCallsErroredByName: toolServerErrors(toolErrorIds, toolNameById, resultEvent?.permission_denials),
|
|
1184
1219
|
// ms from spawn to the child's FIRST byte, or null when it never spoke. RECORDING
|
|
1185
1220
|
// ONLY: nothing branches on it. A recording field is protected by a test or by nothing, so
|
|
1186
1221
|
// engine.anthropic.test.mjs pins it — deleting it must red something.
|
|
@@ -1253,9 +1288,26 @@ function errResult(t0, e, resumeRef) {
|
|
|
1253
1288
|
// modelWire: null for the opposite reason — no turn ran, so the wire said nothing about a model, and
|
|
1254
1289
|
// the record must say UNKNOWN rather than inherit the alias that was asked for.
|
|
1255
1290
|
json: null, usage: null, reads: [], readsTruncated: false, modelWire: null, providerWire: null, sessionRef: resumeRef ?? null,
|
|
1291
|
+
// No session ran, so there is nothing to have been interrupted and no stream: null, never an empty record.
|
|
1292
|
+
toolCallsByName: null, toolCallsErroredByName: null, session: null, stream: null,
|
|
1256
1293
|
};
|
|
1257
1294
|
}
|
|
1258
1295
|
|
|
1296
|
+
/**
|
|
1297
|
+
* The tool-server calls of one turn that came back as an error, by tool name: every `mcp__` ask whose
|
|
1298
|
+
* result carried `is_error`, less the asks the program refused itself (`permission_denials`, which
|
|
1299
|
+
* `toolCallsRefused` counts). An OBJECT is a measurement (`{}`: none). PURE.
|
|
1300
|
+
*/
|
|
1301
|
+
export function toolServerErrors(errorIds, nameById, denials) {
|
|
1302
|
+
const denied = new Set((Array.isArray(denials) ? denials : []).map((d) => String(d?.tool_use_id ?? "")).filter(Boolean));
|
|
1303
|
+
const byName = Object.create(null);
|
|
1304
|
+
for (const id of errorIds ?? []) {
|
|
1305
|
+
const name = nameById?.get(id);
|
|
1306
|
+
if (typeof name === "string" && name.startsWith("mcp__") && !denied.has(id)) countTool(byName, name);
|
|
1307
|
+
}
|
|
1308
|
+
return { ...byName };
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1259
1311
|
/**
|
|
1260
1312
|
* The program's own word for which provider served a turn, from the result event's per-model usage:
|
|
1261
1313
|
* `modelUsage[<model>].provider`, "firstParty" on Anthropic's own API and "foundry" on Azure Foundry
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// exact server/tool wiring. Two inputs, both produced today by the gateway gather block:
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
|
-
import { tomlString } from "../../../shared/toml-string.mjs";
|
|
8
|
+
import { tomlString } from "../../../shared/toml-string.mjs"; import { driverDir } from "../../../shared/driver-dir.mjs";
|
|
9
9
|
|
|
10
10
|
// • mcpConfig — the claude-shaped JSON string `{mcpServers:{name:{command,args,env,connectionTimeoutMs?}}}`
|
|
11
11
|
// (buildGatherMcpConfig → JSON.stringify)
|
|
@@ -218,7 +218,7 @@ export function commandEnvToml(withheld = []) {
|
|
|
218
218
|
// ── the stage's permission profile: what its shell commands may read and write ──────────────────────
|
|
219
219
|
// codex's `workspace-write` sandbox confines where a command WRITES and lets it read the whole disk, the
|
|
220
220
|
// install's settings files and the program's own sign-in included. A permission profile confines reading
|
|
221
|
-
// too (the vendor's Permissions guide, a beta feature since 0.138.0; the floor
|
|
221
|
+
// too (the vendor's Permissions guide, a beta feature since 0.138.0; the floor was 0.154.0 when this was written and is 0.159.0 now, for the judgment tier's model — driver.config.mjs): a stage's
|
|
222
222
|
// commands read the platform's own paths, the instruction trees and the temp folders, and write the run
|
|
223
223
|
// folder, their working folder and the temp folders. Nothing else is readable, so a page a stage fetched
|
|
224
224
|
// cannot get a command to print the account's files. The temp folders stay writable because
|
|
@@ -235,13 +235,18 @@ export function commandEnvToml(withheld = []) {
|
|
|
235
235
|
// this profile never names. Not a refusal of their names inside the temp folder: codex scans a folder
|
|
236
236
|
// before it applies a wildcard refusal there, and in a shared temp folder one entry the account cannot
|
|
237
237
|
// read fails the whole turn (measured, 0.156.1).
|
|
238
|
+
//
|
|
239
|
+
// THE DRIVER'S OWN RECORDS INSIDE THE RUN FOLDER ARE READ-ONLY. `_driver/` holds the run's trace and the
|
|
240
|
+
// files the gates judge from, so a stage's command may read them and never write them. The narrower line
|
|
241
|
+
// wins over the run folder's: measured on codex 0.162.0 with this profile, a write into the run folder
|
|
242
|
+
// succeeded, a read of `_driver/` succeeded, and a write or an append there was refused.
|
|
238
243
|
export const FENCE_PROFILE = "clearotron-stage";
|
|
239
244
|
export function fenceToml({ runDir = null, readRoots = [] } = {}) {
|
|
240
245
|
const key = (p) => tomlString(p);
|
|
241
246
|
const out = [`[permissions.${FENCE_PROFILE}.filesystem]`,
|
|
242
247
|
`":minimal" = "read"`, `":tmpdir" = "write"`, `":slash_tmp" = "write"`];
|
|
243
248
|
for (const r of [...new Set(readRoots.filter(Boolean))]) out.push(`${key(r)} = "read"`);
|
|
244
|
-
if (runDir) out.push(`${key(runDir)} = "write"`);
|
|
249
|
+
if (runDir) out.push(`${key(runDir)} = "write"`, `${key(driverDir(runDir))} = "read"`);
|
|
245
250
|
out.push("", `[permissions.${FENCE_PROFILE}.filesystem.":workspace_roots"]`, `"." = "write"`, "");
|
|
246
251
|
return out;
|
|
247
252
|
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// wired per run by the driver; there is no parameter for it and this tool never guesses one
|
|
20
20
|
// (recording-server.mjs's record_doubt_closure records why the parameter must not exist). The form name comes off the run's own
|
|
21
21
|
// era stamp, the row ids are the driver's, and every other identifier is computed. Read at CALL TIME,
|
|
22
|
-
// never captured at module load (stdio-server.mjs:29-33 states why for exactly this variable).
|
|
22
|
+
// never captured at module load (stdio-server.mjs:29-33 toolLogPath states why for exactly this variable).
|
|
23
23
|
import { serve } from "./stdio-server.mjs";
|
|
24
24
|
import { recordCoverage } from "../../coverage-tool.mjs";
|
|
25
25
|
import { MAX_ROWS_PER_CALL } from "../../coverage-call.mjs";
|
|
@@ -107,7 +107,7 @@ export const REGISTER_SERVERS = {
|
|
|
107
107
|
// public page, and this provider publishes neither. Read 2026-08-22 across providers/signa/src: no
|
|
108
108
|
// vienna or figurative field anywhere in the core, the normaliser carries `imageAvailable`
|
|
109
109
|
// (`rec.has_media`) and nothing else about the mark's figure, `hasPublicRecordUrl: false` and
|
|
110
|
-
// `resolved_link: null`
|
|
110
|
+
// `resolved_link: null` (no page per record of its own). Serving it would return a
|
|
111
111
|
// bare boolean under a name that promises comparable figurative data. NOT A PROBE: this is a read of
|
|
112
112
|
// the code and the vendor doc in this repo, and whether the live API has an image endpoint nobody
|
|
113
113
|
// wired is UNTESTED from here — euipo's `markImage` looked equally absent until `GET
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// ── WHAT THIS CATEGORY IS FOR, AND WHY IT IS NOT A RETRIEVAL GRANT ──────────────────────────────────
|
|
6
6
|
//
|
|
7
7
|
// The two-box model conflates "may reach the outside world" with "may hand back structure"
|
|
8
|
-
// (gather-config.mjs:246-268). A stage converted to a typed return needs the second and not the first.
|
|
8
|
+
// (gather-config.mjs:246-268 RECORDING). A stage converted to a typed return needs the second and not the first.
|
|
9
9
|
// This server carries ONLY the second: it writes the calling stage's own artifact into the calling
|
|
10
10
|
// stage's own run directory, and it dials nothing.
|
|
11
11
|
//
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
//
|
|
23
23
|
// ── THE RUN IS NOT THE SEAT'S TO NAME ───────────────────────────────────────────────────────────────
|
|
24
24
|
//
|
|
25
|
-
// `CLEAROTRON_BAND_RUN_DIR`, read at CALL TIME.
|
|
25
|
+
// `CLEAROTRON_BAND_RUN_DIR`, read at CALL TIME. serverEnv() in gather-config.mjs sets it for every local
|
|
26
26
|
// server, not only band. There is no `run_dir` parameter and there must not be one — the lesson is
|
|
27
27
|
// the earlier one, where an invented `CLEAROTRON_RUN_DIR` fallback sat two lines under a sentence promising the tool
|
|
28
28
|
// never guesses a run. Call time rather than module load: stdio-server.mjs:29-33 states why for this
|
|
@@ -55,7 +55,7 @@ import { PROVIDERS, REGISTER_PROVIDER } from "../../driver.config.mjs";
|
|
|
55
55
|
// already written against them; this conversion is the consumption their headers said was still owed.
|
|
56
56
|
import { recordClosures } from "../../doubt-closure-tool.mjs";
|
|
57
57
|
import { MAX_CLOSURES_PER_CALL, CLOSURE_KINDS } from "../../doubt-closure-call.mjs";
|
|
58
|
-
import { readFileSync } from "node:fs";
|
|
58
|
+
import { readFileSync, existsSync } from "node:fs"; import { FROZEN_METHOD_FILE } from "../../framework-method.mjs";
|
|
59
59
|
import { join } from "node:path";
|
|
60
60
|
import { driverDir } from "../../../shared/driver-dir.mjs"; //
|
|
61
61
|
import { VARIANT_CATEGORIES } from "../../variant-manifest-model.mjs";
|
|
@@ -398,7 +398,7 @@ serve({
|
|
|
398
398
|
item: {
|
|
399
399
|
type: "string",
|
|
400
400
|
description:
|
|
401
|
-
"For a FIRING variant directive this must be a mark-shaped search term (
|
|
401
|
+
"For a FIRING variant directive this must be a mark-shaped search term (WAVA, CORAL " +
|
|
402
402
|
"MAGIC) or carry a remedy. A label — a parenthetical, an enumeration, more than about " +
|
|
403
403
|
"four words — dispatches as a nil search that reads CLEAN, and is refused here.",
|
|
404
404
|
},
|
|
@@ -1134,6 +1134,9 @@ serve({
|
|
|
1134
1134
|
handler: record_synthesis,
|
|
1135
1135
|
}, {
|
|
1136
1136
|
name: "record_knockout_assess",
|
|
1137
|
+
// The framework's `inputs` fields are offered only on a run whose framework states a method: a session
|
|
1138
|
+
// offered a field it may not use can fill it, and the check that refuses it runs after the whole step.
|
|
1139
|
+
schemaNow: (schema) => knockoutAssessSchemaFor(schema, process.env.CLEAROTRON_BAND_RUN_DIR),
|
|
1137
1140
|
description:
|
|
1138
1141
|
"Hand back THIS CHUNK's rated assessment as VALUES. The driver serializes knockout-assess-<n>.json, " +
|
|
1139
1142
|
"so you never format JSON and never write the file. WHICH CHUNK IS NOT YOURS TO SAY — the driver " +
|
|
@@ -1392,7 +1395,7 @@ serve({
|
|
|
1392
1395
|
},
|
|
1393
1396
|
batch: {
|
|
1394
1397
|
type: "object",
|
|
1395
|
-
required: ["productContext", "inUseAs",
|
|
1398
|
+
required: ["productContext", "inUseAs"], // places are per name (ruling 570): the batch list is optional, as in the acceptor and the manual
|
|
1396
1399
|
properties: {
|
|
1397
1400
|
productContext: { type: "string", description: "One sentence: what the batch is for. Every mark's contextFraming is read against it." },
|
|
1398
1401
|
inUseAs: { type: "string", description: "The kinds of use off the register that could conflict in this client's field, as one phrase completing \"Is this name already in use as …?\". The places the batch is searched on include the sites where these uses are listed." },
|
|
@@ -1439,3 +1442,27 @@ serve({
|
|
|
1439
1442
|
handler: record_knockout_frame,
|
|
1440
1443
|
}],
|
|
1441
1444
|
});
|
|
1445
|
+
|
|
1446
|
+
/**
|
|
1447
|
+
* The knockout save tool's schema for the run this server serves. The framework's `inputs` fields are
|
|
1448
|
+
* offered only where the run's framework states a method, which is when its frozen copy
|
|
1449
|
+
* (`_driver/framework-method.json`) exists: a run with no method has none, since the mint removes it. A
|
|
1450
|
+
* session offered a field it may not use can fill it, and the check that refuses it runs only after the
|
|
1451
|
+
* whole step. With no run to read, the schema is the one declared. Read when the tools are listed.
|
|
1452
|
+
*/
|
|
1453
|
+
export function knockoutAssessSchemaFor(schema, runDir) {
|
|
1454
|
+
if (!runDir || existsSync(driverDir(String(runDir), FROZEN_METHOD_FILE))) return schema;
|
|
1455
|
+
const strip = (s) => {
|
|
1456
|
+
if (Array.isArray(s)) return s.map(strip);
|
|
1457
|
+
if (!s || typeof s !== "object") return s;
|
|
1458
|
+
const out = {};
|
|
1459
|
+
for (const [k, v] of Object.entries(s)) {
|
|
1460
|
+
if (k === "properties" && v && typeof v === "object" && !Array.isArray(v)) {
|
|
1461
|
+
out.properties = Object.fromEntries(Object.entries(v).filter(([name]) => name !== "inputs").map(([name, sub]) => [name, strip(sub)]));
|
|
1462
|
+
} else if (k === "required" && Array.isArray(v)) out.required = v.filter((name) => name !== "inputs");
|
|
1463
|
+
else out[k] = strip(v);
|
|
1464
|
+
}
|
|
1465
|
+
return out;
|
|
1466
|
+
};
|
|
1467
|
+
return strip(schema);
|
|
1468
|
+
}
|
|
@@ -115,8 +115,11 @@ export function serve({ name, version = "0.1.0", tools = [] }) {
|
|
|
115
115
|
// it must be read-only. Nothing here needs to claim a write is a read to get called, and a
|
|
116
116
|
// `readOnlyHint: true` on a tool that writes a ledger would be a lie told to a sandbox for
|
|
117
117
|
// convenience — the exact thing annotations exist to prevent.
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
// Listed when asked, not at start: a tool may give the schema for the run it serves (`schemaNow`), read
|
|
119
|
+
// at call time like every per-run read here. A tool without one lists its declared schema, as before.
|
|
120
|
+
const list = () => tools.map((t) => ({
|
|
121
|
+
name: t.name, description: t.description,
|
|
122
|
+
inputSchema: typeof t.schemaNow === "function" ? t.schemaNow(t.inputSchema) : t.inputSchema,
|
|
120
123
|
...(t.annotations ? { annotations: t.annotations } : {}),
|
|
121
124
|
}));
|
|
122
125
|
const send = (m) => process.stdout.write(JSON.stringify(m) + "\n");
|
|
@@ -137,7 +140,7 @@ export function serve({ name, version = "0.1.0", tools = [] }) {
|
|
|
137
140
|
} else if (method === "ping") {
|
|
138
141
|
ok(id, {});
|
|
139
142
|
} else if (method === "tools/list") {
|
|
140
|
-
ok(id, { tools: list });
|
|
143
|
+
ok(id, { tools: list() });
|
|
141
144
|
} else if (method === "tools/call") {
|
|
142
145
|
const tool = byName.get(params?.name);
|
|
143
146
|
if (!tool) return ok(id, { isError: true, content: [{ type: "text", text: `unknown tool: ${params?.name}` }] });
|
|
@@ -167,7 +170,7 @@ export function serve({ name, version = "0.1.0", tools = [] }) {
|
|
|
167
170
|
// interpolate a mark, and this row's own contract is the call and never its content. A row that
|
|
168
171
|
// copied the message would leak the day one of them does; a row that copies `unmet` cannot.
|
|
169
172
|
logToolEvent({ event: "settled", seq, server: name, tool: tool.name, ...(axis ? { axis } : {}), ok: false,
|
|
170
|
-
reason: `missing_required:${unmet.join(",")}` });
|
|
173
|
+
failure: "schema", reason: `missing_required:${unmet.join(",")}` });
|
|
171
174
|
return ok(id, { isError: true, content: [{ type: "text", text:
|
|
172
175
|
`${tool.name}_missing_required:${unmet.join(",")} — the schema you were handed declares `
|
|
173
176
|
+ `${unmet.length === 1 ? "this field" : "these fields"} REQUIRED and the call did not carry `
|
|
@@ -177,13 +180,19 @@ export function serve({ name, version = "0.1.0", tools = [] }) {
|
|
|
177
180
|
try { out = await tool.handler(params?.arguments || {}); }
|
|
178
181
|
catch (e) {
|
|
179
182
|
// A THROWN tool error SETTLES. It returned an answer — a wrong one, but the model saw it and
|
|
180
|
-
// could act on it. Recording it as unsettled would attribute a live failure to a timeout.
|
|
181
|
-
|
|
183
|
+
// could act on it. Recording it as unsettled would attribute a live failure to a timeout. The
|
|
184
|
+
// row names the error's class and never its message, which can carry whatever the tool was given.
|
|
185
|
+
logToolEvent({ event: "settled", seq, server: name, tool: tool.name, ...(axis ? { axis } : {}), ok: false,
|
|
186
|
+
failure: "threw", reason: `threw:${errorClass(e)}` });
|
|
182
187
|
return ok(id, { isError: true, content: [{ type: "text", text: `${tool.name} error: ${e?.message ?? e}` }] });
|
|
183
188
|
}
|
|
184
189
|
const isError = typeof out === "object" && out?.isError === true;
|
|
185
190
|
const text = typeof out === "string" ? out : (out?.text ?? JSON.stringify(out));
|
|
186
|
-
|
|
191
|
+
// A tool that answered with an error rejected the call itself, after the schema let it past. The row
|
|
192
|
+
// says so, and carries the complaint's own code when the text opens with one, never the text.
|
|
193
|
+
const code = isError ? complaintCode(text) : null;
|
|
194
|
+
logToolEvent({ event: "settled", seq, server: name, tool: tool.name, ...(axis ? { axis } : {}), ok: !isError,
|
|
195
|
+
...(isError ? { failure: "server", ...(code ? { reason: code } : {}) } : {}) });
|
|
187
196
|
ok(id, { isError: isError || undefined, content: [{ type: "text", text }] });
|
|
188
197
|
} else if (id != null) {
|
|
189
198
|
err(id, -32601, `method not found: ${method}`);
|
|
@@ -205,3 +214,32 @@ export function serve({ name, version = "0.1.0", tools = [] }) {
|
|
|
205
214
|
// so `claude --mcp-config` reports a connected server whose tool calls return a clear isError.
|
|
206
215
|
process.on("uncaughtException", (e) => { process.stderr.write(`[mcp:${name}] ${e?.stack ?? e}\n`); });
|
|
207
216
|
}
|
|
217
|
+
|
|
218
|
+
// ── WHAT REFUSED A CALL, as the settled row names it ───────────────────────────────────────────────────
|
|
219
|
+
//
|
|
220
|
+
// (At the end of this file so that adding it moved no line another file cites.)
|
|
221
|
+
//
|
|
222
|
+
// A settled row with `ok: false` carries `failure`, one of three words for where the call stopped:
|
|
223
|
+
// schema the call did not carry a field the tool's schema requires; refused before the tool ran
|
|
224
|
+
// threw the tool ran and threw
|
|
225
|
+
// server the tool ran and answered with an error of its own
|
|
226
|
+
// So a call refused before its tool ever ran is told apart from one the tool rejected without knowing
|
|
227
|
+
// that the first leaves no call file behind. `reason` is never the message: a message can quote what the
|
|
228
|
+
// tool was given. It is the missing fields, the error's class, or the complaint's own code.
|
|
229
|
+
|
|
230
|
+
/** An error's class name, never its message. PURE. */
|
|
231
|
+
export function errorClass(e) {
|
|
232
|
+
const n = typeof e?.name === "string" && /^[A-Za-z][A-Za-z0-9]*$/.test(e.name) ? e.name : "";
|
|
233
|
+
return n || "Error";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The code a tool's complaint opens with, as the acceptors write them (`knockoutframe_classes_plain: …`,
|
|
238
|
+
* `judgment_record_not_held:1:…`): a lower-case word with at least one underscore, read up to the first
|
|
239
|
+
* character that is not part of it. `null` when the text opens any other way, so prose never reaches the
|
|
240
|
+
* row. PURE.
|
|
241
|
+
*/
|
|
242
|
+
export function complaintCode(text) {
|
|
243
|
+
const m = /^([a-z][a-z0-9]*(?:_[a-z0-9]+)+)(?=$|[^a-z0-9_])/.exec(String(text ?? ""));
|
|
244
|
+
return m ? m[1] : null;
|
|
245
|
+
}
|