instar 1.3.742 → 1.3.744

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,226 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lint-no-opus-claude-cli-gating.js — INSTAR-Bench v3, Task-4 S2 (bench rules R1/R2).
4
+ *
5
+ * Opus-via-Claude-Code-CLI is the one MEASURED-BANNED route for bounded/gating
6
+ * verdicts: identical Opus 4.8 scores 99.1% via clean API vs 81.7% via the Claude
7
+ * Code CLI (a 17.4-pt door penalty), and on the emergency-stop classifier it missed
8
+ * canonical STOP commands (73%). The Claude Code harness wraps every prompt in ~20k
9
+ * tokens of "helpful coding agent" framing, turning a skeptical judge into a
10
+ * credulous assistant. R1 forbids routing bounded verdicts through that door; R2
11
+ * forbids the emergency-stop classifier on it specifically.
12
+ *
13
+ * This lint enforces the ban STRUCTURALLY, in two prongs:
14
+ *
15
+ * PRONG A — the runtime guardrail must stay intact. The IntelligenceRouter clamps
16
+ * a bounded/gating failure-swap onto `claude-code` from the `capable` tier
17
+ * (=Opus) down to `balanced` (=Sonnet CLI reserve). If someone deletes that
18
+ * clamp, a swap could once again land Opus-via-CLI on a gate. The lint fails if
19
+ * the `clampClaudeCliSwapModel` guardrail is missing from src/core/
20
+ * IntelligenceRouter.ts or is no longer invoked in the swap loop. Structure >
21
+ * Willpower: the guard cannot be silently removed.
22
+ *
23
+ * PRONG B — no committed config statically routes a gating call to opus×claude-CLI.
24
+ * Scans committed JSON config for the dangerous COMBINATION: `frameworkDefaultModels
25
+ * ['claude-code']` set to an Opus model AND `claude-code` reachable as a gating
26
+ * route (listed in `componentFrameworks.failureSwap`, or set as the framework for
27
+ * the `sentinel`/`gate` categories, or a per-component override). Either alone is
28
+ * fine (Opus-CLI as the CHAIN WRITE quality lane is legitimate); the two together
29
+ * are the banned bounded-verdict route.
30
+ *
31
+ * Exit codes: 0 — clean; 1 — guardrail missing (A) or a dangerous config found (B).
32
+ *
33
+ * Usage:
34
+ * node scripts/lint-no-opus-claude-cli-gating.js
35
+ */
36
+
37
+ import fs from 'node:fs';
38
+ import path from 'node:path';
39
+ import { fileURLToPath } from 'node:url';
40
+
41
+ const __filename = fileURLToPath(import.meta.url);
42
+ const __dirname = path.dirname(__filename);
43
+ const ROOT = path.resolve(__dirname, '..');
44
+
45
+ const ROUTER_SRC = path.join(ROOT, 'src', 'core', 'IntelligenceRouter.ts');
46
+
47
+ /** Model ids / tier aliases that resolve to Opus on claude-code. */
48
+ const OPUS_MODELS = ['capable', 'opus', 'claude-opus'];
49
+
50
+ /**
51
+ * PRONG A — the router still contains AND invokes the R1/R2 clamp guardrail.
52
+ * @param {string} routerSrc
53
+ * @returns {string[]} problems (empty = OK)
54
+ */
55
+ export function checkGuardrailIntact(routerSrc) {
56
+ const problems = [];
57
+ // The helper must be DEFINED (exported for its unit test) …
58
+ if (!/export function clampClaudeCliSwapModel\b/.test(routerSrc)) {
59
+ problems.push(
60
+ 'src/core/IntelligenceRouter.ts: the R1/R2 guardrail `clampClaudeCliSwapModel` is missing — ' +
61
+ 'a bounded/gating failure-swap could once again resolve to Opus-via-Claude-CLI.',
62
+ );
63
+ }
64
+ // … AND invoked inside the swap path (a defined-but-unused helper guards nothing).
65
+ const invocations = (routerSrc.match(/clampClaudeCliSwapModel\s*\(/g) || []).length;
66
+ // One for the `export function` signature, at least one real call site.
67
+ if (invocations < 2) {
68
+ problems.push(
69
+ 'src/core/IntelligenceRouter.ts: `clampClaudeCliSwapModel` is defined but never invoked in the ' +
70
+ 'failure-swap loop — the R1/R2 clamp is inert. Wire it where the swap `attemptOptions` is built.',
71
+ );
72
+ }
73
+ return problems;
74
+ }
75
+
76
+ /**
77
+ * @param {unknown} model
78
+ * @returns {boolean} true if this model string resolves to Opus.
79
+ */
80
+ function isOpusModel(model) {
81
+ if (typeof model !== 'string') return false;
82
+ const m = model.toLowerCase();
83
+ return OPUS_MODELS.some((o) => m.includes(o));
84
+ }
85
+
86
+ /**
87
+ * Extract a `componentFrameworks`-shaped block from a parsed config object,
88
+ * checking both the top level and the `sessions` nesting used in .instar/config.json.
89
+ * @param {any} cfg
90
+ * @returns {{ componentFrameworks?: any, frameworkDefaultModels?: any } | null}
91
+ */
92
+ function extractRoutingConfig(cfg) {
93
+ if (!cfg || typeof cfg !== 'object') return null;
94
+ const sessions = cfg.sessions && typeof cfg.sessions === 'object' ? cfg.sessions : {};
95
+ const componentFrameworks = cfg.componentFrameworks ?? sessions.componentFrameworks;
96
+ const frameworkDefaultModels = cfg.frameworkDefaultModels ?? sessions.frameworkDefaultModels;
97
+ if (componentFrameworks === undefined && frameworkDefaultModels === undefined) return null;
98
+ return { componentFrameworks, frameworkDefaultModels };
99
+ }
100
+
101
+ /**
102
+ * PRONG B — is `claude-code` reachable as a GATING route in this componentFrameworks
103
+ * block? (failureSwap tail, or the framework for a gate/sentinel category, or an override.)
104
+ * @param {any} componentFrameworks
105
+ * @returns {boolean}
106
+ */
107
+ function claudeCodeIsGatingRoute(componentFrameworks) {
108
+ if (!componentFrameworks || typeof componentFrameworks !== 'object') return false;
109
+ const cf = componentFrameworks;
110
+ if (Array.isArray(cf.failureSwap) && cf.failureSwap.includes('claude-code')) return true;
111
+ if (cf.categories && typeof cf.categories === 'object') {
112
+ if (cf.categories.gate === 'claude-code' || cf.categories.sentinel === 'claude-code') return true;
113
+ }
114
+ if (cf.overrides && typeof cf.overrides === 'object') {
115
+ if (Object.values(cf.overrides).includes('claude-code')) return true;
116
+ }
117
+ if (cf.default === 'claude-code') return true;
118
+ return false;
119
+ }
120
+
121
+ /**
122
+ * PRONG B — evaluate one parsed config object. Returns a reason string if it routes
123
+ * a gating call to Opus×claude-CLI, else null.
124
+ * @param {any} cfg
125
+ * @returns {string | null}
126
+ */
127
+ export function checkConfigObject(cfg) {
128
+ const routing = extractRoutingConfig(cfg);
129
+ if (!routing) return null;
130
+ const claudeDefault = routing.frameworkDefaultModels?.['claude-code'];
131
+ if (!isOpusModel(claudeDefault)) return null; // claude-code default isn't Opus → safe
132
+ if (!claudeCodeIsGatingRoute(routing.componentFrameworks)) return null; // not a gating route → safe
133
+ return (
134
+ `frameworkDefaultModels['claude-code'] = '${claudeDefault}' (Opus) AND claude-code is reachable as a ` +
135
+ `gating route (failureSwap / gate|sentinel category / override) — this is the banned Opus×claude-CLI ` +
136
+ `bounded-verdict door (R1/R2). Use a non-Opus tier for the claude-code gating fallback (sonnet/haiku), ` +
137
+ `or remove claude-code from the gating route.`
138
+ );
139
+ }
140
+
141
+ /**
142
+ * Recursively collect committed JSON config files worth scanning. We only care
143
+ * about files that could carry a componentFrameworks block; scanning every JSON in
144
+ * the tree is wasteful and node_modules is excluded.
145
+ * @param {string} dir
146
+ * @param {string[]} acc
147
+ */
148
+ function collectConfigJson(dir, acc) {
149
+ let entries;
150
+ try {
151
+ entries = fs.readdirSync(dir, { withFileTypes: true });
152
+ } catch {
153
+ return;
154
+ }
155
+ for (const e of entries) {
156
+ if (e.name === 'node_modules' || e.name === '.git' || e.name === 'dist') continue;
157
+ const full = path.join(dir, e.name);
158
+ if (e.isDirectory()) {
159
+ collectConfigJson(full, acc);
160
+ } else if (e.isFile() && e.name.endsWith('.json')) {
161
+ acc.push(full);
162
+ }
163
+ }
164
+ }
165
+
166
+ /**
167
+ * @param {string[]} files
168
+ * @returns {{ file: string, reason: string }[]}
169
+ */
170
+ export function scanConfigFiles(files) {
171
+ const problems = [];
172
+ for (const file of files) {
173
+ let raw;
174
+ try {
175
+ raw = fs.readFileSync(file, 'utf8');
176
+ } catch {
177
+ continue;
178
+ }
179
+ // Cheap pre-filter: only parse files that mention componentFrameworks.
180
+ if (!raw.includes('componentFrameworks')) continue;
181
+ let parsed;
182
+ try {
183
+ parsed = JSON.parse(raw);
184
+ } catch {
185
+ continue; // not our concern — other lints/CI cover malformed JSON
186
+ }
187
+ const reason = checkConfigObject(parsed);
188
+ if (reason) problems.push({ file, reason });
189
+ }
190
+ return problems;
191
+ }
192
+
193
+ function main() {
194
+ const problems = [];
195
+
196
+ // PRONG A
197
+ let routerSrc = '';
198
+ try {
199
+ routerSrc = fs.readFileSync(ROUTER_SRC, 'utf8');
200
+ } catch (err) {
201
+ console.error(`lint-no-opus-claude-cli-gating: cannot read IntelligenceRouter.ts — ${err.message}`);
202
+ process.exit(1);
203
+ }
204
+ problems.push(...checkGuardrailIntact(routerSrc));
205
+
206
+ // PRONG B
207
+ const configFiles = [];
208
+ collectConfigJson(ROOT, configFiles);
209
+ const configProblems = scanConfigFiles(configFiles);
210
+ for (const p of configProblems) {
211
+ problems.push(`${path.relative(ROOT, p.file)}: ${p.reason}`);
212
+ }
213
+
214
+ if (problems.length === 0) {
215
+ console.log(
216
+ 'lint-no-opus-claude-cli-gating: OK — R1/R2 clamp intact; no config routes a gating call to Opus×claude-CLI.',
217
+ );
218
+ process.exit(0);
219
+ }
220
+ console.error('lint-no-opus-claude-cli-gating: FAILED (INSTAR-Bench v3 R1/R2):');
221
+ for (const p of problems) console.error(` - ${p}`);
222
+ process.exit(1);
223
+ }
224
+
225
+ const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === __filename;
226
+ if (isDirectRun) main();
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lint-routing-registry-freshness.js — the human intentional-defaults layer
4
+ * (docs/LLM-ROUTING-REGISTRY.md) must stay EXHAUSTIVE over the LLM callsite set.
5
+ *
6
+ * INSTAR-Bench v3, Task-4 Piece 3 (gap G2). The routing registry is the layer
7
+ * where a HUMAN records, per benched component, the intended default route +
8
+ * nature — the decision layer above `COMPONENT_CATEGORY` (which framework?) and
9
+ * `LLM_BENCH_COVERAGE` (which benchmark?). If a component exists in the category
10
+ * map but is ABSENT from the registry doc, its routing default was never
11
+ * intentionally decided — an unmeasured routing decision, the exact gap the
12
+ * benchmark discipline exists to close. This lint fails when that happens.
13
+ *
14
+ * WHAT IT CHECKS: every key of `COMPONENT_CATEGORY`
15
+ * (src/core/componentCategories.ts) must appear — as a literal substring — SOME-
16
+ * WHERE in docs/LLM-ROUTING-REGISTRY.md. Substring (not table-row) matching is
17
+ * deliberate: the doc legitimately groups aliases ("TaskClassifier /
18
+ * OverrideDetector / …") and annotates counts ("SessionActivitySentinel ×3"),
19
+ * so a strict per-row parse would false-positive. The invariant is presence, not
20
+ * shape — mirrors the shrink-only spirit of the bench-coverage ratchet.
21
+ *
22
+ * ALLOWLIST: seeded EMPTY. A component that genuinely has no registry row (e.g.
23
+ * a router-bypass callsite documented elsewhere) must be added here WITH A
24
+ * REASON — a visible, reviewed act, exactly like the bench-coverage exemptions.
25
+ * The companion ratchet test (tests/unit/routing-registry-freshness.test.ts)
26
+ * pins the same invariant in CI over the real imported symbol.
27
+ *
28
+ * Exit codes: 0 — every key present; 1 — at least one missing (or a stale
29
+ * allowlist entry: an allowlisted name that is now present fails with "remove me").
30
+ *
31
+ * Usage:
32
+ * node scripts/lint-routing-registry-freshness.js
33
+ */
34
+
35
+ import fs from 'node:fs';
36
+ import path from 'node:path';
37
+ import { fileURLToPath } from 'node:url';
38
+
39
+ const __filename = fileURLToPath(import.meta.url);
40
+ const __dirname = path.dirname(__filename);
41
+ const ROOT = path.resolve(__dirname, '..');
42
+
43
+ const CATEGORY_SRC = path.join(ROOT, 'src', 'core', 'componentCategories.ts');
44
+ const REGISTRY_DOC = path.join(ROOT, 'docs', 'LLM-ROUTING-REGISTRY.md');
45
+
46
+ /**
47
+ * Components with no registry row, each with a REASON. Seeded EMPTY (the doc is
48
+ * exhaustive today). Adding a name is a visible, reviewed act; an entry that is
49
+ * no longer missing fails this lint with "remove me".
50
+ * @type {Record<string,string>}
51
+ */
52
+ export const REGISTRY_FRESHNESS_ALLOWLIST = {};
53
+
54
+ /**
55
+ * Extract COMPONENT_CATEGORY keys from the TypeScript source by lexical scan
56
+ * (the house pattern for lint scripts — they run pre-compile, so they read
57
+ * source text, not the compiled module). Matches `Name: 'category'` and
58
+ * `'quoted-name': 'category'` where category is a known ComponentCategory.
59
+ * @param {string} src
60
+ * @returns {string[]}
61
+ */
62
+ export function extractCategoryKeys(src) {
63
+ const start = src.indexOf('COMPONENT_CATEGORY: Readonly');
64
+ if (start < 0) throw new Error('COMPONENT_CATEGORY object not found in componentCategories.ts');
65
+ const body = src.slice(start, src.indexOf('};', start));
66
+ const keyRe =
67
+ /^\s*(?:([A-Za-z][A-Za-z0-9_]*)|["']([^"']+)["'])\s*:\s*["'](?:sentinel|gate|job|reflector|other)["']/gm;
68
+ const keys = [];
69
+ let m;
70
+ while ((m = keyRe.exec(body)) !== null) keys.push(m[1] || m[2]);
71
+ return [...new Set(keys)];
72
+ }
73
+
74
+ /**
75
+ * @param {string} categorySrc
76
+ * @param {string} docText
77
+ * @param {Record<string,string>} allowlist
78
+ * @returns {{ missing: string[], staleAllowlist: string[] }}
79
+ */
80
+ export function runRegistryFreshness(categorySrc, docText, allowlist = REGISTRY_FRESHNESS_ALLOWLIST) {
81
+ const keys = extractCategoryKeys(categorySrc);
82
+ const missing = [];
83
+ for (const k of keys) {
84
+ if (docText.includes(k)) continue;
85
+ if (k in allowlist) continue;
86
+ missing.push(k);
87
+ }
88
+ // Stale allowlist: an allowlisted name that IS present in the doc now.
89
+ const staleAllowlist = Object.keys(allowlist).filter((k) => docText.includes(k));
90
+ return { missing, staleAllowlist };
91
+ }
92
+
93
+ function main() {
94
+ let categorySrc;
95
+ let docText;
96
+ try {
97
+ categorySrc = fs.readFileSync(CATEGORY_SRC, 'utf8');
98
+ docText = fs.readFileSync(REGISTRY_DOC, 'utf8');
99
+ } catch (err) {
100
+ console.error(`lint-routing-registry-freshness: cannot read inputs — ${err.message}`);
101
+ process.exit(1);
102
+ }
103
+ const { missing, staleAllowlist } = runRegistryFreshness(categorySrc, docText);
104
+ if (missing.length === 0 && staleAllowlist.length === 0) {
105
+ console.log('lint-routing-registry-freshness: OK — every COMPONENT_CATEGORY key has a registry row.');
106
+ process.exit(0);
107
+ }
108
+ if (missing.length > 0) {
109
+ console.error(
110
+ 'lint-routing-registry-freshness: the following LLM component(s) have NO row in ' +
111
+ 'docs/LLM-ROUTING-REGISTRY.md (their routing default was never intentionally decided):\n' +
112
+ missing.map((m) => ` - ${m}`).join('\n') +
113
+ '\n\nAdd a row for each to docs/LLM-ROUTING-REGISTRY.md (the callsite inventory), or — if it ' +
114
+ 'genuinely has no registry row — add it to REGISTRY_FRESHNESS_ALLOWLIST with a reason. ' +
115
+ 'INSTAR-Bench v3, Task-4 Piece 3 (G2).',
116
+ );
117
+ }
118
+ if (staleAllowlist.length > 0) {
119
+ console.error(
120
+ '\nlint-routing-registry-freshness: allowlist entries that are now present in the doc — remove me:\n' +
121
+ staleAllowlist.map((m) => ` - ${m}`).join('\n'),
122
+ );
123
+ }
124
+ process.exit(1);
125
+ }
126
+
127
+ const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === __filename;
128
+ if (isDirectRun) main();
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-07-03T23:34:41.792Z",
5
- "instarVersion": "1.3.742",
4
+ "generatedAt": "2026-07-04T00:06:57.116Z",
5
+ "instarVersion": "1.3.744",
6
6
  "entryCount": 202,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -560,3 +560,86 @@ export const LLM_PARSER_CONTRACT: Readonly<Record<string, ParserContractFlag>> =
560
560
  'enriches standards-coverage rows with content — no closed taught verdict vocabulary a prompt promises and a parser accepts',
561
561
  },
562
562
  };
563
+
564
+ /**
565
+ * Routing nature/chain map — INSTAR-Bench v3 (2026-07-03) JOIN between bench
566
+ * COVERAGE (does a component have a benchmark?) and bench-cited ROUTING (which
567
+ * task-nature did the bench establish it is, and which production chain should
568
+ * it ride?). This is the G1 join of Task-4 Piece 3: `LLM_BENCH_COVERAGE` says
569
+ * "benched"; THIS map says "benched, and here is the winner-nature the v3 bench
570
+ * established for it" — so routing (not just existence) is benchmark-cited.
571
+ *
572
+ * NATURE (task-nature taxonomy — docs/LLM-ROUTING-REGISTRY.md §taxonomy):
573
+ * A = simple bounded verdict (classify / boolean gate / strict-JSON extract —
574
+ * one right answer, terse; speed + format-obedience; reasoning models CLIP)
575
+ * B = nuanced / critical judgment (safety gate, irreversible-action class,
576
+ * completion-stop judge — being RIGHT beats fast/cheap)
577
+ * D = high-volume background (digests, batch classify, summaries, doc-tree)
578
+ * E = deep unbounded reasoning (rare; no output-budget pressure)
579
+ * CHAIN (the four production default→fallback ladders — ELI16 §11):
580
+ * FAST = latency-sensitive quick-sort (Flash-Lite → GPT-5.4 API → … )
581
+ * SORT = background quick-sort (GPT-5.4-mini codex → GPT-5.5 pi → …)
582
+ * JUDGE = careful judgment (GPT-5.5 pi → … → Opus-4.8 API, NEVER CLI)
583
+ * WRITE = open-ended writing (GPT-5.4-mini codex → … → Opus-4.8 CLI)
584
+ *
585
+ * SCOPE — deliberately advisory + NOT (yet) exhaustive over COMPONENT_CATEGORY.
586
+ * This map covers the components whose task-nature the v3 bench established
587
+ * UNAMBIGUOUSLY (a single nature letter in the registry's callsite inventory).
588
+ * Genuinely multi-nature callsites (A/B, B/D) and the router-bypass callsites
589
+ * are left for S4 (the nature-axis router), which ACTUATES this data into
590
+ * IntelligenceRouter model selection and therefore touches critical-gate
591
+ * routing — spec-converge + operator-review gated. Adding an entry HERE changes
592
+ * NO routing today; it is read-only, bench-cited metadata those pieces consume.
593
+ *
594
+ * Ratchet — tests/unit/llm-routing-nature-ratchet.test.ts — enforces:
595
+ * - every key exists in COMPONENT_CATEGORY (no dangling routing claim),
596
+ * - every key present here is bench-COVERED in LLM_BENCH_COVERAGE (you may not
597
+ * cite a routing nature for an unbenched component — cite-the-bench),
598
+ * - nature ∈ {A,B,D,E}, chain ∈ {FAST,SORT,JUDGE,WRITE},
599
+ * - nature→chain coherence: A→FAST|SORT, B→JUDGE, D→SORT|WRITE, E→JUDGE.
600
+ */
601
+ export type TaskNature = 'A' | 'B' | 'D' | 'E';
602
+ export type RoutingChain = 'FAST' | 'SORT' | 'JUDGE' | 'WRITE';
603
+ export interface RoutingNature {
604
+ readonly nature: TaskNature;
605
+ readonly chain: RoutingChain;
606
+ }
607
+
608
+ export const LLM_ROUTING_NATURE: Readonly<Record<string, RoutingNature>> = {
609
+ // ── Nature A — bounded verdicts (background → SORT; latency-critical → FAST) ──
610
+ // MessageSentinel = the emergency-stop classifier: latency-critical AND rule
611
+ // R2 (never rides Opus-via-Claude-CLI — missed canonical STOPs at 73%).
612
+ MessageSentinel: { nature: 'A', chain: 'FAST' },
613
+ // Usher = per-turn topic routing: latency-sensitive quick-sort.
614
+ Usher: { nature: 'A', chain: 'FAST' },
615
+ CommitmentSentinel: { nature: 'A', chain: 'SORT' },
616
+ TemporalCoherenceChecker: { nature: 'A', chain: 'SORT' },
617
+ PresenceProxy: { nature: 'A', chain: 'SORT' },
618
+ ResumeQueueDrainer: { nature: 'A', chain: 'SORT' },
619
+ PromptGate: { nature: 'A', chain: 'SORT' },
620
+ WarrantsReplyGate: { nature: 'A', chain: 'SORT' },
621
+ InputClassifier: { nature: 'A', chain: 'SORT' },
622
+ TelegramAdapter: { nature: 'A', chain: 'SORT' },
623
+ SlackAdapter: { nature: 'A', chain: 'SORT' },
624
+ OverrideDetector: { nature: 'A', chain: 'SORT' },
625
+ TaskClassifier: { nature: 'A', chain: 'SORT' },
626
+ ResumeValidator: { nature: 'A', chain: 'SORT' },
627
+ TopicIntentExtractor: { nature: 'A', chain: 'SORT' },
628
+
629
+ // ── Nature B — critical judgment gates (→ JUDGE; Opus only via API, never CLI) ──
630
+ MessagingToneGate: { nature: 'B', chain: 'JUDGE' },
631
+ CompletionEvaluator: { nature: 'B', chain: 'JUDGE' },
632
+ ExternalOperationGate: { nature: 'B', chain: 'JUDGE' },
633
+ LLMSanitizer: { nature: 'B', chain: 'JUDGE' },
634
+ CoherenceReviewer: { nature: 'B', chain: 'JUDGE' },
635
+ InputGuard: { nature: 'B', chain: 'JUDGE' },
636
+ StallTriageNurse: { nature: 'B', chain: 'JUDGE' },
637
+ ProjectDriftChecker: { nature: 'B', chain: 'JUDGE' },
638
+ SessionWatchdog: { nature: 'B', chain: 'JUDGE' },
639
+ UnjustifiedStopGate: { nature: 'B', chain: 'JUDGE' },
640
+
641
+ // ── Nature D — background digests/summaries (→ SORT; R7 redaction on secret-bearing) ──
642
+ SessionActivitySentinel: { nature: 'D', chain: 'SORT' },
643
+ SessionSummarySentinel: { nature: 'D', chain: 'SORT' },
644
+ 'correction-learning': { nature: 'D', chain: 'SORT' },
645
+ };
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: LLM Bench Refresh
3
+ description: "Cadenced INSTAR-Bench refresh + routing-defaults drift check. On the maintainer/benching agent (the only agent that carries the bench harness under research/llm-pathway-bench/), it runs the bench harness + parity-check and raises ONE operator attention item with the routing-defaults DIFF — it NEVER auto-applies a routing change to a critical gate (operator-review-gated by design, INSTAR-Bench-v2 spec §7/§8). On every other agent the harness is absent, so the job exits silently — it is a no-op. Monthly cadence (spec §7: monthly full run + on-demand after a prompt ships / a new model is enrolled; the on-demand triggers + the versioned auto-reslot catalog are S6, tracked follow-ups). Ships OFF by default (like feedback-factory-process) — a bench run is metered/cost-bearing ($3-20/run, spec §7 budget), so it must be a deliberate opt-in. Tier-1 supervised: this haiku job wraps the deterministic harness + parity-check and sanity-checks the run before it dares surface a diff. Spec: research/llm-pathway-bench/INSTAR-BENCH-V2-SPEC.md §7 (cadence) + §9 (provenance); docs/LLM-ROUTING-REGISTRY.md (the routing defaults it diffs against)."
4
+ schedule: "0 4 1 * *"
5
+ priority: low
6
+ expectedDurationMinutes: 20
7
+ model: haiku
8
+ supervision: tier1
9
+ enabled: false
10
+ tags:
11
+ - cat:bench
12
+ - role:worker
13
+ - exec:prompt
14
+ gate: curl -sf http://localhost:${INSTAR_PORT:-4042}/health >/dev/null 2>&1
15
+ toolAllowlist: "*"
16
+ unrestrictedTools: true
17
+ mcpAccess: none
18
+ ---
19
+ Run one INSTAR-Bench refresh + routing-defaults drift check. This is a deliberate, cost-bearing maintainer job (a bench run spends metered API budget). It is OFF by default and only meaningful on the agent that carries the bench harness. Do NOT auto-apply any routing change — this job only ever RAISES a diff for the operator to review (critical-gate routing is operator-review-gated by design).
20
+
21
+ AUTH="${INSTAR_AUTH_TOKEN:-$(python3 -c "import json; v=json.load(open('.instar/config.json')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)}"
22
+ AGENT_ID="${INSTAR_AGENT_ID:-$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('projectName',''))" 2>/dev/null)}"
23
+ PORT="${INSTAR_PORT:-4042}"
24
+ BENCH_DIR="research/llm-pathway-bench/instar-bench-v2"
25
+
26
+ 1. **Harness-presence gate.** Confirm this agent actually carries the bench harness:
27
+ `test -f "$BENCH_DIR/run2.mjs" && test -f "$BENCH_DIR/parity-check.mjs" && echo present || echo absent`
28
+ If `absent`, this agent is not the benching agent — EXIT SILENTLY, there is nothing to do (this is the no-op path every non-maintainer agent takes). Do not message anyone.
29
+
30
+ 2. **Parity-check FIRST (the cheap, non-metered gate).** A stale battery makes a whole refresh untrustworthy, so verify the batteries still match PRODUCTION prompt text before spending any budget:
31
+ `node "$BENCH_DIR/parity-check.mjs" 2>&1 | tail -40`
32
+ A stale battery is a NAMED failing verdict (spec §9). If parity FAILS, do NOT run the metered bench — surface the parity failure as ONE attention item (step 5, title "bench-refresh: battery parity FAILED") and stop. The batteries must be re-synced to production prompts before a refresh means anything.
33
+
34
+ 3. **Run the refresh at the Wave-1 (cheap smoke) scope.** Full runs cost $12-20 and are a deliberate manual act; the scheduled cadence runs the cheap Wave-1 smoke ($3-5, spec §7). State the cap up front; an unknown price REFUSES to run (spec §7 budget discipline):
35
+ `node "$BENCH_DIR/run2.mjs" --wave 1 2>&1 | tail -60`
36
+ (If `run2.mjs` does not accept `--wave`, read its `--help` and use the documented cheapest-smoke flag; never run the full universe from the scheduled cadence.) The run writes a `run-manifest.json` (bench SHA, per-battery SHA256, price/caps SHA, observed door→model resolution, a `reproduce` command) — that manifest is the provenance record for this refresh.
37
+
38
+ 4. **Tier-1 supervision (your job) — sanity-check BEFORE surfacing anything.** Do not trust a run blindly:
39
+ - The run must have completed with a written manifest and a non-empty results set. A crashed/partial run (no manifest, zero cells) is NOT a signal — note it once and exit; the next cadence retries.
40
+ - Compare the run's per-task winners against the current intentional defaults in `docs/LLM-ROUTING-REGISTRY.md`. A DIFF is only real when it clears the noise floor (spec: <~1.5-pt differences at 218 cells/route are noise — 99.5% vs 99.1% is a TIE, not a diff). Discard sub-noise-floor "changes".
41
+ - If a diff would move a CRITICAL gate (completion/stop/tone/external-op/sanitizer/coherence), it is ALWAYS operator-review — never auto-apply, never present it as "I changed the routing."
42
+
43
+ 5. **Raise the diff as ONE operator attention item (never auto-apply).** Only if step 4 found a real, above-noise diff (or a parity failure from step 2):
44
+ `curl -s -X POST -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID" http://localhost:$PORT/attention -H 'Content-Type: application/json' -d '{"id":"bench-refresh:routing-diff","title":"<short>","body":"<the per-task old-chain → new-chain diff + the manifest reproduce command>","priority":"medium","source":"agent"}'`
45
+ Use a STABLE `id` (`bench-refresh:routing-diff`) so a re-run updates the one item instead of flooding. If step 4 found NO real diff, raise NOTHING and exit silently — a clean refresh is not news.
46
+
47
+ 6. Exit. This job produces a review artifact for the operator, not a running commentary. Do NOT relay progress to Telegram, do NOT summarize a clean run, and do NOT retry-flood a failed curl (the failure is recorded server-side). The routing defaults change only when the operator reviews the diff and acts — this job never touches config.
@@ -0,0 +1,87 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Fixes the member-seat permission-gate false-positive (fb-e5b8b021-b74). With the
9
+ Slack outbound/permission enforcement gate ON, an ordinary workspace MEMBER's
10
+ harmless conversational asks (e.g. "post a check-in note here in 5 minutes") were
11
+ classified as a tier-2 "low-write" — above the member ceiling — and refused with
12
+ an authority challenge ("above what a member can authorize"). The effect was that
13
+ ordinary members effectively could not talk to the bot at all while enforcement
14
+ was on; admin-seat asks still worked.
15
+
16
+ Root cause: the intent classifier treated any write-verb message (post / note /
17
+ schedule) as a tier-2 organizational write, including the bot simply posting a
18
+ conversational note into the CURRENT conversation — which is not an organizational
19
+ action. On the production LLM path, the tier-reconcile step only ever escalates
20
+ the tier, so it could not correct the over-classification.
21
+
22
+ Fix: recognize a harmless conversational self-post — a note / check-in / reminder
23
+ / status update the bot would post into the current conversation, when it is
24
+ deterministically cleared of any floor, organizational-write, external, or
25
+ operational marker — and classify it at tier 1 (the read/draft level a member may
26
+ direct). A recognized conversational self-post also short-circuits the LLM
27
+ (symmetric to the existing floor short-circuit) so the judgment band cannot
28
+ re-escalate it. This is a precision fix, not a floor removal: floor detection
29
+ (money, prod-deploy, credentials, destructive, external send, grant authority)
30
+ runs first and always wins; a genuine low-write (file a ticket), an operational
31
+ action (run a job), and every floor action are still refused for a member exactly
32
+ as before; the name-in-content trap is intact; and a guest still cannot direct
33
+ actions.
34
+
35
+ INSTAR-Bench v3 established the "door penalty": the identical Opus 4.8 model scores 99.1% on bounded judging via a clean API but only 81.7% through the Claude Code CLI door (73% on emergency-stop) — the CLI harness's ~20k-token coding-agent framing turns a skeptical judge credulous. Rules R1/R2 forbid routing any bounded/gating verdict through that door. This ships the three low-risk, dark/reversible pieces of applying that finding to routing, without moving any live routing default (the nature-axis router is separately spec-gated):
36
+
37
+ - **S2 — safety guardrail (the load-bearing piece).** The IntelligenceRouter's failure-swap now clamps a bounded/gating swap that lands on `claude-code` from the `capable` tier (Opus) down to `balanced` (Sonnet 4.6 CLI — 99.5%, 28/28 adversarial). It only ever NARROWS a fallback in the safe direction; it never blocks a call and never touches the open-ended-writing quality lane where Opus-via-CLI is the legitimate primary. A new lint (`lint-no-opus-claude-cli-gating.js`) keeps the clamp intact and refuses any committed config that routes a gating call to Opus×claude-CLI.
38
+ - **S1 — cite-the-bench, extended.** A new registry-doc freshness lint (`lint-routing-registry-freshness.js`) fails the build if any benched component lacks a row in `docs/LLM-ROUTING-REGISTRY.md`, and a new `LLM_ROUTING_NATURE` map records each component's bench-cited task-nature (A/B/D/E) and production chain (FAST/SORT/JUDGE/WRITE). Read-only metadata — it changes no routing today.
39
+ - **S5 — bench-refresh job.** A scaffolded, OFF-by-default, tier-1-supervised monthly job that reruns the bench harness + parity-check and raises ONE operator-review diff when a routing default looks stale. It never auto-applies a change and no-ops on any machine without the bench harness.
40
+
41
+ ## What to Tell Your User
42
+
43
+ - **Members can talk to the bot again**: "If you turned on Slack permission
44
+ enforcement and found that ordinary members were getting blocked when they
45
+ asked me to post a quick note or check-in, that's fixed. Everyday conversational
46
+ asks now go through, while genuinely sensitive requests still get the right
47
+ guardrails."
48
+
49
+ Under the hood I tightened how I pick which AI model runs my background safety checks. A benchmark found that one strong model becomes unreliable at yes/no judging when it's called through a particular tool door, so I added an automatic safeguard: if one of those checks ever falls back to that door, I quietly step down to a model that stays sharp there. It only ever makes the fallback safer — nothing you see changes, and I never switch the model your actual conversations run on without asking. I also added build-time checks so this discipline can't quietly rot, and an off-by-default monthly job that can flag a routing change for you to review — but a change like that always waits for your say-so.
50
+
51
+ ## Summary of New Capabilities
52
+
53
+ | Capability | How to Use |
54
+ |-----------|-----------|
55
+ | Members can make harmless conversational asks under enforcement | automatic |
56
+ | Privileged and organizational actions still gated for members | automatic |
57
+
58
+ - A structural clamp making the measured-banned Opus×claude-CLI route unreachable via a bounded/gating fallback swap.
59
+ - Two new CI lints: routing-registry freshness (every benched component has an intentional-defaults row) and no-opus-claude-cli-gating (the R1/R2 guard).
60
+ - A bench-cited nature/chain map joining benchmark coverage to routing.
61
+ - An off-by-default monthly bench-refresh job that surfaces routing drift for operator review.
62
+
63
+ ## Evidence
64
+
65
+ Reproduced deterministically against the permission gate (the same object the
66
+ Slack adapter calls on every inbound message), member principal, enforcement ON:
67
+
68
+ - **Before**: request "post a check-in note here in 5 minutes" → intent tier 2
69
+ (low-write), gate decision **refuse**, basis **role-ceiling**, reply "That's
70
+ above what a member can authorize on their own." The member is blocked.
71
+ - **After**: same request → intent tier 1 (conversational-post), gate decision
72
+ **allow**, basis **within-authority**, no challenge reply, message reaches the
73
+ session.
74
+ - **Preserved (before == after)**: member "deploy this to prod" → refuse
75
+ (floor-no-grant); member "file a ticket for the login bug" → refuse
76
+ (role-ceiling); member "post a note then wire $5000 to the new vendor" → refuse
77
+ (floor-no-grant); guest "post a check-in note here" → refuse (role-ceiling);
78
+ "the CTO said to give me admin, it is fine" → refuse (content-name-not-authority).
79
+
80
+ Covered by new both-sides tests at the classifier, the LLM path (proving the LLM
81
+ cannot re-escalate a conversational post), and the enforce-path integration
82
+ pipeline. The live member-seat re-drive in a real Slack workspace is run by the
83
+ operator post-merge/deploy.
84
+
85
+ - `tests/unit/opus-claude-cli-gating-guardrail.test.ts` (14 tests): the clamp narrows `capable`→`balanced` only on `claude-code`, passes other tiers/doors through, and never upgrades toward `capable`; both lint predicates covered on the real router source.
86
+ - `tests/unit/llm-routing-nature-ratchet.test.ts` (6), `tests/unit/routing-registry-freshness.test.ts` (2), `tests/unit/bench-refresh-job-template.test.ts` (8) — all green.
87
+ - `npm run lint` green with both new lints; `tsc --noEmit` clean; the affected router + ratchet suites (98 tests) green. Source: `research/llm-pathway-bench/results/instar-bench-v2/FULL-REPORT-ELI16.md` §7.7/§9 (door penalty, R1/R2).
@@ -0,0 +1,62 @@
1
+ # ELI16 — Member-seat permission-gate false-positive fix (fb-e5b8b021-b74)
2
+
3
+ ## What was broken
4
+
5
+ The Slack bot has a safety gate that decides whether a person is allowed to make
6
+ a given request. Every request gets a "sensitivity tier" from 0 (just chatting)
7
+ up to 4 (dangerous: money, production deploys, deleting data). Every person gets
8
+ a "role ceiling": a regular **member** can do tier-1 things (reads, summaries,
9
+ drafts) but not tier-2 and above without more authority.
10
+
11
+ The bug: when a regular member typed a totally harmless message like
12
+ **"post a check-in note here in 5 minutes"**, the gate labeled it a tier-2
13
+ "low-write" — because the word "post"/"note" tripped the write-verb detector.
14
+ Tier 2 is above a member's ceiling, so the gate **refused** the member with an
15
+ authority challenge: *"That's above what a member can authorize on their own."*
16
+
17
+ Because almost anything a member might ask the bot to say/post/schedule tripped
18
+ this, ordinary members effectively **could not talk to the bot at all** while
19
+ enforcement was on. Admins were fine; members were locked out. That was
20
+ demonstrated live from the member seat during today's Slack drive.
21
+
22
+ ## Why it happened
23
+
24
+ Two places conspired:
25
+
26
+ 1. The **heuristic classifier** treated ANY message containing a write verb
27
+ (`post`, `note`, `schedule`, …) as a tier-2 low-write — including the bot
28
+ simply posting a conversational note into the CURRENT channel, which isn't an
29
+ organizational action at all.
30
+ 2. The **LLM classifier** (used in production) couldn't rescue it: its reconcile
31
+ step only ever RAISES the tier (`Math.max`), never lowers it. So even when the
32
+ LLM correctly read the message as conversational, the tier was clamped back up
33
+ to the heuristic's wrong tier-2.
34
+
35
+ ## The fix
36
+
37
+ We taught the classifier to recognize a **harmless conversational self-post** — a
38
+ note / check-in / reminder / status update the bot would post into the current
39
+ conversation — and classify it at **tier 1** (the same level as a draft), which a
40
+ member IS allowed to direct. The recognition is deterministic and conservative:
41
+
42
+ - It fires only when BOTH a post-style verb AND a conversational-content noun are
43
+ present (so plain chatter like "just checking in" stays tier 0).
44
+ - It is disqualified by ANY organizational-write, external, or operational marker
45
+ — a ticket, a record, another named channel (`#…`), a calendar hold, an email,
46
+ or a "run/deploy/job". Those keep their higher tier.
47
+ - Floor detection (money, prod-deploy, credentials, destructive, external-send,
48
+ grant-authority) runs FIRST and always wins, so a privileged action hidden
49
+ inside a "post a note" phrasing is still caught and refused.
50
+
51
+ To make it stick on the production LLM path, a recognized conversational self-post
52
+ short-circuits the LLM entirely (just like a floor action does), so the judgment
53
+ band can't re-escalate it back to tier 2.
54
+
55
+ ## What did NOT change
56
+
57
+ This is a precision fix, not a floor removal. A member asking for a genuine
58
+ low-write (file a ticket), an operational action (run a job), or any privileged
59
+ floor action is STILL refused exactly as before. The "someone said it's fine"
60
+ name-in-content trap still fires on floor actions. Guests still cannot direct
61
+ actions. Only the harmless conversational note/check-in case moved from
62
+ wrongly-refused to correctly-allowed.