instar 1.3.1144 → 1.3.1146
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/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/package.json +1 -1
- package/scripts/lint-journal-actuation-ban.js +116 -10
- package/scripts/lint-no-direct-llm-http.js +159 -1
- package/src/data/builtin-manifest.json +2 -2
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/upgrades/1.3.1145.md +57 -0
- package/upgrades/1.3.1146.md +22 -0
- package/upgrades/side-effects/journal-actuation-ban-load-forms.md +127 -0
- package/upgrades/side-effects/llm-http-constant-folding.md +82 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1146",
|
|
6
6
|
"guards": [
|
|
7
7
|
{
|
|
8
8
|
"ref": "docs/audits/phase-b/f10-triage.md",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sha256": "
|
|
2
|
+
"sha256": "675498ac7f3b9a5fd07a57a530eba5cc8645d84aede82b6f03348f7f502edb8a",
|
|
3
3
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1146"
|
|
5
5
|
}
|
package/package.json
CHANGED
|
@@ -12,8 +12,37 @@
|
|
|
12
12
|
* separate module from the writer so this ban has a precise import target —
|
|
13
13
|
* actuators MAY hold the writer (they emit), they may never read.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* ── 2026-08-14: the header used to declare TWO limitations. One is now closed,
|
|
16
|
+
* one is NOT, and pretending otherwise would be the worse outcome.
|
|
17
|
+
*
|
|
18
|
+
* CLOSED — "this catches the direct pattern". The match required the `from`
|
|
19
|
+
* keyword, so `await import(...)` and `require(...)` walked straight past it.
|
|
20
|
+
* Reproduced against the shipped lint on a REAL listed actuator: it reported
|
|
21
|
+
* "clean". Now every module-loading form is matched, comments are stripped
|
|
22
|
+
* first (so a §3.9 reference in prose can never be a violation), and a
|
|
23
|
+
* runtime-erased `import type` is correctly NOT a violation.
|
|
24
|
+
*
|
|
25
|
+
* NOT CLOSED — "grow this list when a new actuator class lands". The population
|
|
26
|
+
* is still curated by hand. Automatic discovery was BUILT, MEASURED AGAINST THIS
|
|
27
|
+
* TREE, AND REJECTED on the evidence: inferring "is this an actuator?" from
|
|
28
|
+
* declared names flagged nine sites in three files, and every one was a
|
|
29
|
+
* granularity or part-of-speech error rather than a §3.9 breach —
|
|
30
|
+
* · `src/server/routes.ts` matched on `readReaperPeerText`, `isReaperSnapshot`,
|
|
31
|
+
* `reaperPoolHealth`: "reaper" as a NOUN in code that REPORTS ON the reaper,
|
|
32
|
+
* which is exactly the correct-code case (a reporting surface may read).
|
|
33
|
+
* · `src/commands/server.ts` is the 22k-line composition root — every module's
|
|
34
|
+
* authority is wired through it, so any file-level verdict on it is wrong in
|
|
35
|
+
* one direction or the other.
|
|
36
|
+
* · `src/core/WorkingSetPull.ts` matched a runtime-erased `import type`.
|
|
37
|
+
* A guard that blocks commits must not rest on a heuristic with that error rate;
|
|
38
|
+
* over-blocking correct code is the more expensive failure here. What IS closed
|
|
39
|
+
* mechanically is the STALENESS half: a curated entry that no longer exists on
|
|
40
|
+
* disk means an actuator was renamed or moved and silently fell off the ban —
|
|
41
|
+
* that is now a hard failure instead of a skipped line.
|
|
42
|
+
*
|
|
43
|
+
* Still a guardrail, not a proof: a determined consumer can re-read the JSONL by
|
|
44
|
+
* hand or reach the reader through a re-export. The declared §3.9 duty is the
|
|
45
|
+
* authority; this catches the mechanical patterns.
|
|
17
46
|
*/
|
|
18
47
|
import fs from 'node:fs';
|
|
19
48
|
import path from 'node:path';
|
|
@@ -27,7 +56,8 @@ const ROOT = process.argv.includes('--root')
|
|
|
27
56
|
/**
|
|
28
57
|
* Actuator modules: anything holding kill/spawn/place/transfer/reap authority.
|
|
29
58
|
* Grow this list when a new actuator class lands — adding here is cheap;
|
|
30
|
-
* debugging a journal-driven double-kill is not.
|
|
59
|
+
* debugging a journal-driven double-kill is not. See the header for why this
|
|
60
|
+
* stays curated rather than inferred.
|
|
31
61
|
*/
|
|
32
62
|
const ACTUATOR_FILES = [
|
|
33
63
|
'src/core/SessionManager.ts',
|
|
@@ -40,17 +70,93 @@ const ACTUATOR_FILES = [
|
|
|
40
70
|
'src/lifeline/ServerSupervisor.ts',
|
|
41
71
|
];
|
|
42
72
|
|
|
43
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Every way a module can LOAD the reader at runtime. The old pattern required
|
|
75
|
+
* `from`, which is precisely the token a dynamic import does not have.
|
|
76
|
+
*/
|
|
77
|
+
const LOADS_READER =
|
|
78
|
+
/(?:\bfrom\s+|\bimport\s*\(\s*|\brequire\s*\(\s*)['"][^'"]*CoherenceJournalReader(?:\.js)?['"]/;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* `import type { X } from '…Reader.js'` is erased at compile time — it creates
|
|
82
|
+
* no runtime coupling and therefore cannot act on stale data. Borrowing a TYPE
|
|
83
|
+
* from the reader is legal; holding the reader is not. A MIXED import such as
|
|
84
|
+
* `import { type A, CoherenceJournalReader }` does not match this and is still
|
|
85
|
+
* caught, which is correct — it pulls in the runtime binding.
|
|
86
|
+
*/
|
|
87
|
+
const TYPE_ONLY_IMPORT = /^\s*import\s+type\s/;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Blank out comments, preserving length and line count so reported line numbers
|
|
91
|
+
* stay true. Quote-aware, so a `//` inside a string literal is not mistaken for
|
|
92
|
+
* a comment. This is what keeps "the reader named in a comment" legal no matter
|
|
93
|
+
* how wide the load-matching gets.
|
|
94
|
+
*/
|
|
95
|
+
function stripComments(src) {
|
|
96
|
+
const out = src.split('');
|
|
97
|
+
let i = 0;
|
|
98
|
+
let state = 'code'; // code | line | block | single | double | tick
|
|
99
|
+
while (i < src.length) {
|
|
100
|
+
const c = src[i];
|
|
101
|
+
const d = src[i + 1];
|
|
102
|
+
if (state === 'code') {
|
|
103
|
+
if (c === '/' && d === '/') { state = 'line'; out[i] = ' '; out[i + 1] = ' '; i += 2; continue; }
|
|
104
|
+
if (c === '/' && d === '*') { state = 'block'; out[i] = ' '; out[i + 1] = ' '; i += 2; continue; }
|
|
105
|
+
if (c === "'") state = 'single';
|
|
106
|
+
else if (c === '"') state = 'double';
|
|
107
|
+
else if (c === '`') state = 'tick';
|
|
108
|
+
i++; continue;
|
|
109
|
+
}
|
|
110
|
+
if (state === 'line') {
|
|
111
|
+
if (c === '\n') { state = 'code'; i++; continue; }
|
|
112
|
+
out[i] = ' '; i++; continue;
|
|
113
|
+
}
|
|
114
|
+
if (state === 'block') {
|
|
115
|
+
if (c === '*' && d === '/') { state = 'code'; out[i] = ' '; out[i + 1] = ' '; i += 2; continue; }
|
|
116
|
+
if (c !== '\n') out[i] = ' ';
|
|
117
|
+
i++; continue;
|
|
118
|
+
}
|
|
119
|
+
// inside a string literal: honour escapes, then look for the closing quote
|
|
120
|
+
if (c === '\\') { i += 2; continue; }
|
|
121
|
+
if ((state === 'single' && c === "'") || (state === 'double' && c === '"') || (state === 'tick' && c === '`')) {
|
|
122
|
+
state = 'code';
|
|
123
|
+
}
|
|
124
|
+
i++;
|
|
125
|
+
}
|
|
126
|
+
return out.join('');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A lint that reports "clean" because it scanned NOTHING is worse than no lint:
|
|
131
|
+
* absence is the cheapest result to obtain, and it is indistinguishable from a
|
|
132
|
+
* genuinely clean tree. Refuse to render a verdict on a root with no src/.
|
|
133
|
+
*/
|
|
134
|
+
if (!fs.existsSync(path.join(ROOT, 'src'))) {
|
|
135
|
+
console.error(`lint-journal-actuation-ban: no src/ under ${ROOT} — scanned nothing, so no verdict.`);
|
|
136
|
+
process.exit(2);
|
|
137
|
+
}
|
|
44
138
|
|
|
45
139
|
const violations = [];
|
|
140
|
+
|
|
46
141
|
for (const rel of ACTUATOR_FILES) {
|
|
47
142
|
const file = path.join(ROOT, rel);
|
|
48
|
-
|
|
49
|
-
|
|
143
|
+
|
|
144
|
+
// The staleness half of the declared-population gap: a curated actuator that
|
|
145
|
+
// is no longer here was renamed or moved, and silently left the ban behind.
|
|
146
|
+
if (!fs.existsSync(file)) {
|
|
147
|
+
violations.push(
|
|
148
|
+
`${rel}: listed actuator is missing from the tree — renamed or moved? It has silently left the §3.9 ban. Update ACTUATOR_FILES.`,
|
|
149
|
+
);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const lines = stripComments(fs.readFileSync(file, 'utf-8')).split('\n');
|
|
50
154
|
for (let i = 0; i < lines.length; i++) {
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
155
|
+
if (!LOADS_READER.test(lines[i])) continue;
|
|
156
|
+
if (TYPE_ONLY_IMPORT.test(lines[i])) continue;
|
|
157
|
+
violations.push(
|
|
158
|
+
`${rel}:${i + 1}: actuator loads the journal READER (forbidden by §3.9 — the journal answers questions, live systems decide)`,
|
|
159
|
+
);
|
|
54
160
|
}
|
|
55
161
|
}
|
|
56
162
|
|
|
@@ -60,4 +166,4 @@ if (violations.length > 0) {
|
|
|
60
166
|
console.error('\nReplicated journal data is stale by construction. Read the live store instead.');
|
|
61
167
|
process.exit(1);
|
|
62
168
|
}
|
|
63
|
-
console.log(`lint-journal-actuation-ban: clean (${ACTUATOR_FILES.length} actuator modules, none
|
|
169
|
+
console.log(`lint-journal-actuation-ban: clean (${ACTUATOR_FILES.length} actuator modules, none load the reader)`);
|
|
@@ -98,6 +98,162 @@ const FORBIDDEN_PATTERNS = [
|
|
|
98
98
|
|
|
99
99
|
const EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.mjs', '.cjs']);
|
|
100
100
|
|
|
101
|
+
function lineForOffset(text, offset) {
|
|
102
|
+
let line = 1;
|
|
103
|
+
for (let i = 0; i < offset; i++) {
|
|
104
|
+
if (text[i] === '\n') line++;
|
|
105
|
+
}
|
|
106
|
+
return line;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function stripCommentsPreserveLayout(text) {
|
|
110
|
+
let out = '';
|
|
111
|
+
let i = 0;
|
|
112
|
+
let quote = null;
|
|
113
|
+
let escaped = false;
|
|
114
|
+
|
|
115
|
+
while (i < text.length) {
|
|
116
|
+
const ch = text[i];
|
|
117
|
+
const next = text[i + 1];
|
|
118
|
+
|
|
119
|
+
if (quote) {
|
|
120
|
+
out += ch;
|
|
121
|
+
if (escaped) {
|
|
122
|
+
escaped = false;
|
|
123
|
+
} else if (ch === '\\') {
|
|
124
|
+
escaped = true;
|
|
125
|
+
} else if (ch === quote) {
|
|
126
|
+
quote = null;
|
|
127
|
+
}
|
|
128
|
+
i++;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
133
|
+
quote = ch;
|
|
134
|
+
out += ch;
|
|
135
|
+
i++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (ch === '/' && next === '/') {
|
|
140
|
+
out += ' ';
|
|
141
|
+
i += 2;
|
|
142
|
+
while (i < text.length && text[i] !== '\n') {
|
|
143
|
+
out += ' ';
|
|
144
|
+
i++;
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (ch === '/' && next === '*') {
|
|
150
|
+
out += ' ';
|
|
151
|
+
i += 2;
|
|
152
|
+
while (i < text.length) {
|
|
153
|
+
if (text[i] === '*' && text[i + 1] === '/') {
|
|
154
|
+
out += ' ';
|
|
155
|
+
i += 2;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
out += text[i] === '\n' ? '\n' : ' ';
|
|
159
|
+
i++;
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
out += ch;
|
|
165
|
+
i++;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function skipWhitespace(text, index) {
|
|
172
|
+
while (index < text.length && /\s/.test(text[index])) index++;
|
|
173
|
+
return index;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function readStringLiteral(text, start) {
|
|
177
|
+
const quote = text[start];
|
|
178
|
+
if (quote !== '"' && quote !== "'" && quote !== '`') return null;
|
|
179
|
+
|
|
180
|
+
let value = '';
|
|
181
|
+
let i = start + 1;
|
|
182
|
+
let escaped = false;
|
|
183
|
+
while (i < text.length) {
|
|
184
|
+
const ch = text[i];
|
|
185
|
+
if (escaped) {
|
|
186
|
+
value += ch;
|
|
187
|
+
escaped = false;
|
|
188
|
+
i++;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (ch === '\\') {
|
|
192
|
+
escaped = true;
|
|
193
|
+
i++;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (quote === '`' && ch === '$' && text[i + 1] === '{') {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
if (ch === quote) {
|
|
200
|
+
return { value, end: i + 1 };
|
|
201
|
+
}
|
|
202
|
+
value += ch;
|
|
203
|
+
i++;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function findFoldedStringViolations(text, rel) {
|
|
210
|
+
const stripped = stripCommentsPreserveLayout(text);
|
|
211
|
+
const violations = [];
|
|
212
|
+
|
|
213
|
+
for (let i = 0; i < stripped.length; i++) {
|
|
214
|
+
const first = readStringLiteral(stripped, i);
|
|
215
|
+
if (!first) continue;
|
|
216
|
+
|
|
217
|
+
let cursor = skipWhitespace(stripped, first.end);
|
|
218
|
+
if (stripped[cursor] !== '+') {
|
|
219
|
+
i = first.end - 1;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const parts = [first.value];
|
|
224
|
+
let end = first.end;
|
|
225
|
+
let literalCount = 1;
|
|
226
|
+
while (stripped[cursor] === '+') {
|
|
227
|
+
const nextStart = skipWhitespace(stripped, cursor + 1);
|
|
228
|
+
const nextString = readStringLiteral(stripped, nextStart);
|
|
229
|
+
if (!nextString) break;
|
|
230
|
+
parts.push(nextString.value);
|
|
231
|
+
literalCount++;
|
|
232
|
+
end = nextString.end;
|
|
233
|
+
cursor = skipWhitespace(stripped, nextString.end);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (literalCount > 1) {
|
|
237
|
+
const folded = parts.join('');
|
|
238
|
+
const line = lineForOffset(stripped, i);
|
|
239
|
+
for (const pat of FORBIDDEN_PATTERNS) {
|
|
240
|
+
if (folded.includes(pat)) {
|
|
241
|
+
violations.push({
|
|
242
|
+
file: rel,
|
|
243
|
+
line,
|
|
244
|
+
pattern: pat,
|
|
245
|
+
text: folded.slice(0, 200),
|
|
246
|
+
folded: true,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
i = end - 1;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return violations;
|
|
255
|
+
}
|
|
256
|
+
|
|
101
257
|
function readGitignoreDirs() {
|
|
102
258
|
// Skip node_modules, dist, .instar/worktrees, etc.
|
|
103
259
|
return new Set(['node_modules', 'dist', 'build', '.instar', '.git', '.next', 'coverage']);
|
|
@@ -168,6 +324,7 @@ function checkFile(file) {
|
|
|
168
324
|
}
|
|
169
325
|
}
|
|
170
326
|
}
|
|
327
|
+
violations.push(...findFoldedStringViolations(text, rel));
|
|
171
328
|
return violations;
|
|
172
329
|
}
|
|
173
330
|
|
|
@@ -184,7 +341,8 @@ function main() {
|
|
|
184
341
|
console.error('src/core/ClaudeCliIntelligenceProvider.ts so the burn-detection system can');
|
|
185
342
|
console.error('attribute it. See docs/specs/token-burn-detection-and-self-heal.md Phase 1.\n');
|
|
186
343
|
for (const v of all) {
|
|
187
|
-
|
|
344
|
+
const reason = v.folded ? `folds to "${v.pattern}"` : `contains "${v.pattern}"`;
|
|
345
|
+
console.error(` ${v.file}:${v.line} — ${reason}`);
|
|
188
346
|
console.error(` ${v.text}`);
|
|
189
347
|
}
|
|
190
348
|
process.exit(1);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-08-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-08-14T22:54:06.076Z",
|
|
5
|
+
"instarVersion": "1.3.1146",
|
|
6
6
|
"entryCount": 202,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1146",
|
|
6
6
|
"guards": [
|
|
7
7
|
{
|
|
8
8
|
"ref": "docs/audits/phase-b/f10-triage.md",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sha256": "
|
|
2
|
+
"sha256": "675498ac7f3b9a5fd07a57a530eba5cc8645d84aede82b6f03348f7f502edb8a",
|
|
3
3
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1146"
|
|
5
5
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
`scripts/lint-journal-actuation-ban.js` enforces COHERENCE-JOURNAL-SPEC §3.9: no actuator (kill / spawn / place /
|
|
9
|
+
transfer / reap) may import the journal READER, because replicated journal data is stale by construction and an
|
|
10
|
+
actuator trusting it can kill live work or double-place a conversation.
|
|
11
|
+
|
|
12
|
+
The check matched `/from\s+['"]…CoherenceJournalReader…['"]/`, which requires the `from` keyword. `await
|
|
13
|
+
import(…)` and `require(…)` do not have it, so both loaded the reader straight past the ban. Reproduced against
|
|
14
|
+
the shipped lint on a real listed actuator (`src/core/SessionManager.ts`): it printed `clean`. instar-codey
|
|
15
|
+
reproduced the same evasion independently and ranked this lint first of ~20 by consequence-of-defeat.
|
|
16
|
+
|
|
17
|
+
- All three load forms now match.
|
|
18
|
+
- Comments are stripped (quote-aware, line-preserving) before matching, so prose describing the ban can never
|
|
19
|
+
itself be a violation.
|
|
20
|
+
- `import type` is now exempt — it is erased at compile time, so it cannot act on stale data. The old pattern
|
|
21
|
+
flagged it, and a live file in the tree has that shape legitimately.
|
|
22
|
+
- A curated actuator that has vanished from the tree is now a violation instead of a silently skipped line: a
|
|
23
|
+
renamed module used to leave the ban without a word.
|
|
24
|
+
- A root with no `src/` exits 2 instead of printing `clean` over zero files.
|
|
25
|
+
|
|
26
|
+
Deliberately NOT changed: the curated actuator list. Automatic discovery by declared-name shape was built and
|
|
27
|
+
measured against this tree — nine flags across three files, every one a part-of-speech or granularity error
|
|
28
|
+
("reaper" as a noun in reporting code; the 22k-line composition root; an `import type`). This lint blocks
|
|
29
|
+
commits, so over-blocking correct code is the more expensive failure, and the enumerated list is the original
|
|
30
|
+
converged design decision. The limit is now stated plainly in the header and pinned by a test that fails if
|
|
31
|
+
anyone closes it properly.
|
|
32
|
+
|
|
33
|
+
## What to Tell Your User
|
|
34
|
+
|
|
35
|
+
None — internal change (no user-facing surface).
|
|
36
|
+
|
|
37
|
+
## Summary of New Capabilities
|
|
38
|
+
|
|
39
|
+
None — internal change (no user-facing surface).
|
|
40
|
+
|
|
41
|
+
## Evidence
|
|
42
|
+
|
|
43
|
+
- `tests/unit/journal-actuation-ban-lint.test.ts` — 13/13 green.
|
|
44
|
+
- Negative control: with the shipped lint restored, exactly 5 of 13 fail (dynamic import, `require`,
|
|
45
|
+
`import type`, vanished-curated-file, rootless-tree) and all 8 controls still pass — controls should pass both
|
|
46
|
+
ways, which is what makes them controls.
|
|
47
|
+
- Real-tree verdict: `node scripts/lint-journal-actuation-ban.js` → `clean (8 actuator modules, none load the
|
|
48
|
+
reader)`, exit 0. All eight curated actuators verified present, none referencing the reader.
|
|
49
|
+
- Full `npm run lint` chain green; `tests/unit/lint-chain-completeness.test.ts` 3/3.
|
|
50
|
+
- Side-effects review: `upgrades/side-effects/journal-actuation-ban-load-forms.md`.
|
|
51
|
+
- ELI16: `docs/specs/journal-actuation-ban-load-forms.eli16.md`.
|
|
52
|
+
|
|
53
|
+
**Raised separately, not actioned here:** `src/commands/server.ts:20853` wires `OwnershipApplier`, which
|
|
54
|
+
materializes durable topic ownership from the REPLICATED placement journal (it does validate `transferTo`
|
|
55
|
+
against the live known-machine set first, and is specified in
|
|
56
|
+
`docs/specs/ownership-applier-meshself-ordering-fix.md`). Whether §3.9 permits a validated read like that is a
|
|
57
|
+
spec question, not a lint decision; it is on the operator's attention queue. The shipped lint does not flag it.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
`scripts/lint-no-direct-llm-http.js` now folds adjacent constant string and no-expression template literals before checking for direct LLM provider hosts. A raw provider URL split as `'https://api.' + 'anthropic.com/v1/messages'` now fails the same build lint as the unsplit host.
|
|
9
|
+
|
|
10
|
+
The scope is deliberately narrow: literal-plus-literal URL construction only. Dynamic URL construction is not guessed at, and the existing file-level distinction for OAuth/profile/usage metadata endpoints remains unchanged.
|
|
11
|
+
|
|
12
|
+
## What to Tell Your User
|
|
13
|
+
|
|
14
|
+
None — internal change (no user-facing surface).
|
|
15
|
+
|
|
16
|
+
## Summary of New Capabilities
|
|
17
|
+
|
|
18
|
+
None — internal change (no user-facing surface).
|
|
19
|
+
|
|
20
|
+
## Evidence
|
|
21
|
+
|
|
22
|
+
Negative control against the shipped lint: the focused unit file failed 2 tests, both new split-host rejection cases. After the fix, `tests/unit/burn-detection-phase-1.test.ts` passes 20/20, and `node scripts/lint-no-direct-llm-http.js` exits clean against the real tree.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Side-Effects Review — journal-actuation-ban: every load form, and the staleness half of the population
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `journal-actuation-ban-load-forms`
|
|
4
|
+
**Date:** `2026-08-14`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required — Tier 1 (classifyTier: suggestedTier 1, riskFloor 1, no reasons). No spec change: COHERENCE-JOURNAL-SPEC §3.9 is unmodified; this makes the existing ban see load forms it already forbade.`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
`scripts/lint-journal-actuation-ban.js` enforced §3.9 with `/from\s+['"]…CoherenceJournalReader…['"]/`. That
|
|
11
|
+
requires the `from` keyword, which `await import(…)` and `require(…)` do not have, so both walked past the ban.
|
|
12
|
+
Reproduced against the shipped lint on a REAL listed actuator (`src/core/SessionManager.ts`): it reported
|
|
13
|
+
`clean`. Independently reproduced by instar-codey, who ranked this lint #1 of ~20 by consequence-of-defeat.
|
|
14
|
+
|
|
15
|
+
Now: all three load forms are matched; comments are stripped (quote-aware, length- and line-preserving) before
|
|
16
|
+
matching, so prose describing the ban is never a violation; a runtime-erased `import type` is correctly NOT a
|
|
17
|
+
violation; a curated actuator that has vanished from the tree is a violation instead of a skipped line; and a
|
|
18
|
+
root with no `src/` exits 2 rather than printing `clean`.
|
|
19
|
+
|
|
20
|
+
The declared-population gap ("grow this list when a new actuator class lands") is **left open deliberately** —
|
|
21
|
+
see §2.
|
|
22
|
+
|
|
23
|
+
## Decision-point inventory
|
|
24
|
+
|
|
25
|
+
- `LOADS_READER` — WIDEN — now matches `from` / `import(` / `require(`. CI-time only; never runtime.
|
|
26
|
+
- `TYPE_ONLY_IMPORT` — ADD — narrows: `import type` is exempt (erased at compile time).
|
|
27
|
+
- comment stripping — ADD — narrows: commented text cannot be a violation.
|
|
28
|
+
- missing curated file — CHANGE — was `continue` (silent), now a violation.
|
|
29
|
+
- root without `src/` — ADD — exit 2, no verdict.
|
|
30
|
+
- No runtime block/allow decisions added or modified. This script runs in `npm run lint` and CI only.
|
|
31
|
+
|
|
32
|
+
## 1. Over-block
|
|
33
|
+
|
|
34
|
+
The widened matcher can only fire on the eight curated actuator files, so the blast radius is those eight.
|
|
35
|
+
Verified against the real tree: none of the eight so much as mentions `CoherenceJournalReader`, and the lint
|
|
36
|
+
exits 0. Two narrowings actively REDUCE over-block versus the shipped version: `import type` (which the old
|
|
37
|
+
regex flagged — `src/core/WorkingSetPull.ts` is a live example of that shape) and comment stripping.
|
|
38
|
+
|
|
39
|
+
The one new way to fail a build that is not a §3.9 breach: renaming a curated actuator without updating
|
|
40
|
+
`ACTUATOR_FILES`. That is intended — a renamed actuator silently leaving the ban is the failure this closes —
|
|
41
|
+
and the message names the file and the fix.
|
|
42
|
+
|
|
43
|
+
## 2. Under-block
|
|
44
|
+
|
|
45
|
+
**Automatic discovery of actuators was built, measured against this tree, and REJECTED.** Inferring "is this an
|
|
46
|
+
actuator?" from declared names (functions, classes, filename) flagged nine sites across three files, and every
|
|
47
|
+
one was a part-of-speech or granularity error rather than a §3.9 breach:
|
|
48
|
+
|
|
49
|
+
- `src/server/routes.ts` — matched `readReaperPeerText`, `isReaperSnapshot`, `reaperPoolHealth`: "reaper" as a
|
|
50
|
+
NOUN in code that REPORTS ON the reaper. A reporting surface reading the journal is correct code.
|
|
51
|
+
- `src/commands/server.ts` — the 22k-line composition root. Every module's authority is wired through it, so any
|
|
52
|
+
file-level verdict on it is wrong in one direction or the other.
|
|
53
|
+
- `src/core/WorkingSetPull.ts` — a runtime-erased `import type`.
|
|
54
|
+
|
|
55
|
+
This lint blocks commits, so over-blocking correct code is the more expensive failure. The enumerated-list shape
|
|
56
|
+
is also the ORIGINAL converged design decision, stated in `upgrades/side-effects/coherence-journal-p1-2.md`:
|
|
57
|
+
"The enumerated-list shape is deliberate: growable, reviewable." Closing it needs an authoritative actuator
|
|
58
|
+
population, not a heuristic; `src/testing/selfActionRegistry.ts` (`modelsPath`, kept complete by
|
|
59
|
+
`lint-no-unregistered-self-action.js`) is the closest candidate but is a superset in KIND — spend-alert
|
|
60
|
+
emitters and sweeps are self-actions, not §3.9 session actuators. Left open, named in the header, and pinned by
|
|
61
|
+
a test that will fail if someone closes it.
|
|
62
|
+
|
|
63
|
+
Still evadable, unchanged from before: a hand-rolled JSONL read, or reaching the reader through a re-export.
|
|
64
|
+
The §3.9 duty remains the authority.
|
|
65
|
+
|
|
66
|
+
## 3. Level-of-abstraction fit
|
|
67
|
+
|
|
68
|
+
Line-level regex over comment-stripped source, on an enumerated file list. Same layer as the shipped check —
|
|
69
|
+
no AST, no type information, no new dependency. The comment stripper is the only added machinery, and it exists
|
|
70
|
+
so the matcher can widen without making §3.9 prose illegal.
|
|
71
|
+
|
|
72
|
+
## 4. Signal vs authority compliance
|
|
73
|
+
|
|
74
|
+
Unchanged and reinforced. The lint is a CI guard, not a runtime authority; it forbids actuators from HOLDING the
|
|
75
|
+
reader, which is what keeps replicated journal data signal rather than authority. Nothing here reads the journal
|
|
76
|
+
at runtime.
|
|
77
|
+
|
|
78
|
+
## 5. Interactions
|
|
79
|
+
|
|
80
|
+
- `npm run lint` chain (`package.json:31`) — position unchanged; `tests/unit/lint-chain-completeness.test.ts`
|
|
81
|
+
passes (3/3).
|
|
82
|
+
- Husky pre-commit / CI run the same chain. Exit 2 on a rootless tree is new; the repo root always has `src/`,
|
|
83
|
+
and the only caller passing `--root` is this test.
|
|
84
|
+
- No source module, route, config key, or state file is touched.
|
|
85
|
+
|
|
86
|
+
## 6. External surfaces
|
|
87
|
+
|
|
88
|
+
None. No HTTP route, no config key, no user-visible message, no CLAUDE.md template change (the lint is
|
|
89
|
+
developer-facing tooling, not an agent capability). Agent Awareness Standard does not apply.
|
|
90
|
+
|
|
91
|
+
## 7. Rollback cost
|
|
92
|
+
|
|
93
|
+
`git revert` of one script plus one test file. No migration, no state, no deployed artifact. The lint is
|
|
94
|
+
stateless and runs from source.
|
|
95
|
+
|
|
96
|
+
## Conclusion
|
|
97
|
+
|
|
98
|
+
Ship. One real evasion closed with a negative control proving each assertion fails without the fix; two
|
|
99
|
+
narrowings that reduce false positives below the shipped baseline; one silent-failure mode of my own making
|
|
100
|
+
(clean verdict over zero files) caught and closed before it shipped.
|
|
101
|
+
|
|
102
|
+
## Second-pass review (if required)
|
|
103
|
+
|
|
104
|
+
Not required at Tier 1. Independent corroboration of DEFECT 1 exists regardless: instar-codey reproduced the
|
|
105
|
+
dynamic-import evasion separately and ranked this lint first of ~20 by consequence-of-defeat, and recommended
|
|
106
|
+
exactly the scope taken here — "low FP risk if limited to actuator files and comment-stripped `import()`,
|
|
107
|
+
`require()`… Do not ban writer imports."
|
|
108
|
+
|
|
109
|
+
## Evidence pointers
|
|
110
|
+
|
|
111
|
+
- `tests/unit/journal-actuation-ban-lint.test.ts` — 13/13 green with the fix.
|
|
112
|
+
- Negative control: with the shipped lint restored, exactly 5 of the 13 fail (dynamic import, `require`,
|
|
113
|
+
`import type`, vanished-curated-file, rootless-tree) and all 8 controls still pass — controls should pass
|
|
114
|
+
both ways, which is what makes them controls.
|
|
115
|
+
- Real-tree verdict: `node scripts/lint-journal-actuation-ban.js` → `clean (8 actuator modules, none load the
|
|
116
|
+
reader)`, exit 0.
|
|
117
|
+
- `upgrades/side-effects/coherence-journal-p1-2.md` — the original converged decision to enumerate.
|
|
118
|
+
|
|
119
|
+
## Finding raised separately (NOT fixed here)
|
|
120
|
+
|
|
121
|
+
Discovery, before it was rejected, surfaced five `await import('../core/CoherenceJournalReader.js')` sites in
|
|
122
|
+
`src/commands/server.ts`. One (`:20853`) wires `OwnershipApplier`, which materializes durable topic ownership
|
|
123
|
+
FROM the replicated placement journal — replicated data feeding an ownership decision that placement and
|
|
124
|
+
session routing then act on. It is deliberate and specified (`docs/specs/ownership-applier-meshself-ordering-fix.md`)
|
|
125
|
+
and validates `transferTo` against the live known-machine set before materializing. Whether §3.9 permits it is a
|
|
126
|
+
spec question with real consequence, and it is not mine to settle inside a lint change. Raised to the operator;
|
|
127
|
+
deliberately NOT actioned here, and the lint does not flag it.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Side-Effects Review — LLM HTTP lint constant folding
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `llm-http-constant-folding`
|
|
4
|
+
**Date:** `2026-08-14`
|
|
5
|
+
**Author:** `Instar-codey`
|
|
6
|
+
**Tier:** 1 (one lint script plus focused unit coverage; no runtime code, route, config, migration, or persistence change)
|
|
7
|
+
**Second-pass reviewer:** `not required — CI/pre-commit lint hardening only; no runtime outbound message or session lifecycle decision`
|
|
8
|
+
|
|
9
|
+
## Summary of the change
|
|
10
|
+
|
|
11
|
+
`scripts/lint-no-direct-llm-http.js` now folds adjacent constant string and no-expression template literals joined by `+` before checking for known LLM provider hosts. This closes the reproduced split-host bypass (`'api.' + 'anthropic.com/v1/messages'`) while keeping scope at literal URL construction only. `tests/unit/burn-detection-phase-1.test.ts` adds red/green coverage for split Anthropic, OpenAI, and Google host literals plus a non-folded dynamic construction control.
|
|
12
|
+
|
|
13
|
+
Build location re-grounding: work was built in fresh worktree `/Users/justin_instar_1/.instar/agents/instar-codey/.worktrees/agent-llm-http-constant-folding` from current `JKHeadley/main` (`4731ec6a90356ed319454a996b4eb72edcf38ab2`), created through `npx -y instar@1.3.1144 worktree create ... --base origin/main` after the local wrapper lacked an installed package. Remote verified as `origin https://github.com/JKHeadley/instar.git`; package version verified as `1.3.1144`.
|
|
14
|
+
|
|
15
|
+
## Decision-point inventory
|
|
16
|
+
|
|
17
|
+
- `scripts/lint-no-direct-llm-http.js` — modify — build-time block/allow decision for direct provider HTTP references outside the provider chokepoint and named metadata exceptions.
|
|
18
|
+
|
|
19
|
+
## 1. Over-block
|
|
20
|
+
|
|
21
|
+
The new over-block risk is a benign constant string in production source that names a provider host across literal pieces without making a call. That is the same policy as the existing unsplit-host lint: production source outside the allowlist should not carry raw provider host literals because they become copyable direct-call paths. The real-tree scan after the fix was clean.
|
|
22
|
+
|
|
23
|
+
OAuth/profile/usage metadata readers keep their existing allowlist/grandfather treatment; this change did not add endpoint-level bans that would collapse metadata reads into inference calls.
|
|
24
|
+
|
|
25
|
+
## 2. Under-block
|
|
26
|
+
|
|
27
|
+
The lint still misses dynamic construction, such as `'https://api.' + providerHost + '/v1/messages'`, computed arrays joined into a host, decoded strings, or runtime config that points at a provider endpoint. That is intentional for this PR: automatic discovery/dataflow was the high-false-positive direction. The closure here is constant URL literals, not semantic HTTP-call proof.
|
|
28
|
+
|
|
29
|
+
## 3. Level-of-abstraction fit
|
|
30
|
+
|
|
31
|
+
Correct layer: this is a deterministic CI lint in the existing `lint-no-*` family. The protected invariant is "new production raw LLM provider host literals outside the chokepoint require review." The lower-level primitive is a string-literal scanner, not an authority over runtime user intent.
|
|
32
|
+
|
|
33
|
+
## 4. Signal vs authority compliance
|
|
34
|
+
|
|
35
|
+
Reference: [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md).
|
|
36
|
+
|
|
37
|
+
This change holds blocking authority with brittle logic, but it is a build-time hard invariant/safety guard, not a conversational judgment point. The lint blocks source code containing direct provider endpoint literals outside reviewed locations, analogous to a safety guard on data egress and spend attribution. It does not decide whether a user message is appropriate or whether an agent action should proceed at runtime.
|
|
38
|
+
|
|
39
|
+
## 4b. Judgment-point check
|
|
40
|
+
|
|
41
|
+
No new static heuristic at a competing-signals decision point. The input is source text and an enumerated provider-host list; there are no live competing signals such as urgency, recency, ownership, or liveness.
|
|
42
|
+
|
|
43
|
+
## 5. Interactions
|
|
44
|
+
|
|
45
|
+
- **Shadowing:** It runs inside the existing `npm run lint` chain. It may fail before later lints, as before; no downstream lint depends on side effects from this script.
|
|
46
|
+
- **Double-fire:** `lint-llm-attribution.js` and other provider-path checks may catch related defects, but this one reports direct HTTP host literals specifically. Duplicate CI failures are acceptable because they point at the same source change.
|
|
47
|
+
- **Races:** No shared state. It reads files and exits.
|
|
48
|
+
- **Feedback loops:** None.
|
|
49
|
+
|
|
50
|
+
## 6. External surfaces
|
|
51
|
+
|
|
52
|
+
No runtime external surface changes. Other agents and users only see this as a stricter build/pre-commit/CI failure when source contains a newly-recognized split provider URL. No Telegram, Slack, GitHub API, dashboard, database, ledger, or generated URL behavior changes. No operator-facing action is added.
|
|
53
|
+
|
|
54
|
+
## 6b. Operator-surface quality
|
|
55
|
+
|
|
56
|
+
No operator surface — not applicable.
|
|
57
|
+
|
|
58
|
+
## 7. Multi-machine posture
|
|
59
|
+
|
|
60
|
+
Machine-local by design: this is a repository lint run independently in each checkout/CI runner. It emits no user-facing notices, holds no durable state, and generates no URLs. Multi-machine consistency comes from the shared git commit containing the lint and tests.
|
|
61
|
+
|
|
62
|
+
## 8. Rollback cost
|
|
63
|
+
|
|
64
|
+
Pure code/test/docs change. Rollback is a hot-fix revert of the lint folding helper and tests. No data migration, no agent state repair, and no user-visible runtime regression while rollback propagates.
|
|
65
|
+
|
|
66
|
+
## Conclusion
|
|
67
|
+
|
|
68
|
+
The real-tree scan stayed clean and the negative control failed for the intended old-lint cases, so the scoped constant-folding fix is clear to ship. No broader automatic actuator discovery or dynamic URL dataflow was added.
|
|
69
|
+
|
|
70
|
+
## Second-pass review
|
|
71
|
+
|
|
72
|
+
Not required.
|
|
73
|
+
|
|
74
|
+
## Evidence pointers
|
|
75
|
+
|
|
76
|
+
- Old-lint focused run: `npx vitest run tests/unit/burn-detection-phase-1.test.ts` failed 2 of 20 tests, specifically the two new split-host rejection tests.
|
|
77
|
+
- Fixed targeted run: `npx vitest run tests/unit/burn-detection-phase-1.test.ts` passed 20/20.
|
|
78
|
+
- Fixed real-tree scan: `node scripts/lint-no-direct-llm-http.js` exited 0.
|
|
79
|
+
|
|
80
|
+
## Class-Closure Declaration (display-only mirror)
|
|
81
|
+
|
|
82
|
+
No agent-authored-artifact defect — not applicable.
|