instar 1.3.483 → 1.3.485
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/index.html +4 -1
- package/dist/core/crossModelReviewer.d.ts +139 -9
- package/dist/core/crossModelReviewer.d.ts.map +1 -1
- package/dist/core/crossModelReviewer.js +342 -16
- package/dist/core/crossModelReviewer.js.map +1 -1
- package/dist/server/PeerStreamProxy.d.ts +7 -0
- package/dist/server/PeerStreamProxy.d.ts.map +1 -1
- package/dist/server/PeerStreamProxy.js +11 -2
- package/dist/server/PeerStreamProxy.js.map +1 -1
- package/dist/server/WebSocketManager.d.ts +10 -1
- package/dist/server/WebSocketManager.d.ts.map +1 -1
- package/dist/server/WebSocketManager.js +27 -3
- package/dist/server/WebSocketManager.js.map +1 -1
- package/package.json +1 -1
- package/skills/spec-converge/SKILL.md +41 -14
- package/skills/spec-converge/scripts/cross-model-review.mjs +113 -15
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.484.md +37 -0
- package/upgrades/1.3.485.md +31 -0
- package/upgrades/side-effects/cross-model-hardening.md +93 -0
- package/upgrades/side-effects/pool-stream-cross-machine-fix.md +94 -0
|
@@ -15,20 +15,37 @@
|
|
|
15
15
|
* access, so context must be inlined before the spawn.
|
|
16
16
|
*
|
|
17
17
|
* Modes:
|
|
18
|
-
* --detect-only Print detection JSON and exit (no
|
|
19
|
-
* { available,
|
|
18
|
+
* --detect-only Print detection JSON and exit (no spawn).
|
|
19
|
+
* { available, frameworks: [...all available...],
|
|
20
|
+
* framework?, model?, reason? } — the `frameworks`
|
|
21
|
+
* array is the Piece-3 family-diverse collection;
|
|
22
|
+
* the single-framework fields stay for back-compat.
|
|
23
|
+
* With --state-dir <dir>, also records the
|
|
24
|
+
* activation observation to the durable
|
|
25
|
+
* framework-activation history (the standing-
|
|
26
|
+
* framework baseline for the mandatory check).
|
|
27
|
+
* --hash-only Print { hash } — sha256 of the spec's reviewable
|
|
28
|
+
* body (frontmatter-stripped, CRLF-normalized) for
|
|
29
|
+
* the skill's delta-gating. Requires --spec.
|
|
20
30
|
* (default) Detect; if available, assemble the prompt + run
|
|
21
|
-
* the
|
|
31
|
+
* the review; print the ReviewerResult JSON. With
|
|
32
|
+
* --family <id>, run through THAT framework
|
|
33
|
+
* specifically (must be on the trusted first-party
|
|
34
|
+
* allowlist — spec text is never sent to a custom/
|
|
35
|
+
* base-URL endpoint; pi-cli is excluded by design).
|
|
22
36
|
*
|
|
23
37
|
* Usage:
|
|
24
38
|
* node skills/spec-converge/scripts/cross-model-review.mjs \
|
|
25
39
|
* --spec docs/specs/<slug>.md \
|
|
26
40
|
* [--context docs/foo.md --context docs/bar.md ...] \
|
|
27
|
-
* [--detect-only] \
|
|
41
|
+
* [--detect-only] [--state-dir .instar] \
|
|
42
|
+
* [--hash-only] \
|
|
43
|
+
* [--family codex-cli|gemini-cli] \
|
|
28
44
|
* [--timeout-ms 120000]
|
|
29
45
|
*
|
|
30
46
|
* Output: a single JSON object on stdout (machine-readable for the skill).
|
|
31
|
-
* On detect-only: the
|
|
47
|
+
* On detect-only: the detection JSON above.
|
|
48
|
+
* On hash-only: { hash }.
|
|
32
49
|
* On full run: the ReviewerResult ({ status, framework?, model?, verdict?,
|
|
33
50
|
* findings?, reason?, flag }).
|
|
34
51
|
*
|
|
@@ -60,19 +77,30 @@ function fail(msg) {
|
|
|
60
77
|
|
|
61
78
|
function parseArgs() {
|
|
62
79
|
const args = process.argv.slice(2);
|
|
63
|
-
const out = {
|
|
80
|
+
const out = {
|
|
81
|
+
spec: null,
|
|
82
|
+
context: [],
|
|
83
|
+
detectOnly: false,
|
|
84
|
+
hashOnly: false,
|
|
85
|
+
family: null,
|
|
86
|
+
stateDir: null,
|
|
87
|
+
timeoutMs: null,
|
|
88
|
+
};
|
|
64
89
|
for (let i = 0; i < args.length; i++) {
|
|
65
90
|
const a = args[i];
|
|
66
91
|
if (a === '--spec') out.spec = args[++i];
|
|
67
92
|
else if (a === '--context') out.context.push(args[++i]);
|
|
68
93
|
else if (a === '--detect-only') out.detectOnly = true;
|
|
94
|
+
else if (a === '--hash-only') out.hashOnly = true;
|
|
95
|
+
else if (a === '--family') out.family = args[++i];
|
|
96
|
+
else if (a === '--state-dir') out.stateDir = args[++i];
|
|
69
97
|
else if (a === '--timeout-ms') out.timeoutMs = parseInt(args[++i], 10);
|
|
70
98
|
else fail(`Unknown arg: ${a}`);
|
|
71
99
|
}
|
|
72
100
|
if (!out.detectOnly && !out.spec) {
|
|
73
101
|
fail(
|
|
74
102
|
'Usage: cross-model-review.mjs --spec PATH [--context PATH ...] ' +
|
|
75
|
-
'[--detect-only] [--timeout-ms N]',
|
|
103
|
+
'[--detect-only] [--hash-only] [--family ID] [--state-dir DIR] [--timeout-ms N]',
|
|
76
104
|
);
|
|
77
105
|
}
|
|
78
106
|
return out;
|
|
@@ -98,17 +126,81 @@ function readRepoFile(rel) {
|
|
|
98
126
|
}
|
|
99
127
|
|
|
100
128
|
async function main() {
|
|
101
|
-
const { spec, context, detectOnly, timeoutMs } = parseArgs();
|
|
129
|
+
const { spec, context, detectOnly, hashOnly, family, stateDir, timeoutMs } = parseArgs();
|
|
102
130
|
const mod = await loadModule();
|
|
103
131
|
|
|
104
|
-
//
|
|
105
|
-
|
|
132
|
+
// ── --hash-only: the delta-gating hash of the spec's reviewable body ──
|
|
133
|
+
if (hashOnly) {
|
|
134
|
+
if (!spec) fail('--hash-only requires --spec PATH');
|
|
135
|
+
const specMarkdown = readRepoFile(spec);
|
|
136
|
+
process.stdout.write(JSON.stringify({ hash: mod.hashSpecReviewableBody(specMarkdown) }) + '\n');
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
106
139
|
|
|
140
|
+
// ── --detect-only: report ALL available families (Piece 3) ──
|
|
107
141
|
if (detectOnly) {
|
|
108
|
-
|
|
142
|
+
const all = mod.detectAllCrossModelReviewers();
|
|
143
|
+
// Back-compat: keep the old single-framework fields (first-match shape).
|
|
144
|
+
const first = mod.detectCrossModelReviewer();
|
|
145
|
+
const report = {
|
|
146
|
+
available: all.length > 0,
|
|
147
|
+
frameworks: all,
|
|
148
|
+
...(first.framework ? { framework: first.framework } : {}),
|
|
149
|
+
...(first.model ? { model: first.model } : {}),
|
|
150
|
+
...(first.reason ? { reason: first.reason } : {}),
|
|
151
|
+
};
|
|
152
|
+
// Record the activation observation into the durable standing-framework
|
|
153
|
+
// baseline when a state dir was provided. A record failure is surfaced in
|
|
154
|
+
// the JSON (fail-loud), never silently swallowed — a missing baseline
|
|
155
|
+
// would quietly weaken the externals-mandatory check.
|
|
156
|
+
if (stateDir) {
|
|
157
|
+
const frameworks = {};
|
|
158
|
+
for (const entry of mod.SUPPORTED_REVIEWER_FRAMEWORKS) {
|
|
159
|
+
frameworks[entry.id] = all.some((d) => d.framework === entry.id);
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
mod.recordFrameworkActivationObservation(stateDir, { frameworks });
|
|
163
|
+
report.activationRecorded = true;
|
|
164
|
+
} catch (err) {
|
|
165
|
+
report.activationRecorded = false;
|
|
166
|
+
report.activationRecordError =
|
|
167
|
+
err instanceof Error ? err.message.slice(0, 200) : String(err);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
process.stdout.write(JSON.stringify(report) + '\n');
|
|
109
171
|
process.exit(0);
|
|
110
172
|
}
|
|
111
173
|
|
|
174
|
+
// ── full run ──
|
|
175
|
+
// --family: run through ONE specific framework. Allowlist-gated — the full
|
|
176
|
+
// spec text is never sent to a custom/base-URL endpoint (pi-cli excluded).
|
|
177
|
+
let familyEntry = null;
|
|
178
|
+
if (family) {
|
|
179
|
+
if (!mod.isTrustedReviewerFramework(family)) {
|
|
180
|
+
process.stdout.write(
|
|
181
|
+
JSON.stringify({
|
|
182
|
+
status: 'unavailable',
|
|
183
|
+
reason: 'untrusted-framework',
|
|
184
|
+
flag: 'cross-model-review: unavailable',
|
|
185
|
+
}) + '\n',
|
|
186
|
+
);
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
189
|
+
familyEntry = mod.SUPPORTED_REVIEWER_FRAMEWORKS.find((f) => f.id === family) ?? null;
|
|
190
|
+
if (!familyEntry) {
|
|
191
|
+
process.stdout.write(
|
|
192
|
+
JSON.stringify({
|
|
193
|
+
status: 'unavailable',
|
|
194
|
+
reason: 'no-supported-framework',
|
|
195
|
+
flag: 'cross-model-review: unavailable',
|
|
196
|
+
}) + '\n',
|
|
197
|
+
);
|
|
198
|
+
process.exit(0);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const detection = familyEntry ? familyEntry.detect() : mod.detectCrossModelReviewer();
|
|
203
|
+
|
|
112
204
|
// Unavailable → print the unavailable flag, exit 0. Never block.
|
|
113
205
|
if (!detection.available) {
|
|
114
206
|
const flag = mod.buildCrossModelFlag('unavailable', detection.reason);
|
|
@@ -130,10 +222,16 @@ async function main() {
|
|
|
130
222
|
context: contextDocs,
|
|
131
223
|
});
|
|
132
224
|
|
|
133
|
-
const result =
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
225
|
+
const result = familyEntry
|
|
226
|
+
? await familyEntry.review({
|
|
227
|
+
promptText: assembled.promptText,
|
|
228
|
+
timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : mod.REVIEW_TIMEOUT_MS,
|
|
229
|
+
detectionOverride: detection,
|
|
230
|
+
})
|
|
231
|
+
: await mod.runCrossModelReview({
|
|
232
|
+
assembled,
|
|
233
|
+
...(Number.isFinite(timeoutMs) ? { timeoutMs } : {}),
|
|
234
|
+
});
|
|
137
235
|
|
|
138
236
|
// Surface truncation in the emitted result so the skill/report can note it.
|
|
139
237
|
process.stdout.write(JSON.stringify({ ...result, promptTruncated: assembled.truncated }) + '\n');
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-06-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-11T21:37:39.431Z",
|
|
5
|
+
"instarVersion": "1.3.485",
|
|
6
6
|
"entryCount": 201,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: minor -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Implements Piece 3 (final piece) of `docs/specs/AUTONOMY-PRINCIPLES-ENFORCEMENT-SPEC.md`. The spec-converge skill's external (non-Claude) review pass becomes mandatory with teeth, dynamic, and safe:
|
|
9
|
+
|
|
10
|
+
- **gemini-cli joins the reviewer registry** (`src/core/crossModelReviewer.ts`) — the registry seam's first extension beyond codex: never-throws detection (binary + cached OAuth at the path the CLI actually reads), provider via the existing factory with its own circuit breaker, identical degraded semantics.
|
|
11
|
+
- **Family-diverse**: one external pass per AVAILABLE family (detect-all), not first-match-only.
|
|
12
|
+
- **Delta-gated**: a content hash of the spec's reviewable body (frontmatter-stripped) decides when externals must re-run — round 1 and any changed round; unchanged rounds record a skip-with-logged-note instead of burning external quota.
|
|
13
|
+
- **Durable activation baseline**: framework availability observations are recorded to `state/framework-activation-history.jsonl`; externals are non-skippable whenever a non-Claude framework was active within a 7-day lookback — deactivating a framework just before converging no longer exempts a spec.
|
|
14
|
+
- **Trusted-provider allowlist**: only first-party OAuth CLIs (codex-cli, gemini-cli) can receive spec text; pi-cli/custom endpoints are structurally excluded (`untrusted-framework` refusal).
|
|
15
|
+
- **Fail-loud model canary**: a review pass degrades loudly (`model-resolution-canary`) rather than silently running with an unresolved tier-word model.
|
|
16
|
+
|
|
17
|
+
Honest scope notes: the spec's "broken `resolveModelForFramework` foundation" was already fixed on main before this build (recorded at PR #1055); the Claude-only-agent different-family floor + tracked-gap signal are disclosed residuals (see the side-effects artifact).
|
|
18
|
+
|
|
19
|
+
## What to Tell Your User
|
|
20
|
+
|
|
21
|
+
- "When I review a spec before building, I now get a second opinion from EVERY non-Claude AI family I have access to (GPT via codex, Gemini via the gemini CLI) — automatically, on every round where the spec actually changed. I can't quietly skip it anymore: a record of which frameworks I've had active in the last week keeps the outside-opinion requirement honest."
|
|
22
|
+
- "Your spec text only ever goes to first-party AI providers you've logged into — never to custom or unknown endpoints."
|
|
23
|
+
|
|
24
|
+
## Summary of New Capabilities
|
|
25
|
+
|
|
26
|
+
| Capability | How to Use |
|
|
27
|
+
|-----------|-----------|
|
|
28
|
+
| Gemini external reviewer | Automatic in spec-converge when the gemini CLI is installed + authed |
|
|
29
|
+
| Family-diverse external pass | Automatic — one pass per available family per changed round |
|
|
30
|
+
| Delta-gated externals | Automatic — unchanged rounds log a skip note instead of re-reviewing |
|
|
31
|
+
| 7-day activation baseline | Automatic — recorded on every detection; read at convergence |
|
|
32
|
+
|
|
33
|
+
## Evidence
|
|
34
|
+
|
|
35
|
+
- 32 new unit tests (`crossModelReviewer-piece3.test.ts`) + 45 existing (`crossModelReviewer.test.ts`) + 3 integration (`cross-model-review-flow.test.ts`) — all green; `tsc` exit 0; full lint clean.
|
|
36
|
+
- Live smoke on the dev machine: detect-all reported codex-cli:gpt-5.5 AND gemini-cli:gemini-2.5-pro; `--family pi-cli` refused with `untrusted-framework`; the activation observation landed in the JSONL.
|
|
37
|
+
- Independent second-pass review verified the egress allowlist end-to-end and caught a real bug (a `GEMINI_HOME` env var the CLI doesn't actually honor) — fixed before ship.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Fixes the two live bugs from 2026-06-08 testing that left cross-machine dashboard streaming connecting-but-blank with no recovery (POOL-DASHBOARD-STREAM-SPEC, shipped #950–#970):
|
|
9
|
+
|
|
10
|
+
- **Closed-proxy eviction** (`WebSocketManager.peerProxyFor`): a peer's stream proxy that reached `closed` — after the bounded reconnect failed, or after the 60s idle grace once the last viewer left — stayed cached forever, and a closed proxy silently ignores every later subscribe while the server still answered `subscribed`. One hiccup (or one minute of nobody watching) made that machine permanently unstreamable until a server restart. The get-or-create chokepoint now evicts a closed proxy and opens a fresh episode with its own bounded reconnect budget (P19 no-storm guarantee preserved: reconnects remain one-per-episode, new episodes only on explicit user subscribes).
|
|
11
|
+
- **Cross-machine scrollback** (`history` relay): the terminal-history fetch only ever captured locally — structurally empty for a session owned by another machine. It now relays upstream for remote-subscribed sessions exactly like input/key (spec §2.2: capture happens ONLY on the owning machine), via a new read-only `PeerStreamProxy.relayFrame`; the dashboard client sends `machineId` on history requests for remote tiles.
|
|
12
|
+
- **Honest history miss**: the "no output for session" reply was a sessionless error frame, which the peer fan-out structurally drops; it now carries `session` + `code:'session-not-found'` and renders in the terminal (§2.4 failure honesty).
|
|
13
|
+
|
|
14
|
+
The serving side of the protocol is unchanged — an updated machine streaming from a not-yet-updated peer degrades to exactly today's behavior at worst. The remote-input default-off security gate is untouched.
|
|
15
|
+
|
|
16
|
+
## What to Tell Your User
|
|
17
|
+
|
|
18
|
+
- "Watching one machine's terminal from another machine's dashboard now actually shows the terminal — including scrolling back through its history — and if the link between machines hiccups, clicking the session again recovers it instead of staying dead until a restart."
|
|
19
|
+
|
|
20
|
+
## Summary of New Capabilities
|
|
21
|
+
|
|
22
|
+
| Capability | How to Use |
|
|
23
|
+
|-----------|-----------|
|
|
24
|
+
| Remote session scrollback | Automatic — scroll up in any remote session's terminal in the dashboard |
|
|
25
|
+
| Stream recovery after a dropped peer link | Automatic — re-clicking the session tile opens a fresh, bounded connection episode |
|
|
26
|
+
|
|
27
|
+
## Evidence
|
|
28
|
+
|
|
29
|
+
- 7 new unit tests (5 behavior + 2 regression guards) across `tests/unit/WebSocketManager.test.ts` and `tests/unit/PeerStreamProxy.test.ts`; each behavior test was run against the UNFIXED code first and failed for the stated reason (missing relayFrame, ignored re-subscribe after closed, sessionless error), then green with the fix.
|
|
30
|
+
- Full unit suite green in the worktree; `pnpm build` clean.
|
|
31
|
+
- Live verification on the real laptop + Mac Mini (a Mini session terminal rendering on the laptop dashboard, then recovery after an induced link drop) — performed post-deploy, results recorded in topic 13481.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Side-Effects Review — Cross-Model Convergence Hardening (Autonomy Principles Enforcement, Piece 3)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `cross-model-hardening`
|
|
4
|
+
**Date:** `2026-06-10`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `independent reviewer subagent (see appended verdict)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Implements Piece 3 of `docs/specs/AUTONOMY-PRINCIPLES-ENFORCEMENT-SPEC.md`: cross-model convergence becomes **mandatory (with a durable activation baseline), dynamic (no pinning), family-diverse, delta-gated, allowlist-constrained, and fail-loud**. Honest scope note: the spec's headline "broken foundation" claim (`resolveModelForFramework` returning a dead tier string for gemini/pi) was **already fixed on main** before this build — recorded at PR #1055; this PR implements everything else.
|
|
11
|
+
|
|
12
|
+
- **gemini-cli reviewer registry entry** (`src/core/crossModelReviewer.ts`) — the registry seam's first extension beyond codex: injectable never-throws detection (binary + cached OAuth creds), provider via the existing factory (own circuit breaker), identical degraded semantics.
|
|
13
|
+
- **Family-diverse collection** — `detectAllCrossModelReviewers()` returns ALL available frameworks; the skill now runs one external pass per available family instead of first-match-only.
|
|
14
|
+
- **Delta-gating** — `hashSpecReviewableBody()` (sha256, frontmatter-stripped, CRLF-normalized); externals are mandatory on round 1 + any round where the reviewable body changed; an unchanged round records a skip-with-logged-note (≠ skipped-abbreviated).
|
|
15
|
+
- **Durable activation baseline** — `recordFrameworkActivationObservation()` / `wasNonClaudeFrameworkActiveWithin()` (JSONL at `state/framework-activation-history.jsonl`, 2000-line cap): externals are non-skippable whenever a non-Claude framework was active at any point in a 7-day lookback — a just-before-converge deactivation no longer exempts a spec (round-2 adversarial F-R2-3).
|
|
16
|
+
- **Trusted-provider allowlist** — `TRUSTED_REVIEWER_FRAMEWORKS = ['codex-cli','gemini-cli']`; the registry constructively carries only first-party OAuth CLIs; `--family pi-cli` (or any unlisted id) is refused (`untrusted-framework`) so the full spec text is never sent to a custom/base-URL endpoint.
|
|
17
|
+
- **Fail-loud canary** — `isConcreteReviewerModel()`: both entries degrade LOUDLY (`model-resolution-canary`) rather than silently reviewing with a bare tier word.
|
|
18
|
+
|
|
19
|
+
Files: `src/core/crossModelReviewer.ts`, `skills/spec-converge/scripts/cross-model-review.mjs`, `skills/spec-converge/SKILL.md`, `tests/unit/crossModelReviewer-piece3.test.ts` (new, 32 tests), hermeticity fixes in 2 existing test files.
|
|
20
|
+
|
|
21
|
+
## Decision-point inventory
|
|
22
|
+
|
|
23
|
+
- Cross-model framework detection/selection — **modify** — first-match → all-available (collection only; no blocking authority).
|
|
24
|
+
- `--family` allowlist guard — **add** — hard-invariant refusal of an untrusted framework id at the script edge (egress protection, enumerable set).
|
|
25
|
+
- Externals-mandatory check (SKILL.md + `wasNonClaudeFrameworkActiveWithin`) — **add** — a deterministic read over the durable history that decides whether the SKILL FLOW may skip an external pass. Process-gating inside spec review; blocks no agent action or message.
|
|
26
|
+
- Model canary — **add** — degrades a review pass; blocks nothing.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 1. Over-block
|
|
31
|
+
|
|
32
|
+
The allowlist refusing `pi-cli` could be seen as over-blocking a legitimate reviewer — deliberate: pi is multi-provider by design (its model flag may resolve to a custom endpoint), and the cost of a wrongly-allowed egress (full spec text to an attacker-controlled model) dwarfs the cost of one fewer reviewer family. The activation lookback could keep externals mandatory for an agent that genuinely uninstalled a framework yesterday — accepted: the 7-day tail is the entire point (anti-gaming), and the pass degrades to advisory-unavailable rather than hard-failing when the framework is truly gone.
|
|
33
|
+
|
|
34
|
+
## 2. Under-block
|
|
35
|
+
|
|
36
|
+
The activation history only records when `--detect-only`/review runs execute — an agent that NEVER ran spec-converge in the lookback has an empty history and presents as single-framework. Acceptable: the first detect of the current convergence records the observation, so the gaming window is "deactivate before the FIRST round" — and a genuinely deactivated-everywhere agent degrades to the advisory floor exactly as specced. The history is also local-file mutable by the agent itself (not tamper-proof) — consistent with every other instar state file; the report banner + frontmatter flag remain the human-visible disclosure.
|
|
37
|
+
|
|
38
|
+
## 3. Level-of-abstraction fit
|
|
39
|
+
|
|
40
|
+
Correct: detection/selection/hash/history are cheap structural helpers in the existing crossModelReviewer module (the registry's documented extension point); the SKILL owns the flow policy; nothing re-implements the provider factory, circuit breaker, or aggregation (`aggregateRoundOutcomes` unchanged and still computes the spec-level flag).
|
|
41
|
+
|
|
42
|
+
## 4. Signal vs authority compliance
|
|
43
|
+
|
|
44
|
+
**Required reference:** docs/signal-vs-authority.md
|
|
45
|
+
|
|
46
|
+
- [x] No — no block/allow surface over agent behavior or messages. The two refusal points (allowlist, canary) are **hard-invariant validation** over enumerable sets (the principle's explicitly-allowed class — egress protection is the "safety guards on irreversible actions" case). Review outcomes only ever DEGRADE with logged reasons; judgment stays with the reviewers and the convergence process.
|
|
47
|
+
|
|
48
|
+
## 5. Interactions
|
|
49
|
+
|
|
50
|
+
- **Back-compat:** `detectCrossModelReviewer` (first-match) and the script's default single-pass mode are unchanged; existing callers/tests pass (hermeticity fixes only — the suite previously assumed gemini absent, false on hosts with gemini installed).
|
|
51
|
+
- **Aggregation:** per-family results feed the existing per-round → spec-level aggregation unchanged; a delta-skip is recorded as a logged note, not a round outcome, so `degraded-all-rounds` semantics are preserved.
|
|
52
|
+
- **Double-fire/races:** the activation JSONL is append-with-cap from a single skill flow; worst case under concurrent convergences is a lost observation line (history is a union-over-time read, so this cannot flip mandatory→optional within the lookback).
|
|
53
|
+
- **Circuit breakers:** each framework keeps its own breaker via the factory — a rate-limited codex degrades that pass; gemini still runs (the family-diversity goal).
|
|
54
|
+
|
|
55
|
+
## 6. External surfaces
|
|
56
|
+
|
|
57
|
+
- **LLM egress:** spec text now also flows to gemini (first-party Google OAuth CLI) when available — the same egress class as the existing codex pass, now explicitly allowlist-bounded. No new third-party endpoints; pi/custom endpoints structurally excluded.
|
|
58
|
+
- **Persistent state:** `state/framework-activation-history.jsonl` (capped). No fleet surface: spec-converge remains agent-private (decision recorded in the Piece-2 artifact); no `src/` runtime route/config change — `crossModelReviewer.ts` is library code consumed by the skill script.
|
|
59
|
+
|
|
60
|
+
## 7. Rollback cost
|
|
61
|
+
|
|
62
|
+
Revert the commit. The history file is inert if orphaned. No migration, no API, no fleet propagation.
|
|
63
|
+
|
|
64
|
+
## Conclusion
|
|
65
|
+
|
|
66
|
+
Piece 3 closes the spec: externals can no longer be quietly skipped (durable 7-day baseline + delta-gating with logged notes), selection is dynamic and family-diverse with zero pinned model names, and the two real security findings from convergence (spec egress to untrusted endpoints; silent dead-model reviews) are structurally closed (allowlist + canary). Verified: tsc clean, full lint clean, 77 unit (45 existing + 32 new) + 3 integration green, live smoke on this host detected both codex + gemini and refused pi-cli. Clear to ship as the final PR of the spec.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Scope disclosure (honest residual)
|
|
71
|
+
|
|
72
|
+
Two spec clauses are NOT implemented in this PR and are tracked, not orphaned <!-- tracked: CMT-1317 -->: (a) the Claude-only-agent floor reaching a *different-family* model via the subscription-path/provider-registry fallback — a single-framework agent currently gets the honest advisory `unavailable` flag (the pre-existing behavior, loudly disclosed in the report banner), and (b) surfacing "no external family reachable at all" as a HumanAsDetector-style tracked-gap signal rather than the advisory note. Both need provider-registry plumbing that doesn't belong in this hardening pass.
|
|
73
|
+
|
|
74
|
+
## Second-pass review
|
|
75
|
+
|
|
76
|
+
**Reviewer:** independent reviewer subagent (verified findings against the actually-installed gemini CLI v0.25.2).
|
|
77
|
+
**Independent read of the artifact: concern raised → all resolved this pass.**
|
|
78
|
+
|
|
79
|
+
- MUST-FIX (resolved): `GEMINI_HOME` is not a real gemini CLI env var (zero occurrences in the CLI dist; creds are unconditionally `~/.gemini/oauth_creds.json`). Honoring it made detection probe a path the CLI never reads — a false-unavailable would silently skip the gemini pass AND poison the activation baseline with `gemini-cli:false`, the exact suppression Piece 3 prevents. Fixed: env lookup dropped; injectable test seam kept.
|
|
80
|
+
- NICE-TO-HAVE (applied): frontmatter-strip close anchor tightened to a whole-line fence (`\n---(\n|$)`) so `--- text`/`----` inside the block can't terminate the strip mid-line.
|
|
81
|
+
- INFORMATIONAL (applied anyway): `wasNonClaudeFrameworkActiveWithin` now counts only TRUSTED reviewer framework ids, so a stray `{"claude-code":true}` line can't flip the externals-mandatory decision.
|
|
82
|
+
- HONESTY (applied): the scope disclosure above was added at the reviewer's prompting — the artifact previously implied full Piece-3 coverage.
|
|
83
|
+
- Confirmed clean: `--family` allowlist guard sits before any detect/provider call; no path (default, detect-all, or detectionOverride) can route spec text to a non-registry framework — the registry⊆allowlist invariant is itself unit-tested; `buildIntelligenceProvider({framework:'gemini-cli'})` cannot reach a custom base-URL (allowlisted child env); refresh-token-only IS authed for this CLI; the canary precedes provider construction in both entries; the 2000-line cap keeps the most recent lines; the reader never throws on corruption; the existing-test edits weaken no assertion.
|
|
84
|
+
|
|
85
|
+
After fixes: tsc exit 0, 77/77 unit + 3/3 integration green.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Evidence pointers
|
|
90
|
+
|
|
91
|
+
- `tests/unit/crossModelReviewer-piece3.test.ts` (32): gemini detect triad, detect-all cardinality, canary accept/reject, hash frontmatter/CRLF stability, activation write/read/lookback/corrupt-line/cap, allowlist, gemini degraded paths.
|
|
92
|
+
- Live smoke: `--detect-only --state-dir` → codex-cli:gpt-5.5 + gemini-cli:gemini-2.5-pro both detected + observation written; `--family pi-cli` → `untrusted-framework`; `--hash-only` → stable hash.
|
|
93
|
+
- `npx tsc --noEmit` exit 0; `npm run lint` exit 0.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Side-Effects Review — Pool Dashboard Streaming: cross-machine history relay + closed-proxy recovery
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `pool-stream-cross-machine-fix`
|
|
4
|
+
**Date:** `2026-06-11`
|
|
5
|
+
**Author:** `echo (instar-dev agent)`
|
|
6
|
+
**Second-pass reviewer:** `not required` (no block/allow surface on messaging/dispatch, no session lifecycle, no sentinel/gate/watchdog logic — see Phase-5 assessment in Conclusion)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Fixes the two live bugs found during 2026-06-08 live testing of Pool Dashboard Streaming (POOL-DASHBOARD-STREAM-SPEC, converged-approved 2026-06-06), which left a remote (Mac Mini) session tile connecting but rendering a blank terminal, with no recovery until server restart:
|
|
11
|
+
|
|
12
|
+
1. **Closed-proxy eviction** (`src/server/WebSocketManager.ts` → `peerProxyFor`): a `PeerStreamProxy` that reached `closed` (idle-grace close after the last viewer left, or `machine-unreachable` after the bounded reconnect failed) stayed cached in `peerProxies` forever. A closed proxy ignores every further subscribe by design, while the subscribe handler still answered `subscribed` — so one upstream hiccup (or 60s of nobody watching) made that peer permanently unstreamable, silently. `peerProxyFor` now evicts a closed proxy and creates a fresh one; a fresh user-initiated subscribe is a fresh episode with its own bounded reconnect budget (P19 preserved).
|
|
13
|
+
2. **Cross-machine history relay** (`src/server/WebSocketManager.ts` `history` case, `src/server/PeerStreamProxy.ts` `relayFrame`, `dashboard/index.html` `loadMoreHistory`): the scrollback fetch (`{type:'history'}`) had no remote branch — it always called local `captureOutput`, which can only return empty for a session owned by another machine (spec §2.2: capture happens ONLY on the owning machine). The handler now relays history upstream for a remote-subscribed session exactly like input/key, the proxy gained a read-only `relayFrame` (relayInput delegates to it), and the dashboard client sends `machineId` on history requests for remote sessions.
|
|
14
|
+
3. **Relayable history miss** (same `history` case): the local "no output" reply was `{type:'error', message}` with NO session field — the peer fan-out drops sessionless frames, so the error could never travel. It now carries `session` + `code:'session-not-found'`, which the dashboard already renders honestly (§2.4).
|
|
15
|
+
|
|
16
|
+
Files: `src/server/WebSocketManager.ts`, `src/server/PeerStreamProxy.ts`, `dashboard/index.html`, `tests/unit/WebSocketManager.test.ts`, `tests/unit/PeerStreamProxy.test.ts`.
|
|
17
|
+
|
|
18
|
+
## Decision-point inventory
|
|
19
|
+
|
|
20
|
+
- `WebSocketManager.peerProxyFor` — modify — get-or-create now evicts a `closed` proxy; routing decision (which proxy serves a peer), not a block/allow decision.
|
|
21
|
+
- `WebSocketManager.handleMessage 'history'` — modify — adds the remote-routing branch (same shape as the existing `input`/`key` remote branches); read-only data fetch, no authority.
|
|
22
|
+
- `PeerStreamProxy.relayFrame` — add — forwards a read-only frame while the link is active; drops otherwise (same drop semantics relayInput always had).
|
|
23
|
+
- `gateWrite` (remote input authority, `poolStreamAllowRemoteInput`) — **pass-through, untouched** — history is read-only and never reaches `gateWrite`; the input default-off security gate is not modified by this change.
|
|
24
|
+
- Stream ticket auth (`StreamTicketStore`, `/pool-stream` upgrade) — **pass-through, untouched**.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 1. Over-block
|
|
29
|
+
|
|
30
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
31
|
+
|
|
32
|
+
No issue identified. The change adds no new rejection. The history remote branch only triggers when the client explicitly sent a `machineId` AND that client already holds a live remote subscription for exactly that `(machine, session)` pair (`client.remoteSubs.has(...)`) — a history request for a never-subscribed remote session falls through to the local path and answers `session-not-found`, which is the honest answer (you cannot scroll back a session you are not watching). Local history requests (no machineId) are byte-for-byte unchanged.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 2. Under-block
|
|
37
|
+
|
|
38
|
+
**What failure modes does this still miss?**
|
|
39
|
+
|
|
40
|
+
- A history request relayed while the upstream is `connecting` (mid-reconnect) is dropped, not queued — the user's infinite-scroll simply doesn't load that batch and re-fires on the next scroll (the client re-requests on scroll position). Stale queued requests racing a reconnect would be worse. Accepted, documented in `relayFrame`'s contract.
|
|
41
|
+
- The history reply fans to EVERY local client subscribed to that (machine, session), not just the requester — same multiplexing behavior the spec defines for all frames (§2.2 "frames relay 1:1"); clients gate rendering on `activeSession`, so a non-requesting viewer at most refreshes its scrollback.
|
|
42
|
+
- The 2026-06-07 mint-timeout failure class (wedged ticket mint) is already handled by the explicit 10s timeout shipped in #970, upstream of this change.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 3. Level-of-abstraction fit
|
|
47
|
+
|
|
48
|
+
**Is this at the right layer?**
|
|
49
|
+
|
|
50
|
+
Yes. The history relay is placed at exactly the layer the spec assigned it: the SUBSCRIBE-side routing chokepoint in `WebSocketManager.handleMessage`, parallel to the existing `input`/`key` remote branches — not a new parallel path. `relayFrame` lives in `PeerStreamProxy` because that class owns "is the upstream link usable" state; the manager owns "is this session remote for this client" state. The eviction lives in `peerProxyFor` (the single get-or-create chokepoint) rather than inside the proxy (e.g. a self-reopening proxy), keeping the proxy's one-shot bounded-reconnect state machine — and its P19 guarantee — untouched.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 4. Signal vs authority compliance
|
|
55
|
+
|
|
56
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
57
|
+
|
|
58
|
+
- [x] No — this change has no block/allow surface.
|
|
59
|
+
|
|
60
|
+
The change is pure data-plane plumbing (routing read-only frames to the machine that owns the data, and replacing a dead connection object). The ONLY authority in this subsystem — the serving machine's `poolStreamAllowRemoteInput` gate on remote keystrokes — is not touched, weakened, or bypassed: history never reaches `gateWrite` because it is a read, and reads were already permitted to any authenticated peer link (the initial subscribe capture has always shipped full screen content to the peer). No new information becomes visible to any principal that could not already see it.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 5. Interactions
|
|
65
|
+
|
|
66
|
+
- **Shadowing:** the remote history branch runs BEFORE the local capture path, gated on `client.remoteSubs` — the local path is unreachable only for sessions the client explicitly subscribed remotely, which is exactly the set local capture could never serve (it returned null for them). Nothing that previously worked is shadowed.
|
|
67
|
+
- **Double-fire:** none — a history request takes exactly one branch (remote XOR local). Eviction cannot double-open: `peerProxyFor` is synchronous, and only a `closed` proxy (no live transport, no pending timers except a fired/cleared one) is evicted, so no second live upstream to the same peer can exist.
|
|
68
|
+
- **Races:** a client disconnect between failAll and re-subscribe calls `dropRemoteSubsForClient`, which looks up the CURRENT map entry — if eviction already replaced the proxy, the unsubscribe lands on the fresh proxy where `unsubscribe()` of an unknown clientId is a documented no-op. An old evicted proxy holds no timers and no transport (failAll/idle-close tear both down), so it is plain garbage, not a leak.
|
|
69
|
+
- **Feedback loops:** none — the eviction does not auto-reconnect; it only acts on the next explicit user subscribe, so a permanently-down peer costs one bounded reconnect cycle per user click, never a storm.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 6. External surfaces
|
|
74
|
+
|
|
75
|
+
- **Other machines:** the serving side of the protocol is UNCHANGED — an updated laptop streaming from a not-yet-updated Mini works (the Mini's serving side always answered history requests arriving on a peer link; the bug was the laptop never sending them). Mixed-version fleets degrade to exactly today's behavior at worst.
|
|
76
|
+
- **Protocol:** no new frame types; `history` was already in the documented protocol and in `UpstreamTransport.send`'s doc comment ("subscribe/unsubscribe/input/key/history"). The local history-miss error gains `code` + `session` fields (additive; the dashboard's existing `error` handler renders `code`-carrying frames and still `console.error`s unknown shapes).
|
|
77
|
+
- **Persistent state:** none touched. No config, no migrations, no ledgers.
|
|
78
|
+
- **Timing:** `relayFrame` depends on the upstream being `active` — bounded, observable via the existing `[pool-stream-connector]` log lines.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 7. Rollback cost
|
|
83
|
+
|
|
84
|
+
Pure code change in three files with no persistent state: revert the commit, ship as the next patch. During a rollback window the symptom is exactly the pre-fix behavior (blank remote scrollback, no post-hiccup recovery) — degraded, not destructive. No data migration, no agent state repair, no template/hook migration (dashboard/index.html ships in the npm package and replaces wholesale on update).
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Conclusion
|
|
89
|
+
|
|
90
|
+
Review produced one design adjustment: the local history-miss error was originally left sessionless; the relay analysis (§5 fan-out drops sessionless frames) surfaced that it could never travel cross-machine, so it now carries `session` + `code` — that is also what makes the dashboard render it honestly instead of burying it in the console. Phase-5 second-pass assessment: the change touches none of the listed high-risk surfaces (no outbound/inbound messaging block decisions, no session spawn/restart/kill/recovery, no compaction, no coherence gates/idempotency/trust, no sentinel/guard/gate/watchdog logic — the one gate in this subsystem, remote-input default-off, is untouched pass-through), so a dedicated reviewer subagent is not required; the artifact stands on the three-tier test evidence. Clear to ship.
|
|
91
|
+
|
|
92
|
+
**Phase 1 principle check (recorded):** the change involves no new decision point that gates information flow or constrains behavior — it repairs the data plane of an approved feature to match its converged spec (§2.2 capture-on-owner, §2.4 failure honesty, P19 bounded reconnect). Signal-vs-authority applies as a constraint check only: confirmed no brittle logic gained blocking authority.
|
|
93
|
+
|
|
94
|
+
**Phase 2 plan (recorded):** built in a FRESH worktree `.worktrees/fix-pool-stream-cross-machine` off `JKHeadley/main` @ `60c4e3a3c` (v1.3.484), canonical remote verified (`JKHeadley → github.com/JKHeadley/instar.git`), per-worktree identity `Instar Agent (echo) <echo@instar.local>`. Acceptance criteria: (a) new unit tests fail on unfixed code for the stated reasons — verified (5 failures: missing relayFrame ×2, ignored re-subscribe ×2 → silent blank, sessionless error ×1); (b) all green with fix; (c) full suite green; (d) live verify on the real laptop+Mini after deploy. Rollback: revert commit (see §7).
|