instar 1.3.764 → 1.3.766

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.
@@ -91,6 +91,14 @@
91
91
  "file": "src/core/ModelTierEscalation.ts",
92
92
  "regex": "'claude-code':\\s*\\{\\s*default:\\s*'([^']+)',\\s*escalated:\\s*'([^']+)'",
93
93
  "note": "DEFAULT_TIER_ESCALATION.frameworks['claude-code'] default + escalated ids."
94
+ },
95
+ {
96
+ "id": "claude-clean-door-reviewer-default",
97
+ "door": "claude-code",
98
+ "tier": "capable",
99
+ "file": "src/core/crossModelReviewer.ts",
100
+ "regex": "CLAUDE_REVIEWER_DEFAULT_MODEL\\s*=\\s*'((?:claude|gpt|gemini)-[^']+)'",
101
+ "note": "CLAUDE_REVIEWER_DEFAULT_MODEL — the Anthropic clean-door reviewer pin (REVIEWER-DOOR-REWIRING §1.3). Extraction matches a frontier CONSTANT (not a capable:'…' tier decl), so the non-vacuity test (tests/unit/model-registry-freshness-reviewer-pin.test.ts) rots this constant and asserts the strict lint fails — closing the vacuous-tooth risk."
94
102
  }
95
103
  ],
96
104
  "$flaggedStaleNote": "Both prior flagged pins RESOLVED (operator-confirmed 2026-07-03): gemini capable swapped gemini-2.5-pro -> gemini-3.1-pro-preview (pin + topModels updated in the same change); codex capable stays gpt-5.5 (GA flagship — gpt-5.6-sol is preview-only/gov-gated/unreachable, NOT pinned). Nothing pending confirmation.",
@@ -115,12 +115,14 @@ The external "cross-model" pass is a set of independent non-Claude reads that si
115
115
  --spec <spec-path> --family gemini-cli \
116
116
  --context docs/foo.md --context docs/bar.md
