@muretai/agent-entry 1.10.0 → 1.11.0

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.
@@ -0,0 +1,10 @@
1
+ {
2
+ "comment": "First dojo: a booking desk that cannot answer a book request without a day. Gold is the fixture responder (same policy as examples/server.mjs). Placeholders only.",
3
+ "cases": [
4
+ { "id": "book-no-day", "split": "train", "intent": "book", "naive_text": "I want to book a shoot" },
5
+ { "id": "book-saturday", "split": "train", "intent": "book", "naive_text": "Book Saturday 14:00" },
6
+ { "id": "price", "split": "train", "intent": "price", "naive_text": "What does a 60-minute shoot cost?" },
7
+ { "id": "book-holdout", "split": "holdout", "intent": "book", "naive_text": "Can I reserve a session" },
8
+ { "id": "price-holdout", "split": "holdout", "intent": "price", "naive_text": "How much is a half-day?" }
9
+ ]
10
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Local AE distill helpers. The Distiller never imports the door runtime.
3
+ * Gold labels come from the fixture responder (the same policy the example
4
+ * studio already implements: a book request without a day is not yet useful).
5
+ */
6
+ import { readFileSync, existsSync } from 'node:fs';
7
+ import { dirname, join } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const HERE = dirname(fileURLToPath(import.meta.url));
11
+ const DAY = /\b(mon|tue|wed|thu|fri|sat|sun|monday|tuesday|wednesday|thursday|friday|saturday|sunday|20\d{2}-\d{2}-\d{2})\b/i;
12
+ const PRICE = /\b(price|cost|how much|jpy|half-day|60-minute)\b/i;
13
+
14
+ export function loadCases() {
15
+ return JSON.parse(readFileSync(join(HERE, 'fixtures.json'), 'utf8')).cases;
16
+ }
17
+
18
+ /** Environment: what this desk actually answers. */
19
+ export function respond(text) {
20
+ const t = String(text || '');
21
+ if (PRICE.test(t)) {
22
+ return { tag: 'price', useful: true, text: 'A 60-minute shoot is 12000 JPY, two people included.' };
23
+ }
24
+ if (DAY.test(t)) {
25
+ return { tag: 'open', useful: true, text: 'Saturday 14:00 is open. 12000 JPY for a 60-minute shoot.' };
26
+ }
27
+ return { tag: 'ask_day', useful: false, text: 'Which day? Name a weekday or YYYY-MM-DD.' };
28
+ }
29
+
30
+ export function naiveText(case_) {
31
+ return case_.naive_text;
32
+ }
33
+
34
+ export function inferRules(traces) {
35
+ const rules = {
36
+ empty: traces.length === 0,
37
+ book_require_day: false,
38
+ examples: [],
39
+ evidence: [],
40
+ };
41
+ for (const tr of traces) {
42
+ rules.evidence.push({ id: tr.id, intent: tr.intent, useful: tr.environment_useful, tag: tr.tag });
43
+ if (tr.intent === 'book' && !tr.environment_useful && tr.tag === 'ask_day') {
44
+ rules.book_require_day = true;
45
+ }
46
+ if (tr.environment_useful && tr.intent === 'book' && DAY.test(tr.text)) {
47
+ rules.examples.push(tr.text);
48
+ }
49
+ if (tr.environment_useful && tr.intent === 'price') {
50
+ rules.examples.push(tr.text);
51
+ }
52
+ }
53
+ rules.examples = [...new Set(rules.examples)];
54
+ return rules;
55
+ }
56
+
57
+ export function applyText(case_, rules) {
58
+ if (rules.empty) return naiveText(case_);
59
+ let text = naiveText(case_);
60
+ if (rules.book_require_day && case_.intent === 'book' && !DAY.test(text)) {
61
+ const ex = rules.examples.find((e) => DAY.test(e));
62
+ text = ex || `${text} Saturday`;
63
+ }
64
+ return text;
65
+ }
66
+
67
+ export function tracesFromCases(cases, split = 'train') {
68
+ return cases.filter((c) => c.split === split).map((c) => {
69
+ const text = naiveText(c);
70
+ const env = respond(text);
71
+ return {
72
+ id: c.id,
73
+ intent: c.intent,
74
+ text,
75
+ tag: env.tag,
76
+ environment_useful: env.useful,
77
+ };
78
+ });
79
+ }
80
+
81
+ export function proposedSkills(rules) {
82
+ if (rules.empty) return [];
83
+ const examples = rules.examples.length
84
+ ? rules.examples
85
+ : (rules.book_require_day ? ['Book Saturday 14:00'] : []);
86
+ return [{
87
+ id: 'ask',
88
+ name: 'signed-answers-about-the-studio',
89
+ description: rules.book_require_day
90
+ ? 'Ask what a shoot costs, and book by naming a day. The answer comes back signed.'
91
+ : 'Ask what a shoot costs and how to book. The answer comes back signed.',
92
+ tags: ['studio', 'booking', 'signed', 'inline-reply'],
93
+ examples,
94
+ }];
95
+ }
96
+
97
+ export function readJsonl(path) {
98
+ if (!existsSync(path)) return [];
99
+ return readFileSync(path, 'utf8').split('\n').filter(Boolean).map((line) => JSON.parse(line));
100
+ }
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Local loop: distill fixtures (+ optional var/traces.jsonl) → measure → mutation.
4
+ * --m0 also runs the door conformance suite. Never uploads. Never edits the card.
5
+ */
6
+ import { spawnSync } from 'node:child_process';
7
+ import { dirname, resolve } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const HERE = dirname(fileURLToPath(import.meta.url));
11
+ const ROOT = resolve(HERE, '../..');
12
+ const runM0 = process.argv.includes('--m0');
13
+
14
+ function run(file, extra = []) {
15
+ const r = spawnSync(process.execPath, [resolve(HERE, file), ...extra], {
16
+ cwd: ROOT,
17
+ encoding: 'utf8',
18
+ stdio: ['ignore', 'pipe', 'pipe'],
19
+ });
20
+ if (r.stdout) process.stdout.write(r.stdout);
21
+ if (r.stderr) process.stderr.write(r.stderr);
22
+ if (r.status !== 0) process.exit(r.status ?? 1);
23
+ }
24
+
25
+ run('distill.mjs');
26
+ run('measure.mjs');
27
+ process.stdout.write('\n--- mutation (empty Distiller) ---\n');
28
+ run('distill.mjs', ['--empty']);
29
+ run('measure.mjs', ['--empty']);
30
+ run('distill.mjs');
31
+
32
+ const unit = spawnSync(process.execPath, ['--test', resolve(HERE, 'test.mjs')], {
33
+ cwd: ROOT,
34
+ encoding: 'utf8',
35
+ });
36
+ if (unit.stdout) process.stdout.write(unit.stdout);
37
+ if (unit.stderr) process.stderr.write(unit.stderr);
38
+ if (unit.status !== 0) process.exit(unit.status ?? 1);
39
+
40
+ if (runM0) {
41
+ process.stdout.write('\n--- M0 npm test (conformance) ---\n');
42
+ const t = spawnSync('npm', ['test'], { cwd: ROOT, encoding: 'utf8' });
43
+ if (t.stdout) process.stdout.write(t.stdout);
44
+ if (t.stderr) process.stderr.write(t.stderr);
45
+ if (t.status !== 0) process.exit(t.status ?? 1);
46
+ }
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * M2: held-out visitor prompts. Naive sends the prompt as-is.
4
+ * Skill rewrites a book request that names no day using a distilled example.
5
+ * Gold = fixture responder. Mutation: --empty → apply == naive, lift 0.
6
+ */
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { dirname, resolve } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { applyText, loadCases, naiveText, respond } from './lib.mjs';
11
+
12
+ const HERE = dirname(fileURLToPath(import.meta.url));
13
+ const ROOT = resolve(HERE, '../..');
14
+ const OUT = resolve(ROOT, 'generated');
15
+
16
+ function loadRules(empty) {
17
+ if (empty) return { empty: true, examples: [] };
18
+ const p = resolve(OUT, 'rules.json');
19
+ if (!existsSync(p)) {
20
+ process.stderr.write('measure.mjs: run distill.mjs first (no generated/rules.json)\n');
21
+ process.exit(1);
22
+ }
23
+ return JSON.parse(readFileSync(p, 'utf8'));
24
+ }
25
+
26
+ function score(cases, textOf) {
27
+ const rows = [];
28
+ let ok = 0;
29
+ for (const c of cases) {
30
+ const text = textOf(c);
31
+ const env = respond(text);
32
+ if (env.useful) ok += 1;
33
+ rows.push({ id: c.id, intent: c.intent, text, useful: env.useful, tag: env.tag });
34
+ }
35
+ return { ok, n: cases.length, pct: cases.length ? ok / cases.length : 0, rows };
36
+ }
37
+
38
+ const empty = process.argv.includes('--empty');
39
+ const rules = loadRules(empty);
40
+ const holdout = loadCases().filter((c) => c.split === 'holdout');
41
+ const without = score(holdout, naiveText);
42
+ const withSkill = score(holdout, (c) => applyText(c, rules));
43
+ const lift = withSkill.pct - without.pct;
44
+
45
+ const lines = [
46
+ '# Agent Entry — local distill measure',
47
+ '',
48
+ empty ? 'Arm: **empty Distiller** (mutation).' : 'Arm: distilled `generated/rules.json`.',
49
+ '',
50
+ `| arm | holdout first-knock useful |`,
51
+ `|---|---|`,
52
+ `| without skill (send the prompt) | ${without.ok}/${without.n} (${(without.pct * 100).toFixed(0)}%) |`,
53
+ `| with skill | ${withSkill.ok}/${withSkill.n} (${(withSkill.pct * 100).toFixed(0)}%) |`,
54
+ `| lift | ${(lift * 100).toFixed(0)} pt |`,
55
+ '',
56
+ 'Gold is the fixture responder, not a transcript judge.',
57
+ '',
58
+ '### holdout',
59
+ '',
60
+ ...withSkill.rows.map((r) => {
61
+ const w = without.rows.find((x) => x.id === r.id);
62
+ return `- \`${r.id}\` naive="${w.text}" → ${w.tag} | skill="${r.text}" → ${r.tag} ${r.useful ? 'ok' : 'MISS'}`;
63
+ }),
64
+ '',
65
+ 'Proposed menu: generated/skills.json — not written onto the running card.',
66
+ '',
67
+ ];
68
+
69
+ mkdirSync(OUT, { recursive: true });
70
+ writeFileSync(resolve(OUT, 'report.md'), lines.join('\n'));
71
+ process.stdout.write(lines.join('\n'));
72
+
73
+ if (empty && lift !== 0) {
74
+ process.stderr.write('mutation failed: empty Distiller still produced lift\n');
75
+ process.exit(2);
76
+ }
77
+ if (!empty && lift <= 0) {
78
+ process.stderr.write('no lift on holdout — do not publish the proposed skills[]\n');
79
+ process.exit(2);
80
+ }
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Owner-local observer sink. Writes to a path on THIS machine. Never uploads.
4
+ *
5
+ * Wire it when you construct the door:
6
+ *
7
+ * import { fileSink } from './scripts/distill/record.mjs';
8
+ * createAgentEntry({ ..., observer: fileSink() });
9
+ *
10
+ * CLI: print the snippet, or append a JSON row from stdin.
11
+ *
12
+ * node scripts/distill/record.mjs
13
+ * node scripts/distill/record.mjs --file var/traces.jsonl --stdin < row.json
14
+ */
15
+ import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
16
+ import { dirname, resolve } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const HERE = dirname(fileURLToPath(import.meta.url));
20
+ const ROOT = resolve(HERE, '../..');
21
+
22
+ export function defaultTracePath() {
23
+ return process.env.AE_TRACES || resolve(ROOT, 'var/traces.jsonl');
24
+ }
25
+
26
+ /**
27
+ * Observer that cannot change a verdict (the door already discards its return).
28
+ * Drops card/notice GETs. Never writes a DID.
29
+ */
30
+ export function fileSink(path = defaultTracePath()) {
31
+ return (env) => {
32
+ try {
33
+ if (!env || env.stage === 'card_get' || env.stage === 'notice_get') return;
34
+ const row = {
35
+ ts: new Date().toISOString(),
36
+ text: typeof env.text === 'string' ? env.text : '',
37
+ verified: Boolean(env.verified),
38
+ stage: env.stage || null,
39
+ refuse_code: env.refuse_code ?? null,
40
+ };
41
+ mkdirSync(dirname(path), { recursive: true });
42
+ appendFileSync(path, JSON.stringify(row) + '\n');
43
+ } catch {
44
+ // Observer contract: a throw is swallowed by the door; we swallow here too.
45
+ }
46
+ };
47
+ }
48
+
49
+ function usage(code = 0) {
50
+ process.stdout.write(
51
+ 'Local AE traces — this machine only, never uploaded.\n'
52
+ + '\n'
53
+ + ' import { fileSink } from \'./scripts/distill/record.mjs\';\n'
54
+ + ' createAgentEntry({ seedHex, name, baseUrl, responder, observer: fileSink() });\n'
55
+ + '\n'
56
+ + ' node scripts/distill/record.mjs --stdin < row.json\n'
57
+ + ` default file: ${defaultTracePath()}\n`,
58
+ );
59
+ process.exit(code);
60
+ }
61
+
62
+ if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('record.mjs')) {
63
+ const argv = process.argv.slice(2);
64
+ if (!argv.length || argv.includes('--help') || argv.includes('-h')) usage(0);
65
+ let file = defaultTracePath();
66
+ let stdin = false;
67
+ for (let i = 0; i < argv.length; i++) {
68
+ if (argv[i] === '--file') { file = resolve(argv[++i] || ''); continue; }
69
+ if (argv[i] === '--stdin') { stdin = true; continue; }
70
+ usage(1);
71
+ }
72
+ if (!stdin) usage(1);
73
+ const raw = readFileSync(0, 'utf8');
74
+ if (!raw.trim()) {
75
+ process.stderr.write('record.mjs: stdin is empty\n');
76
+ process.exit(1);
77
+ }
78
+ let doc;
79
+ try { doc = JSON.parse(raw); } catch (e) {
80
+ process.stderr.write(`record.mjs: stdin is not JSON: ${e.message}\n`);
81
+ process.exit(1);
82
+ }
83
+ fileSink(file)({
84
+ text: doc.text || '',
85
+ verified: Boolean(doc.verified),
86
+ stage: doc.stage || 'signed_post',
87
+ refuse_code: doc.refuse_code ?? null,
88
+ });
89
+ process.stdout.write(`recorded 1 row → ${file}\n`);
90
+ }
@@ -0,0 +1,43 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { applyText, inferRules, loadCases, naiveText, proposedSkills, respond, tracesFromCases } from './lib.mjs';
4
+
5
+ function score(cases, textOf) {
6
+ let ok = 0;
7
+ for (const c of cases) {
8
+ if (respond(textOf(c)).useful) ok += 1;
9
+ }
10
+ return cases.length ? ok / cases.length : 0;
11
+ }
12
+
13
+ test('responder asks for a day when the book prompt names none', () => {
14
+ assert.equal(respond('I want to book a shoot').useful, false);
15
+ assert.equal(respond('I want to book a shoot').tag, 'ask_day');
16
+ assert.equal(respond('Book Saturday 14:00').useful, true);
17
+ });
18
+
19
+ test('distilled menu beats sending the prompt as-is on holdout', () => {
20
+ const cases = loadCases();
21
+ const rules = inferRules(tracesFromCases(cases, 'train'));
22
+ const holdout = cases.filter((c) => c.split === 'holdout');
23
+ const without = score(holdout, naiveText);
24
+ const withSkill = score(holdout, (c) => applyText(c, rules));
25
+ assert.ok(withSkill > without, `expected lift, got without=${without} with=${withSkill}`);
26
+ assert.equal(withSkill, 1);
27
+ });
28
+
29
+ test('empty Distiller produces no lift (producer mutation)', () => {
30
+ const holdout = loadCases().filter((c) => c.split === 'holdout');
31
+ const without = score(holdout, naiveText);
32
+ const withEmpty = score(holdout, (c) => applyText(c, { empty: true }));
33
+ assert.equal(withEmpty, without);
34
+ });
35
+
36
+ test('proposed skills[] examples are answerable by the responder', () => {
37
+ const rules = inferRules(tracesFromCases(loadCases(), 'train'));
38
+ const skills = proposedSkills(rules);
39
+ assert.ok(skills.length >= 1);
40
+ for (const ex of skills[0].examples) {
41
+ assert.equal(respond(ex).useful, true, `example not answerable: ${ex}`);
42
+ }
43
+ });
@@ -0,0 +1,219 @@
1
+ # Agent Entry — Episode-to-Skill (site side)
2
+
3
+ Status: **local loop shipped** (`scripts/distill/`). Does not change the v1
4
+ HTTP contract (`spec/v1.md`).
5
+ Method: Repo-To-Skill (arXiv 2609.02749) — distill, verify, load only what
6
+ the task needs. Source: outcome-labeled knocks at **this** door, not a
7
+ GitHub crawl and not a network-wide catalog.
8
+
9
+ This repository stays the door. Distillation is an **owner-side, offline**
10
+ loop. Nothing here is added to `muretai-agent-entry.mjs` at request time.
11
+
12
+ ---
13
+
14
+ ## 1. What is missing today
15
+
16
+ A visiting agent reads the card before it knocks. If `skills[]` is empty or
17
+ wrong, it guesses and learns the menu from refusals. The README already
18
+ states the two owner duties:
19
+
20
+ - every `examples[]` entry must be answerable by the responder
21
+ - declare only what the responder actually does
22
+
23
+ Those duties are still hand-written. The paper's claim is that this
24
+ operating knowledge already exists in **grounded outcomes** — signed
25
+ envelopes plus what the door did — and can be distilled into the menu the
26
+ next visitor sees.
27
+
28
+ The door already emits the labels, without changing a verdict:
29
+
30
+ - `observer(env)` runs **after** the reply; its return is discarded
31
+ - `stats()` / `clientStats()` count family and stage, including refusals
32
+ - JSON-RPC refusals teach (`-32004` over-rate, unsigned, replay, wrong
33
+ recipient); the recipe already rides in the refusal
34
+ - the responder's own result (answered / asked for a missing field /
35
+ handed off) is the site-specific outcome
36
+
37
+ User-Agent never affects `verified`, a ledger row, a rate lane, or any
38
+ refusal. Distillation must not reopen that. UA is a counting hint, not a
39
+ training label for "who to trust".
40
+
41
+ ---
42
+
43
+ ## 2. What to steal, what to refuse
44
+
45
+ Steal: four-stage distillation (scope → ground → construct → verify);
46
+ skills as operating context, not a new control loop; environment-grounded
47
+ admission (a transcript-only gate cannot improve uniformly); withhold-the-
48
+ skill measurement.
49
+
50
+ Refuse:
51
+
52
+ - Distiller inside the one-file runtime (zero dependencies, no database)
53
+ - a hosted catalog of every site's skills
54
+ - FOLLOW of visitor-authored text (`env.text` stays untrusted data)
55
+ - stamping a third-party `howToUrl` (empty means omitted; only a URL the
56
+ owner operates)
57
+ - changing any wire byte or verdict because a skill exists
58
+
59
+ ---
60
+
61
+ ## 3. Mapping
62
+
63
+ | Paper | This repo |
64
+ |---|---|
65
+ | Declarative source | Signed inbound `text` + door outcome (verify / refuse code / responder result) |
66
+ | Skill | A2A `skills[]` on the **signed** card; optional owner `howToUrl`; optional `SKILL.md` the owner hosts |
67
+ | Skill graph | One entry skill (what this door answers) → component skills per declared capability |
68
+ | Router | The card itself. Visitors read `skills[]` before POST. Agent Web Router finds the door; it does not write this menu |
69
+ | Verification | Card signature still covers the menu. Examples are driven through the responder in tests (already required) |
70
+ | Creator / researcher | Owner Distiller writes a proposed menu; the running entry only **serves** what the owner published |
71
+
72
+ ```
73
+ visitor --probe--> card.skills[] (menu; signed)
74
+ visitor --knock--> POST door (unchanged contract)
75
+ observer --trace--> local log (after verdict)
76
+ Distiller (offline) --> proposed skills[] / how-to
77
+ owner publishes --> card resigns
78
+ ```
79
+
80
+ ---
81
+
82
+ ## 4. Distiller pipeline (offline, this repo or a sibling package)
83
+
84
+ Anchor is this origin's door, not a task on the open web.
85
+
86
+ 1. **Scope.** One capability the responder already implements (hours,
87
+ quote, book, …). Do not invent a skill the responder cannot keep.
88
+ 2. **Ground.** Read owner-local traces only: `observer` rows the owner
89
+ chose to keep, plus the door's refuse reason. A row is
90
+ `{text, verified, refuse_code?, responder_tag, ts}`. No operator
91
+ aggregation across sites.
92
+ 3. **Construct.** Propose:
93
+ - `skills[]` id / name / description / tags / **answerable** examples
94
+ - optional how-to prose at a URL the owner operates
95
+ - construction record `R`: evidence (trace ids), checks, remaining gaps
96
+ 4. **Verify (M0).** Before publish:
97
+ - the new card still signs
98
+ - every example POSTed to the local entry gets a signed reply the
99
+ visitor could accept (existing "examples are promises" rule)
100
+ - refuse rate on a frozen visitor suite does not rise
101
+ - remaining gaps stay in `R`
102
+
103
+ The running entry never imports the Distiller. Shipped layout (not
104
+ imported by the one-file runtime):
105
+
106
+ ```
107
+ scripts/distill/ # owner CLI
108
+ record.mjs # fileSink() for observer → var/traces.jsonl
109
+ distill.mjs # traces → generated/skills.json
110
+ measure.mjs # M2 with/without the proposed menu
111
+ loop.mjs # distill → measure → mutation
112
+ ```
113
+
114
+ `npm run distill`. Proposed `skills[]` is never written onto the running
115
+ card. `--m0` also runs the door conformance suite.
116
+
117
+ `createAgentEntry({ observer })` already exists. Recording is an owner
118
+ choice. Default remains no disk.
119
+
120
+ ---
121
+
122
+ ## 5. Measurement
123
+
124
+ A skill cannot be graded by reading it (ACES, arXiv 2608.20614).
125
+
126
+ - **M0 admission** — signed card; examples answerable; contract suite
127
+ (`conformance/`) still green. Fail-closed.
128
+ - **M2 lift** — freeze responder, rates, and a held-out visitor prompt
129
+ set. Arm A: current `skills[]` / how-to. Arm B: proposed menu. Score
130
+ only what a visitor observes: first-knock useful answer, refuse-for-
131
+ missing-field, handoff that the card still names. Not token count.
132
+ - **Producer mutation** — Distiller emits empty `skills[]`. Lift must
133
+ fall to ~0. If it stays green, the test was scoring the responder.
134
+ - **No leaderboard.** One site, that site's traces, that site's reader.
135
+
136
+ Publish rule: M0 holds, M2 first-knock success rises or refuse-for-guess
137
+ falls, mutation kills the lift.
138
+
139
+ ---
140
+
141
+ ## 6. First dojo (this repo, no production traffic)
142
+
143
+ Frozen visitor prompts against `examples/server.mjs` (or a fixture
144
+ responder) that requires a missing field the naive visitor omits.
145
+
146
+ - Train traces: naive knocks that the responder rejects with a teachable
147
+ reason, plus one complete knock.
148
+ - Distiller writes a skill example that includes the field.
149
+ - Holdout: a new wording of the same need.
150
+ - Naive visitor still omits the field; skill-equipped visitor includes it.
151
+ - Mutation: empty Distiller, both arms omit, lift 0.
152
+
153
+ Do not use live `muretai.com` traffic for the first number.
154
+
155
+ ---
156
+
157
+ ## 7. What this does not change
158
+
159
+ v1 HTTP surface, Ed25519 envelope, account-from-first-signature, rate
160
+ lanes, Web Bot Auth as recognition-only, `Link` signpost, path mounts,
161
+ `domains`. Agent Web Router remains a **visitor** of this door. It may
162
+ read the distilled menu; it must not write it.
163
+
164
+ ## 8. Relation to `muretai-skill-distill`
165
+
166
+ That sibling app proved withhold-the-skill lift on a fail-closed parse
167
+ dojo. This design is the same method with a different source: **door
168
+ outcomes**, not `x-rlds` lines. Do not vendor-copy core crypto. The entry
169
+ already verifies signatures.
170
+
171
+ ---
172
+
173
+ ## 9. Live loop — two rails (we never see installer traffic)
174
+
175
+ This package is installed by other people, on origins we do not operate.
176
+ Their visitors' messages, accounts, and `observer` rows are **not ours**
177
+ and MUST NOT be fetched, phoned home, or scraped. There is no telemetry
178
+ channel. Evolution therefore splits.
179
+
180
+ ### Rail A — this package (what we can update)
181
+
182
+ We improve the **door machinery and the refusal recipe**, not a site's
183
+ menu. Labels come only from sources we already own or that someone
184
+ chose to publish:
185
+
186
+ - `conformance/` vectors and the contract suite (attacks we author)
187
+ - the doors **we** run (for example the muretai.com entry)
188
+ - a GitHub issue a site owner **opts into**, attaching a redacted
189
+ fixture they exported (`export --redact` if later built; default off,
190
+ never runs itself)
191
+
192
+ A package release may change refusal text, `howTo` defaults (still no
193
+ third-party host), rate-lane behaviour, or new contract tests. It MUST
194
+ NOT patch a stranger's `skills[]`. That list is their signed claim.
195
+
196
+ Measure a release against **our** fixtures (§5). If we do not have a
197
+ new fixture, we do not have a new skill.
198
+
199
+ ### Rail B — each installer's machine (what they can update)
200
+
201
+ The only place their knock data exists is their process. The Distiller,
202
+ if they run it, reads a local `observer` sink they configured. The
203
+ child `skills[]` stays on their card. We never receive it.
204
+
205
+ ```
206
+ their observer → their disk → their Distiller → their next card
207
+ ```
208
+
209
+ `stats()` they already have is for **them**. It is not a feed to us.
210
+ User-Agent remains unusable as a label.
211
+
212
+ ### What we do when we cannot see production
213
+
214
+ We do not wait for it. The first dojo (§6) and every later package
215
+ change are fixture-grown. When a user reports "visitors keep getting
216
+ refused and the recipe did not teach X", the artifact we want is a
217
+ **reproducing envelope**, not their ledger. That becomes a conformance
218
+ case. That is the only upstream ratchet that does not take their
219
+ customers.