117
117
  ```
118
- Each emits a JSON `ReviewerResult` on stdout: `{ status, framework?, model?, verdict?, findings?, reason?, flag }`. Fold each `findings` into the round alongside the internal reviewers'. `status:'degraded'` (framework present but the call failed — timeout / error / rate-limited) is a *partial* cross-model pass for the round: fold in whatever came back and record the `degraded` flag — it does **not** collapse to `unavailable`. (Omitting `--family` keeps the old single-pass first-match behavior for existing callers.)
118
+ Each emits a JSON `ReviewerResult` on stdout: `{ status, framework?, model?, verdict?, findings?, reason?, flag, crossFamily }`. Fold each `findings` into the round alongside the internal reviewers'. `status:'degraded'` (framework present but the call failed — timeout / error / rate-limited) is a *partial* cross-model pass for the round: fold in whatever came back and record the `degraded` flag — it does **not** collapse to `unavailable`. (Omitting `--family` keeps the old single-pass first-match behavior for existing callers.)
119
+
120
+ **Anthropic clean-door reviewer (`--family claude-code`) — a clean-door SECOND READ, NOT a cross-model opinion (REVIEWER-DOOR-REWIRING).** A third family reads the spec through the clean `claude -p` door on the strongest model (`claude-fable-5`), off the measured-penalized `opus × coding-harness` pair. It is **config-gated** (`specConverge.reviewers.anthropic.enabled` — omitted ⇒ live on a development agent, DARK on the fleet) and its result carries `crossFamily: false`, so it books into its OWN disclosure field **`clean-door-anthropic-review: claude-code:<model>`** — NEVER the `cross-model-review:` flag. A claude-only round therefore aggregates to `degraded-all-rounds`/`unavailable` exactly as before; the clean-door read can never launder a cross-model pass. Run it (when enabled) exactly like the others: `--family claude-code`. **D7 per-round-model disclosure:** the six INTERNAL reviewers run on the authoring session's model (opus, or `claude-fable-5` under tier-escalation — a quota-refused escalation silently drops them to opus-via-harness); record the ACTUAL per-round session/subagent model in the iteration log so a silent drop to opus is VISIBLE, not assumed.
119
121
 
120
122
  Two structural guards on the pass:
121
123
 
122
124
  - **Fail-loud model canary.** Model selection is dynamic (the `'capable'` tier resolved per framework — never a pinned name), and a tier word that falls through resolution (`'capable'` is not a model) is caught by `isConcreteReviewerModel` BEFORE the provider is invoked: the round degrades loudly with reason `model-resolution-canary` instead of silently selecting a dead reviewer.
123
- - **Trusted-provider allowlist.** `--family` only accepts frameworks on `TRUSTED_REVIEWER_FRAMEWORKS` (`codex-cli`, `gemini-cli` — first-party OAuth CLI adapters). The full spec text is handed to the reviewer, so it must NEVER be sent to a custom/base-URL endpoint; **pi-cli is deliberately excluded** from cross-model review for this reason (its provider may be a custom endpoint). An untrusted family is refused with `reason: 'untrusted-framework'`.
125
+ - **Trusted-provider allowlist.** `--family` only accepts frameworks on `TRUSTED_REVIEWER_FRAMEWORKS` (`codex-cli`, `gemini-cli`, `claude-code` — first-party OAuth CLI adapters). The full spec text is handed to the reviewer, so it must NEVER be sent to a custom/base-URL endpoint; **pi-cli is deliberately excluded** from cross-model review for this reason (its provider may be a custom endpoint). An untrusted family is refused with `reason: 'untrusted-framework'`; a trusted-but-config-disabled family (the `claude-code` clean-door family on the fleet) is refused with `reason: 'no-supported-framework'` (trusted-egress ≠ enabled). The `claude-code` clean-door call is additionally hardened to codex-door parity for the untrusted spec text (empty allowed-tools, `--strict-mcp-config`, neutral scratch cwd, stdin prompt, env allowlist) — a spec under review is untrusted inbound text.
124
126
 
125
127
  The detection + invocation + parsing live in the unit-tested `src/core/crossModelReviewer.ts` module (built to `dist/core/crossModelReviewer.js`); the script is a thin file-I/O wrapper. codex and gemini are the supported frameworks in an extensible registry (`SUPPORTED_REVIEWER_FRAMEWORKS`) — further first-party CLIs plug in there with **no skill change**.
126
128
 
@@ -125,9 +125,33 @@ function readRepoFile(rel) {
125
125
  return fs.readFileSync(abs, 'utf-8');
126
126
  }
127
127
 
128
+ /**
129
+ * Read the minimal reviewer config the module needs (REVIEWER-DOOR-REWIRING
130
+ * §1.5): the developmentAgent gate flag + the `specConverge.reviewers` block, so
131
+ * the Anthropic clean-door family resolves live-on-dev / dark-fleet. Best-effort:
132
+ * a missing/unparseable `.instar/config.json` ⇒ `{}` ⇒ fleet-dark (byte-identical
133
+ * `[codex, gemini]`). `.instar/config.json` is machine-local (no config
134
+ * replication in instar) — enabling the family is a per-machine edit.
135
+ */
136
+ function loadReviewerConfig() {
137
+ try {
138
+ const cfgPath = path.join(ROOT, '.instar', 'config.json');
139
+ if (!fs.existsSync(cfgPath)) return {};
140
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
141
+ const out = {};
142
+ if (typeof cfg.developmentAgent === 'boolean') out.developmentAgent = cfg.developmentAgent;
143
+ if (cfg.specConverge && typeof cfg.specConverge === 'object') out.specConverge = cfg.specConverge;
144
+ return out;
145
+ } catch {
146
+ return {};
147
+ }
148
+ }
149
+
128
150
  async function main() {
129
151
  const { spec, context, detectOnly, hashOnly, family, stateDir, timeoutMs } = parseArgs();
130
152
  const mod = await loadModule();
153
+ // Config-gate the Anthropic clean-door family (§1.5). Absent config ⇒ fleet-dark.
154
+ const reviewerConfig = loadReviewerConfig();
131
155
 
132
156
  // ── --hash-only: the delta-gating hash of the spec's reviewable body ──
133
157
  if (hashOnly) {
@@ -139,9 +163,9 @@ async function main() {
139
163
 
140
164
  // ── --detect-only: report ALL available families (Piece 3) ──
141
165
  if (detectOnly) {
142
- const all = mod.detectAllCrossModelReviewers();
166
+ const all = mod.detectAllCrossModelReviewers({}, reviewerConfig);
143
167
  // Back-compat: keep the old single-framework fields (first-match shape).
144
- const first = mod.detectCrossModelReviewer();
168
+ const first = mod.detectCrossModelReviewer({}, reviewerConfig);
145
169
  const report = {
146
170
  available: all.length > 0,
147
171
  frameworks: all,
@@ -152,10 +176,11 @@ async function main() {
152
176
  // Record the activation observation into the durable standing-framework
153
177
  // baseline when a state dir was provided. A record failure is surfaced in
154
178
  // the JSON (fail-loud), never silently swallowed — a missing baseline
155
- // would quietly weaken the externals-mandatory check.
179
+ // would quietly weaken the externals-mandatory check. Iterate the ACTIVE
180
+ // set so a config-disabled claude family is not recorded as present.
156
181
  if (stateDir) {
157
182
  const frameworks = {};
158
- for (const entry of mod.SUPPORTED_REVIEWER_FRAMEWORKS) {
183
+ for (const entry of mod.resolveActiveReviewerFrameworks(reviewerConfig)) {
159
184
  frameworks[entry.id] = all.some((d) => d.framework === entry.id);
160
185
  }
161
186
  try {
@@ -186,7 +211,10 @@ async function main() {
186
211
  );
187
212
  process.exit(0);
188
213
  }
189
- familyEntry = mod.SUPPORTED_REVIEWER_FRAMEWORKS.find((f) => f.id === family) ?? null;
214
+ // Config gate (§1.5): a trusted-but-disabled family (the claude clean-door
215
+ // family on the fleet) is refused here — trusted-egress ≠ enabled.
216
+ const active = mod.resolveActiveReviewerFrameworks(reviewerConfig);
217
+ familyEntry = active.find((f) => f.id === family) ?? null;
190
218
  if (!familyEntry) {
191
219
  process.stdout.write(
192
220
  JSON.stringify({
@@ -199,7 +227,7 @@ async function main() {
199
227
  }
200
228
  }
201
229
 
202
- const detection = familyEntry ? familyEntry.detect() : mod.detectCrossModelReviewer();
230
+ const detection = familyEntry ? familyEntry.detect() : mod.detectCrossModelReviewer({}, reviewerConfig);
203
231
 
204
232
  // Unavailable → print the unavailable flag, exit 0. Never block.
205
233
  if (!detection.available) {
@@ -227,9 +255,11 @@ async function main() {
227
255
  promptText: assembled.promptText,
228
256
  timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : mod.REVIEW_TIMEOUT_MS,
229
257
  detectionOverride: detection,
258
+ reviewerConfig,
230
259
  })
231
260
  : await mod.runCrossModelReview({
232
261
  assembled,
262
+ config: reviewerConfig,
233
263
  ...(Number.isFinite(timeoutMs) ? { timeoutMs } : {}),
234
264
  });
235
265
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-07-04T22:57:22.237Z",
5
- "instarVersion": "1.3.764",
4
+ "generatedAt": "2026-07-04T23:55:08.310Z",
5
+ "instarVersion": "1.3.766",
6
6
  "entryCount": 202,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -11,7 +11,7 @@
11
11
  "domain": "identity",
12
12
  "sourcePath": "src/core/PostUpdateMigrator.ts",
13
13
  "installedPath": ".instar/hooks/instar/session-start.sh",
14
- "contentHash": "50b8f7db5235fe798e3a2f18cd9dc0f60cef0e167521ee43a362a915ff611657",
14
+ "contentHash": "de95f71c64d605e358e30e39626a1c25ccfc1eabf480623febc3e2c7dbab948e",
15
15
  "since": "2025-01-01"
16
16
  },
17
17
  "hook:dangerous-command-guard": {
@@ -20,7 +20,7 @@
20
20
  "domain": "safety",
21
21
  "sourcePath": "src/core/PostUpdateMigrator.ts",
22
22
  "installedPath": ".instar/hooks/instar/dangerous-command-guard.sh",
23
- "contentHash": "50b8f7db5235fe798e3a2f18cd9dc0f60cef0e167521ee43a362a915ff611657",
23
+ "contentHash": "de95f71c64d605e358e30e39626a1c25ccfc1eabf480623febc3e2c7dbab948e",
24
24
  "since": "2025-01-01"
25
25
  },
26
26
  "hook:grounding-before-messaging": {
@@ -29,7 +29,7 @@
29
29
  "domain": "safety",
30
30
  "sourcePath": "src/core/PostUpdateMigrator.ts",
31
31
  "installedPath": ".instar/hooks/instar/grounding-before-messaging.sh",
32
- "contentHash": "50b8f7db5235fe798e3a2f18cd9dc0f60cef0e167521ee43a362a915ff611657",
32
+ "contentHash": "de95f71c64d605e358e30e39626a1c25ccfc1eabf480623febc3e2c7dbab948e",
33
33
  "since": "2025-01-01"
34
34
  },
35
35
  "hook:compaction-recovery": {
@@ -38,7 +38,7 @@
38
38
  "domain": "identity",
39
39
  "sourcePath": "src/core/PostUpdateMigrator.ts",
40
40
  "installedPath": ".instar/hooks/instar/compaction-recovery.sh",
41
- "contentHash": "50b8f7db5235fe798e3a2f18cd9dc0f60cef0e167521ee43a362a915ff611657",
41
+ "contentHash": "de95f71c64d605e358e30e39626a1c25ccfc1eabf480623febc3e2c7dbab948e",
42
42
  "since": "2025-01-01"
43
43
  },
44
44
  "hook:external-operation-gate": {
@@ -47,7 +47,7 @@
47
47
  "domain": "safety",
48
48
  "sourcePath": "src/core/PostUpdateMigrator.ts",
49
49
  "installedPath": ".instar/hooks/instar/external-operation-gate.js",
50
- "contentHash": "50b8f7db5235fe798e3a2f18cd9dc0f60cef0e167521ee43a362a915ff611657",
50
+ "contentHash": "de95f71c64d605e358e30e39626a1c25ccfc1eabf480623febc3e2c7dbab948e",
51
51
  "since": "2025-01-01"
52
52
  },
53
53
  "hook:deferral-detector": {
@@ -56,7 +56,7 @@
56
56
  "domain": "safety",
57
57
  "sourcePath": "src/core/PostUpdateMigrator.ts",
58
58
  "installedPath": ".instar/hooks/instar/deferral-detector.js",
59
- "contentHash": "50b8f7db5235fe798e3a2f18cd9dc0f60cef0e167521ee43a362a915ff611657",
59
+ "contentHash": "de95f71c64d605e358e30e39626a1c25ccfc1eabf480623febc3e2c7dbab948e",
60
60
  "since": "2025-01-01"
61
61
  },
62
62
  "hook:self-stop-guard": {
@@ -65,7 +65,7 @@
65
65
  "domain": "coherence",
66
66
  "sourcePath": "src/core/PostUpdateMigrator.ts",
67
67
  "installedPath": ".instar/hooks/instar/self-stop-guard.js",
68
- "contentHash": "50b8f7db5235fe798e3a2f18cd9dc0f60cef0e167521ee43a362a915ff611657",
68
+ "contentHash": "de95f71c64d605e358e30e39626a1c25ccfc1eabf480623febc3e2c7dbab948e",
69
69
  "since": "2025-01-01"
70
70
  },
71
71
  "hook:post-action-reflection": {
@@ -74,7 +74,7 @@
74
74
  "domain": "evolution",
75
75
  "sourcePath": "src/core/PostUpdateMigrator.ts",
76
76
  "installedPath": ".instar/hooks/instar/post-action-reflection.js",
77
- "contentHash": "50b8f7db5235fe798e3a2f18cd9dc0f60cef0e167521ee43a362a915ff611657",
77
+ "contentHash": "de95f71c64d605e358e30e39626a1c25ccfc1eabf480623febc3e2c7dbab948e",
78
78
  "since": "2025-01-01"
79
79
  },
80
80
  "hook:external-communication-guard": {
@@ -83,7 +83,7 @@
83
83
  "domain": "safety",
84
84
  "sourcePath": "src/core/PostUpdateMigrator.ts",
85
85
  "installedPath": ".instar/hooks/instar/external-communication-guard.js",
86
- "contentHash": "50b8f7db5235fe798e3a2f18cd9dc0f60cef0e167521ee43a362a915ff611657",
86
+ "contentHash": "de95f71c64d605e358e30e39626a1c25ccfc1eabf480623febc3e2c7dbab948e",
87
87
  "since": "2025-01-01"
88
88
  },
89
89
  "hook:scope-coherence-collector": {
@@ -92,7 +92,7 @@
92
92
  "domain": "coherence",
93
93
  "sourcePath": "src/core/PostUpdateMigrator.ts",
94
94
  "installedPath": ".instar/hooks/instar/scope-coherence-collector.js",
95
- "contentHash": "50b8f7db5235fe798e3a2f18cd9dc0f60cef0e167521ee43a362a915ff611657",
95
+ "contentHash": "de95f71c64d605e358e30e39626a1c25ccfc1eabf480623febc3e2c7dbab948e",
96
96
  "since": "2025-01-01"
97
97
  },
98
98
  "hook:scope-coherence-checkpoint": {
@@ -101,7 +101,7 @@
101
101
  "domain": "coherence",
102
102
  "sourcePath": "src/core/PostUpdateMigrator.ts",
103
103
  "installedPath": ".instar/hooks/instar/scope-coherence-checkpoint.js",
104
- "contentHash": "50b8f7db5235fe798e3a2f18cd9dc0f60cef0e167521ee43a362a915ff611657",
104
+ "contentHash": "de95f71c64d605e358e30e39626a1c25ccfc1eabf480623febc3e2c7dbab948e",
105
105
  "since": "2025-01-01"
106
106
  },
107
107
  "hook:free-text-guard": {
@@ -110,7 +110,7 @@
110
110
  "domain": "safety",
111
111
  "sourcePath": "src/core/PostUpdateMigrator.ts",
112
112
  "installedPath": ".instar/hooks/instar/free-text-guard.sh",
113
- "contentHash": "50b8f7db5235fe798e3a2f18cd9dc0f60cef0e167521ee43a362a915ff611657",
113
+ "contentHash": "de95f71c64d605e358e30e39626a1c25ccfc1eabf480623febc3e2c7dbab948e",
114
114
  "since": "2025-01-01"
115
115
  },
116
116
  "hook:claim-intercept": {
@@ -119,7 +119,7 @@
119
119
  "domain": "coherence",
120
120
  "sourcePath": "src/core/PostUpdateMigrator.ts",
121
121
  "installedPath": ".instar/hooks/instar/claim-intercept.js",
122
- "contentHash": "50b8f7db5235fe798e3a2f18cd9dc0f60cef0e167521ee43a362a915ff611657",
122
+ "contentHash": "de95f71c64d605e358e30e39626a1c25ccfc1eabf480623febc3e2c7dbab948e",
123
123
  "since": "2025-01-01"
124
124
  },
125
125
  "hook:claim-intercept-response": {
@@ -128,7 +128,7 @@
128
128
  "domain": "coherence",
129
129
  "sourcePath": "src/core/PostUpdateMigrator.ts",
130
130
  "installedPath": ".instar/hooks/instar/claim-intercept-response.js",
131
- "contentHash": "50b8f7db5235fe798e3a2f18cd9dc0f60cef0e167521ee43a362a915ff611657",
131
+ "contentHash": "de95f71c64d605e358e30e39626a1c25ccfc1eabf480623febc3e2c7dbab948e",
132
132
  "since": "2025-01-01"
133
133
  },
134
134
  "hook:stop-gate-router": {
@@ -137,7 +137,7 @@
137
137
  "domain": "safety",
138
138
  "sourcePath": "src/core/PostUpdateMigrator.ts",
139
139
  "installedPath": ".instar/hooks/instar/stop-gate-router.js",
140
- "contentHash": "50b8f7db5235fe798e3a2f18cd9dc0f60cef0e167521ee43a362a915ff611657",
140
+ "contentHash": "de95f71c64d605e358e30e39626a1c25ccfc1eabf480623febc3e2c7dbab948e",
141
141
  "since": "2025-01-01"
142
142
  },
143
143
  "hook:auto-approve-permissions": {
@@ -146,7 +146,7 @@
146
146
  "domain": "safety",
147
147
  "sourcePath": "src/core/PostUpdateMigrator.ts",
148
148
  "installedPath": ".instar/hooks/instar/auto-approve-permissions.js",
149
- "contentHash": "50b8f7db5235fe798e3a2f18cd9dc0f60cef0e167521ee43a362a915ff611657",
149
+ "contentHash": "de95f71c64d605e358e30e39626a1c25ccfc1eabf480623febc3e2c7dbab948e",
150
150
  "since": "2025-01-01"
151
151
  },
152
152
  "job:health-check": {
@@ -1562,7 +1562,7 @@
1562
1562
  "type": "subsystem",
1563
1563
  "domain": "updates",
1564
1564
  "sourcePath": "src/core/PostUpdateMigrator.ts",
1565
- "contentHash": "50b8f7db5235fe798e3a2f18cd9dc0f60cef0e167521ee43a362a915ff611657",
1565
+ "contentHash": "de95f71c64d605e358e30e39626a1c25ccfc1eabf480623febc3e2c7dbab948e",
1566
1566
  "since": "2025-01-01"
1567
1567
  },
1568
1568
  "subsystem:scheduler": {
@@ -0,0 +1,63 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Added a third **spec-converge external reviewer family**: the strongest Anthropic model
9
+ (`claude-fable-5`) now reads a spec through the clean `claude -p` door — off the measured-penalized
10
+ `opus × coding-harness` pair (INSTAR-Bench v2: Opus-4.8-via-Claude-Code 81.7% vs clean-API 99.1%, a
11
+ 17.4-point door penalty). This is **inc1** of REVIEWER-DOOR-REWIRING, and ships **dark on the fleet /
12
+ live on a development agent** (the `specConverge.reviewers.anthropic.enabled` developmentAgent gate —
13
+ absent config keeps today's exact `[codex, gemini]` behavior byte-for-byte).
14
+
15
+ The reviewer is a **clean-door second read, NOT a cross-model opinion** — it carries `crossFamily:
16
+ false` and books into its own `clean-door-anthropic-review` disclosure field, so a claude-only round
17
+ can never launder the `cross-model-review` flag. Because a spec under review is untrusted ~60KB text,
18
+ the call is **hardened to codex-door parity**: empty allowed-tools (`--allowedTools ''`, an allow-list
19
+ not a denylist), `--strict-mcp-config`, a neutral scratch cwd, the prompt via stdin (never argv), an
20
+ env allowlist that strips agent secrets (`INSTAR_AUTH_TOKEN`), and a runtime fail-closed preflight
21
+ that degrades (never runs unhardened) if the installed CLI lacks the flags. The model argument is the
22
+ CONCRETE frontier pin — never the tier word `'capable'`, which resolves to opus.
23
+
24
+ ## What to Tell Your User
25
+
26
+ Nothing proactive — this is instar-developing-agent tooling that ships off on the fleet. If a user
27
+ asks why a spec-converge run now shows a "clean-door Anthropic review" line, or whether the strongest
28
+ Claude model reviews specs: on a development agent the spec is now also read by Fable 5 (the strongest
29
+ Claude model) through a clean door (separate from the six in-session reviewers), disclosed on its own line and never
30
+ counted as a cross-model opinion. It is dogfooded on the development agent first, before any wider
31
+ rollout; the "clean door" claim is scoped to *off the measured-penalized coding-harness door*, not
32
+ bench-verified-clean (that direct bench is a tracked follow-up).
33
+
34
+ ## Summary of New Capabilities
35
+
36
+ - **`--family claude-code` clean-door reviewer** in spec-converge — config-gated
37
+ (`specConverge.reviewers.anthropic.enabled`, dev-on / fleet-dark); an optional
38
+ `specConverge.reviewers.anthropic.model` override is validated against the frontier accept-set
39
+ (a non-frontier concrete id like `claude-opus-4-8` is rejected, never silently honored).
40
+ - **Cross-model honesty guard** — `crossFamily` is threaded onto the reviewer registry,
41
+ `ReviewerResult`, and `CrossModelDetectionResult` (fail-closed on an unknown id); the aggregate
42
+ flag, both detection paths, and the 7-day externals-mandatory baseline all filter on
43
+ `crossFamily: true`, so the claude family can never masquerade as cross-model.
44
+ - **Inbound-safety hardening** for reviewing untrusted spec text (empty allowed-tools,
45
+ strict-mcp-config, neutral cwd, stdin prompt, env allowlist), verified by a STATE-level
46
+ zero-tool-execution test against the real Claude CLI.
47
+ - No behavior change while the family stays dark (the fleet default); this only makes the reviewer
48
+ reachable on a development agent.
49
+
50
+ ## Evidence
51
+
52
+ - `tests/unit/crossModelReviewer-clean-door.test.ts` — detection (static presence reasons only),
53
+ model resolution (default pin, tier-word + non-frontier-override rejection), the §5 lockdown
54
+ battery (a–e), the required-`crossFamily`-field guard, per-family concrete-pin model-arg, and the
55
+ config-gate default (fleet-absent = exactly `[codex, gemini]`).
56
+ - `tests/unit/claude-reviewer-inbound-safety.test.ts` — hardened argv/env asserted deterministically
57
+ + a live-claude-gated STATE test proving a benign tool-invoking payload creates ZERO files.
58
+ - `tests/unit/model-registry-freshness-reviewer-pin.test.ts` — the freshness-lint tooth on the
59
+ reviewer pin is NOT vacuous (a rotted constant fails the strict lint).
60
+ - `tests/integration/clean-door-reviewer-driver.test.ts` — the driver `--family`/`--detect-only`
61
+ paths: clean-door flag shape, trusted-but-disabled refusal, family-present-only-when-enabled.
62
+ - `tests/unit/PostUpdateMigrator-anthropicReviewerDisclosure.test.ts` — the SKILL.md content
63
+ migration reaches already-installed agents (idempotent, fingerprint-guarded).
@@ -0,0 +1,37 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Added a CI lint (`scripts/lint-no-unreachable-messaging-gate.js`, wired into the `lint` chain) that
9
+ fails the build if any source reads a **default-OFF** config gate at a `messaging.<child>.*`
10
+ dot-path. On a real install `messaging` is a JSON **array** of adapter configs, so
11
+ `messaging.<child>.*` resolves `undefined` → the `false` default → the feature is structurally
12
+ un-enablable. This is the class-level guard for the PR #1379 bug (the Action-Claim / Slack-followthrough
13
+ sentinel that couldn't be turned on). It flags only the concrete `.get('messaging.*', false)` shape,
14
+ exempts default-`true` gates, and supports an inline `// lint-allow-messaging-gate: <reason>`
15
+ suppression. Zero current offenders — a pure forward guard.
16
+
17
+ ## What to Tell Your User
18
+
19
+ Nothing proactive — this is an internal build-time safety check. It only matters to developers: it
20
+ stops a whole class of "the on-switch was written in a place the program can't read" mistakes from
21
+ ever landing again, which is the exact bug that made the promise follow-through tracker impossible to
22
+ turn on earlier. It is the automated backstop so a human doesn't have to remember that quirk.
23
+
24
+ ## Summary of New Capabilities
25
+
26
+ - CI now refuses a default-off config gate placed at an unreachable position under the messaging list.
27
+ - Points the author at the fix (move the switch to a reachable top-level setting) with a precise
28
+ file:line, or lets them suppress a deliberate exception visibly.
29
+ - No runtime effect; a build-time guard only.
30
+
31
+ ## Evidence
32
+
33
+ - `tests/unit/lint-no-unreachable-messaging-gate.test.ts` — 8 cases: flags the #1379 shape (generic +
34
+ plain, both quote styles); exempts default-true, non-messaging, and top-level `actionClaim`; honors
35
+ same-line and preceding-line suppression; the exported regex matches the incident line.
36
+ - `node scripts/lint-no-unreachable-messaging-gate.js` → `clean` on current `main` (0 offenders).
37
+ - Side-effects review: `upgrades/side-effects/lint-unreachable-messaging-gate.md`.
@@ -0,0 +1,42 @@
1
+ # ELI16 — a guard so the "un-turn-on-able switch" bug can't come back
2
+
3
+ ## The story so far
4
+
5
+ Earlier today we found a safety feature whose on-switch was written in a spot the program could never
6
+ read: the config's "messaging" section is a *list* of chat platforms, but the switch was addressed as
7
+ if it were a labeled box inside that list. Lists don't have named boxes, so the program always found
8
+ "nothing," and "nothing" means off. The feature was impossible to turn on, and every test had used a
9
+ fake box-shaped config so nobody noticed. We fixed that specific feature (PR #1379). This change makes
10
+ sure the *whole class* of that mistake can't happen again.
11
+
12
+ ## What this adds
13
+
14
+ A tiny automated check (a "lint") that runs whenever the code is built or someone tries to commit. It
15
+ scans the code for the exact broken shape: reading an **off-by-default** switch from a
16
+ `messaging.<something>` address. Because "messaging" is a list, any such address is unreadable, and an
17
+ off-by-default switch there can never be turned on — so the check fails the build and points at the
18
+ line, telling the author to move the switch to a spot that actually works (a top-level setting).
19
+
20
+ ## Why it's careful (not annoying)
21
+
22
+ - It only flags the genuinely-broken case: **off-by-default** switches. Switches that default to *on*
23
+ are left alone, because "unreadable" just means they quietly stay on — no harm.
24
+ - It only looks at the one common way switches are read (`.get("messaging.x.enabled", false)`), so it
25
+ won't cry wolf on unrelated code.
26
+ - If there's ever a real reason to keep such a line, an author can add a short "allow this on purpose"
27
+ comment and the check steps aside — but that choice is now visible in the code instead of silent.
28
+ - Right now there are **zero** offending lines (the earlier fix removed the last one), so turning this
29
+ check on breaks nothing today — it's purely a guard for the future.
30
+
31
+ ## How we know it works
32
+
33
+ Eight small tests feed the checker example lines: it correctly flags the exact bug shape (in a few
34
+ spellings), correctly ignores the safe cases (default-on switches, top-level switches, non-messaging
35
+ switches), and correctly honors the "allow this on purpose" comment. So the guard catches the mistake
36
+ without blocking legitimate code.
37
+
38
+ ## The bigger idea
39
+
40
+ This is the "Structure beats Willpower" principle in one line: instead of hoping future authors
41
+ *remember* that "messaging" is a list and won't hold a switch, the build now *refuses* the mistake
42
+ automatically. A guard that a human missed once shouldn't rely on a human not missing it again.
@@ -0,0 +1,107 @@
1
+ # Side-Effects Review — lint: ban default-off config gate at an unreachable messaging.* path
2
+
3
+ **Version / slug:** `lint-unreachable-messaging-gate`
4
+ **Date:** `2026-07-04`
5
+ **Author:** `Echo`
6
+ **Second-pass reviewer:** `not required (Tier-1)`
7
+
8
+ ## Summary of the change
9
+
10
+ Adds `scripts/lint-no-unreachable-messaging-gate.js` (wired into the `lint` chain in `package.json`)
11
+ and its unit test. The lint fails CI if any source file reads a **default-OFF** config gate at a
12
+ `messaging.<child>.*` dot-path — the exact shape of the PR #1379 bug: on a real install `messaging`
13
+ is a JSON **array**, so `messaging.<child>.*` resolves `undefined` → the `false` default → the
14
+ feature is structurally un-enablable. It flags only the concrete `.get('messaging.*', false)`
15
+ LiveConfig gate-read shape, exempts default-`true` gates (unreachable just leaves them on), and
16
+ supports an inline `// lint-allow-messaging-gate: <reason>` suppression. Currently 0 offenders on
17
+ `main` (PR #1379 removed the last one) — this is a pure forward guard.
18
+
19
+ ## Decision-point inventory
20
+
21
+ - `scripts/lint-no-unreachable-messaging-gate.js` — **add** — a new CI lint gate.
22
+ - `package.json` `lint` script — **modify** — append the new lint to the chain.
23
+ - `tests/unit/lint-no-unreachable-messaging-gate.test.ts` — **add** — detector unit tests.
24
+
25
+ ## 1. Over-block
26
+
27
+ The regex targets `.get('messaging.<...>', false)` specifically. A legitimate default-OFF gate that
28
+ genuinely must live under `messaging` (none known) would be flagged — but the correct answer is to
29
+ move it to a reachable top-level key (the whole point), and the inline suppression exists for a
30
+ deliberate exception. Default-`true` gates and non-`messaging` paths are not flagged.
31
+
32
+ ## 2. Under-block
33
+
34
+ It only catches the `.get(path, false)` shape. A default-off gate read via a cast
35
+ (`(cfg as {...}).messaging?.x`) or a raw-file read (`cfg.messaging.x.enabled` in a generated hook)
36
+ is not caught — a known, accepted limitation (the `.get` gate is the primary and highest-frequency
37
+ pattern; the two others are rarer and harder to lint without false positives). Documented in the
38
+ lint header.
39
+
40
+ ## 3. Level-of-abstraction fit
41
+
42
+ Right layer: a build-time lint (cheap, deterministic, CI-enforced) is exactly the "Structure beats
43
+ Willpower" guard for a class that a human/CI missed once (object-shaped-messaging tests hid it). It
44
+ does not add runtime behavior or authority.
45
+
46
+ ## 4. Signal vs authority compliance
47
+
48
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
49
+
50
+ - [x] No — this change has no runtime block/allow surface. It is a build-time CI lint. Its "authority"
51
+ is failing a build on a precise, suppressible pattern — not gating any live message or action.
52
+
53
+ ## 5. Interactions
54
+
55
+ - **Shadowing:** none — it's an independent step appended to the end of the `lint` chain.
56
+ - **Double-fire:** none.
57
+ - **Races:** none — pure file scan at build time.
58
+ - **Feedback loops:** none.
59
+
60
+ ## 6. External surfaces
61
+
62
+ - **Install base / agents:** none at runtime — a CI/dev-time lint only.
63
+ - **External systems / persistent state:** none.
64
+ - **Operator surface:** no operator-facing action.
65
+
66
+ ## 6b. Operator-surface quality
67
+
68
+ No operator surface — not applicable.
69
+
70
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
71
+
72
+ **machine-local BY DESIGN** — a build-time lint runs in CI / on a developer's checkout; it has no
73
+ runtime, no cross-machine state, no notices, no URLs. It is identical everywhere it runs by
74
+ construction.
75
+
76
+ ## 8. Rollback cost
77
+
78
+ Pure additive tooling — revert the script + the one-line `package.json` edit + the test. No
79
+ persistent state, no runtime effect, no user-visible change. Zero-cost back-out.
80
+
81
+ ## Conclusion
82
+
83
+ A small, precise, suppressible CI lint that would have caught the PR #1379 un-enablable bug at the
84
+ read site (`.get('messaging.actionClaim.enabled', false)`). 0 current offenders, so it is a pure
85
+ forward guard closing the class the operator's live-enable attempt surfaced. Clear to ship.
86
+
87
+ ## Second-pass review (if required)
88
+
89
+ **Reviewer:** not required (Tier-1)
90
+
91
+ ## Evidence pointers
92
+
93
+ - `tests/unit/lint-no-unreachable-messaging-gate.test.ts` — 8 cases: flags the #1379 shape (generic +
94
+ plain + both quote styles), exempts default-true and non-messaging and top-level `actionClaim`,
95
+ honors same-line and preceding-line suppression, regex matches the incident line.
96
+ - `node scripts/lint-no-unreachable-messaging-gate.js` → `clean` (0 offenders on current `main`).
97
+
98
+ ## Class-Closure Declaration (display-only mirror)
99
+
100
+ - **`defectClass`** — `config-unreachable-on-shape` (the class named in the PR #1379 side-effects
101
+ artifact; the guard the operator asked be built to close it).
102
+ - **`closure`** — `guard` — this lint IS the class-level guard: it structurally fails CI when a
103
+ default-off config gate is read at an unreachable `messaging.<child>.*` dot-path.
104
+ - **`guardEvidence`** — `{ enforcementType: lint, citation: scripts/lint-no-unreachable-messaging-gate.js
105
+ (wired into package.json `lint`, run by .husky/pre-commit + CI), howCaught: it flags
106
+ `.get('messaging.<child>.*', false)` — the exact read shape of the #1379 master gate — so the
107
+ un-enablable gate can never re-land }`.