instar 1.3.967 → 1.3.969
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/package.json +1 -1
- package/scripts/generate-spec-contract.mjs +249 -12
- package/src/data/builtin-manifest.json +2 -2
- package/src/scaffold/templates/jobs/instar/evolution-overdue-check.md +7 -3
- package/src/scaffold/templates/jobs/instar/evolution-proposal-evaluate.md +7 -3
- package/src/scaffold/templates/jobs/instar/evolution-proposal-implement.md +6 -2
- package/src/scaffold/templates/jobs/instar/identity-review.md +1 -1
- package/src/scaffold/templates/jobs/instar/insight-harvest.md +7 -3
- package/src/scaffold/templates/jobs/instar/reflection-trigger.md +1 -1
- package/upgrades/1.3.968.md +121 -0
- package/upgrades/1.3.969.md +35 -0
- package/upgrades/side-effects/evo007-job-template-auth.eli16.md +51 -0
- package/upgrades/side-effects/evo007-job-template-auth.md +160 -0
- package/upgrades/side-effects/spec-contract-generator.md +187 -0
package/package.json
CHANGED
|
@@ -22,7 +22,8 @@ import fs from 'node:fs';
|
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
|
|
24
24
|
/** A heading that begins the review-history half. Matched case-insensitively. */
|
|
25
|
-
const HISTORY_HEADING_RE =
|
|
25
|
+
const HISTORY_HEADING_RE =
|
|
26
|
+
/^##\s+(?:\d+\.\s+)?(?:Round-\d+\b[^\n]*?(?:change log|hand-check|consistency sweep)|Review record\b|Appendix\b)/i;
|
|
26
27
|
|
|
27
28
|
/** Headings that are contract even though they sort after a history heading. */
|
|
28
29
|
const ALWAYS_CONTRACT_RE = /^##\s+(?:\d+\.\s+)?(?:Decision points touched|Open questions|Frontloaded Decisions|Dependencies|Honest limits|Multi-machine posture|What this does not do)/i;
|
|
@@ -53,11 +54,176 @@ export function stripInlineAnnotations(text) {
|
|
|
53
54
|
return out.replace(/[ \t]{2,}/g, ' ').replace(/ +([.,;:])/g, '$1');
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
/**
|
|
58
|
+
* A blockquote block that exists to talk ABOUT the document — normative-boundary
|
|
59
|
+
* markers, "this file is rationale", scope-changed notices. They are meta, not
|
|
60
|
+
* contract, and they read as especially authoritative because of the `>` and the
|
|
61
|
+
* bold. Round-37 (codex) found several of them surviving into the generated
|
|
62
|
+
* contract, where a "NON-NORMATIVE FROM HERE" marker inside a file that claims to
|
|
63
|
+
* be normative-only is worse than no marker at all.
|
|
64
|
+
*/
|
|
65
|
+
const META_BLOCKQUOTE_RE =
|
|
66
|
+
/^>\s*(?:#{1,4}\s*)?\**\s*(?:NON-)?NORMATIVE\b|^>\s*\**\s*(?:It (?:is|began)|This file|Build from|The normative artifact|And the review)/i;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A residual the generator CANNOT remove: narrative prose that states a rule and
|
|
70
|
+
* narrates its history in the same sentence ("round-36 found this paragraph
|
|
71
|
+
* stating only the p99"). Counting them is the honest alternative to pretending
|
|
72
|
+
* they are gone — the run reports the number so a reader knows what they are
|
|
73
|
+
* getting.
|
|
74
|
+
*/
|
|
75
|
+
const NARRATIVE_HISTORY_RE = /\b(?:round|rounds)[- ]\d+\b/gi;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* STRICT mode (`--strict`): an ALLOWLIST of contract-bearing headings, instead of
|
|
79
|
+
* the default denylist of history headings.
|
|
80
|
+
*
|
|
81
|
+
* The denylist answers "what is definitely history?" and keeps everything else.
|
|
82
|
+
* That is the wrong default for an implementation artifact: rationale, accepted
|
|
83
|
+
* residuals and self-correcting narrative are all "not definitely history", so
|
|
84
|
+
* they survive — and SEVEN consecutive review rounds (33-39) said the resulting
|
|
85
|
+
* contract still read as archaeology. An allowlist answers the question that
|
|
86
|
+
* actually matters, "what must be built?", and everything else is absent by
|
|
87
|
+
* default rather than by pattern-match.
|
|
88
|
+
*
|
|
89
|
+
* Kept deliberately narrow: the contract table, rollout/acceptance, honest
|
|
90
|
+
* limits, decision points, and the test plan. Rationale lives in the source spec,
|
|
91
|
+
* which is where a reader goes for judgment.
|
|
92
|
+
*/
|
|
93
|
+
const STRICT_CONTRACT_HEADING_RE =
|
|
94
|
+
/^#{2,3}\s+(?:\d+(?:\.\d+)*\.?\s+)?(?:Final contract|What an implementer builds|Current design overview|Design\b|Glossary|Normative outcome table|Rollout|Honest limits|Privacy posture|Decision points touched|Open questions|Frontloaded Decisions|Dependencies|Multi-machine posture|Test plan|Fail directions|What this does not do)/i;
|
|
95
|
+
|
|
96
|
+
export function splitStrictContract(markdown) {
|
|
97
|
+
const lines = markdown.split('\n');
|
|
98
|
+
// Pre-pass: how big is each allowlisted section in the SOURCE (to the next
|
|
99
|
+
// heading of the same-or-higher level)?
|
|
100
|
+
const sourceSizes = new Map();
|
|
101
|
+
for (let i = 0; i < lines.length; i++) {
|
|
102
|
+
if (!STRICT_CONTRACT_HEADING_RE.test(lines[i])) continue;
|
|
103
|
+
const level = (lines[i].match(/^#+/) || ['##'])[0].length;
|
|
104
|
+
let bytes = 0;
|
|
105
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
106
|
+
const m = lines[j].match(/^(#+)\s/);
|
|
107
|
+
// The source measure must NOT use the capture's stopping rule, or it
|
|
108
|
+
// shrinks in lockstep with the truncation it is meant to detect (an
|
|
109
|
+
// interior non-allowlisted H3 would end both, and the guard sees nothing).
|
|
110
|
+
// Stop at the next ALLOWLISTED heading, the next H2, or a NUMBERED SIBLING
|
|
111
|
+
// at the same level.
|
|
112
|
+
//
|
|
113
|
+
// The sibling test is what separates the two shapes this guard cannot
|
|
114
|
+
// otherwise tell apart (both are "a same-level heading follows"):
|
|
115
|
+
// "### 3.0 Final contract" → "### Normative checklist" = meant as a CHILD;
|
|
116
|
+
// its content belongs to 3.0 and capture losing it is a real defect.
|
|
117
|
+
// "### 3.10 Test plan" → "### 3.11 Judgeable-record" = a genuine SIBLING;
|
|
118
|
+
// its content was never 3.10's and excluding it is correct.
|
|
119
|
+
// Without this, the guard fired permanently on every numbered spec — and a
|
|
120
|
+
// warning that always fires is one nobody reads (the lesson from its own
|
|
121
|
+
// second implementation).
|
|
122
|
+
const isNumberedSibling = m && m[1].length === level && /^#+\s+\d+(\.\d+)*[.\s]/.test(lines[j]);
|
|
123
|
+
if (m && (STRICT_CONTRACT_HEADING_RE.test(lines[j]) || m[1].length <= 2 || isNumberedSibling)) break;
|
|
124
|
+
// Content that belongs to a NESTED allowlisted subsection is captured
|
|
125
|
+
// under that subsection's own entry — it is not lost, so it must not
|
|
126
|
+
// count against this section (otherwise a container heading like
|
|
127
|
+
// "## 4. Honest limits" followed by an allowlisted "### 4.0 …" warns
|
|
128
|
+
// forever, and a guard that always warns is a guard nobody reads).
|
|
129
|
+
bytes += lines[j].length + 1;
|
|
130
|
+
}
|
|
131
|
+
sourceSizes.set(lines[i].trim(), bytes);
|
|
132
|
+
}
|
|
133
|
+
const kept = [];
|
|
134
|
+
let keeping = false;
|
|
135
|
+
let openLevel = 0;
|
|
136
|
+
let keptSections = 0;
|
|
137
|
+
const sectionSizes = [];
|
|
138
|
+
// Front matter always rides along — it carries the approval + convergence tags.
|
|
139
|
+
let inFrontMatter = lines[0] === '---';
|
|
140
|
+
for (let i = 0; i < lines.length; i++) {
|
|
141
|
+
const line = lines[i];
|
|
142
|
+
if (inFrontMatter) {
|
|
143
|
+
kept.push(line);
|
|
144
|
+
if (i > 0 && line === '---') inFrontMatter = false;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (/^#{1,3}\s/.test(line)) {
|
|
148
|
+
const level = (line.match(/^#+/) || ['##'])[0].length;
|
|
149
|
+
if (STRICT_CONTRACT_HEADING_RE.test(line)) {
|
|
150
|
+
keeping = true;
|
|
151
|
+
openLevel = level;
|
|
152
|
+
keptSections++;
|
|
153
|
+
sectionSizes.push({ heading: line.trim(), bytes: 0, sourceBytes: sourceSizes.get(line.trim()) || 0 });
|
|
154
|
+
} else if (keeping && level > openLevel) {
|
|
155
|
+
// A NON-allowlisted sub-heading DEEPER than the open section is that
|
|
156
|
+
// section's own child — "### 3.2 The credential arm" under "## 3.
|
|
157
|
+
// Design". Closing on it silently emptied the section while the section
|
|
158
|
+
// COUNT stayed the same, so the contract looked complete and was not:
|
|
159
|
+
// the outbound-gate design (86 KB) shipped as 14 bytes, and the reviews
|
|
160
|
+
// run against it produced four objections the missing text already
|
|
161
|
+
// answered. Children ride along; a sibling or an allowlisted heading
|
|
162
|
+
// still closes it.
|
|
163
|
+
} else {
|
|
164
|
+
keeping = false;
|
|
165
|
+
}
|
|
166
|
+
// An H1 (the title) is kept as an anchor but does not open a section.
|
|
167
|
+
if (/^#\s/.test(line)) {
|
|
168
|
+
kept.push(line);
|
|
169
|
+
keeping = false;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (keeping) {
|
|
174
|
+
kept.push(line);
|
|
175
|
+
if (sectionSizes.length) sectionSizes[sectionSizes.length - 1].bytes += line.length + 1;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const body = stripInlineAnnotations(kept.join('\n')).replace(/\n{4,}/g, '\n\n\n');
|
|
179
|
+
const narrativeResidual = (body.match(NARRATIVE_HISTORY_RE) || []).length;
|
|
180
|
+
// Guard against the allowlist's own failure mode: SILENT OMISSION. If strict
|
|
181
|
+
// mode keeps only a small slice of a large spec, the likely cause is that the
|
|
182
|
+
// spec's headings do not match the allowlist — not that the spec is mostly
|
|
183
|
+
// rationale. Confirmed in practice on outbound-gate-advisory-override, where
|
|
184
|
+
// the normative outcome table lived under headings the list did not name, and
|
|
185
|
+
// the reviewer's first finding was "normative behavior is missing".
|
|
186
|
+
const sourceHeadings = (markdown.match(/^#{2,3}\s/gm) || []).length;
|
|
187
|
+
const keptRatio = sourceHeadings ? keptSections / sourceHeadings : 1;
|
|
188
|
+
// Section COUNT is not content coverage: an H3 inside an allowlisted section
|
|
189
|
+
// ends its capture, silently emptying it while the count stays identical.
|
|
190
|
+
// Observed live (2026-07-25) — a new "### Normative checklist" heading inside
|
|
191
|
+
// §3.0 dropped the entire contract table and the checklist itself from the
|
|
192
|
+
// output, and the capture-ratio guard below did not fire because 10 sections
|
|
193
|
+
// still "matched". So also guard on captured BYTES per kept section.
|
|
194
|
+
// A short section is only suspicious when the SOURCE section is long — a
|
|
195
|
+
// container heading (e.g. "## 4. Honest limits" immediately followed by an
|
|
196
|
+
// allowlisted "### 4.0 …") is legitimately near-empty and must not warn.
|
|
197
|
+
const truncated = sectionSizes.filter(
|
|
198
|
+
(x) => x.sourceBytes > 1000 && x.bytes < x.sourceBytes * 0.25,
|
|
199
|
+
);
|
|
200
|
+
const thinSectionWarning = truncated.length
|
|
201
|
+
? `${truncated.length} allowlisted section(s) captured far less than the ` +
|
|
202
|
+
`source contains: ` +
|
|
203
|
+
truncated
|
|
204
|
+
.map((x) => `${x.heading} (${x.bytes}/${x.sourceBytes} bytes)`)
|
|
205
|
+
.join(' | ') +
|
|
206
|
+
` — an H3 inside an allowlisted section ENDS its capture, emptying it while ` +
|
|
207
|
+
`the section count stays the same. Sub-headings inside a contract section ` +
|
|
208
|
+
`must be H4.`
|
|
209
|
+
: null;
|
|
210
|
+
const underCaptureWarning =
|
|
211
|
+
sourceHeadings >= 8 && keptRatio < 0.25
|
|
212
|
+
? `only ${keptSections}/${sourceHeadings} sections matched the allowlist ` +
|
|
213
|
+
`(${Math.round(keptRatio * 100)}%) — the strict contract may be MISSING ` +
|
|
214
|
+
`normative sections whose headings are not on the list. Verify before ` +
|
|
215
|
+
`building from it.`
|
|
216
|
+
: null;
|
|
217
|
+
return { contract: body, keptSections, narrativeResidual, sourceHeadings, underCaptureWarning, thinSectionWarning };
|
|
218
|
+
}
|
|
219
|
+
|
|
56
220
|
export function splitContract(markdown) {
|
|
57
221
|
const lines = markdown.split('\n');
|
|
58
222
|
const kept = [];
|
|
59
223
|
let inHistory = false;
|
|
224
|
+
let inMetaQuote = false;
|
|
60
225
|
let droppedSections = 0;
|
|
226
|
+
let droppedMetaBlocks = 0;
|
|
61
227
|
for (const line of lines) {
|
|
62
228
|
if (/^##\s/.test(line)) {
|
|
63
229
|
if (HISTORY_HEADING_RE.test(line) && !ALWAYS_CONTRACT_RE.test(line)) {
|
|
@@ -68,21 +234,56 @@ export function splitContract(markdown) {
|
|
|
68
234
|
// A non-history H2 ends a history run (sections are not strictly ordered).
|
|
69
235
|
inHistory = false;
|
|
70
236
|
}
|
|
237
|
+
// Meta blockquotes run until the first non-blockquote, non-blank line.
|
|
238
|
+
if (!inHistory) {
|
|
239
|
+
if (!inMetaQuote && META_BLOCKQUOTE_RE.test(line)) {
|
|
240
|
+
inMetaQuote = true;
|
|
241
|
+
droppedMetaBlocks++;
|
|
242
|
+
} else if (inMetaQuote && !/^>/.test(line) && line.trim() !== '') {
|
|
243
|
+
inMetaQuote = false;
|
|
244
|
+
}
|
|
245
|
+
if (inMetaQuote) continue;
|
|
246
|
+
}
|
|
71
247
|
if (!inHistory) kept.push(line);
|
|
72
248
|
}
|
|
73
249
|
const body = stripInlineAnnotations(kept.join('\n')).replace(/\n{4,}/g, '\n\n\n');
|
|
74
|
-
|
|
250
|
+
const narrativeResidual = (body.match(NARRATIVE_HISTORY_RE) || []).length;
|
|
251
|
+
return { contract: body, droppedSections, droppedMetaBlocks, narrativeResidual };
|
|
75
252
|
}
|
|
76
253
|
|
|
77
254
|
/** The banner that makes the generated file unmistakable and un-editable-by-hand. */
|
|
78
|
-
function banner(specRel) {
|
|
255
|
+
function banner(specRel, narrativeResidual, strict) {
|
|
256
|
+
if (strict) {
|
|
257
|
+
return [
|
|
258
|
+
'<!-- GENERATED FILE — DO NOT EDIT.',
|
|
259
|
+
` Source: ${specRel}`,
|
|
260
|
+
' Regenerate: node scripts/generate-spec-contract.mjs --spec ' + specRel + ' --strict',
|
|
261
|
+
' STRICT IMPLEMENTATION CONTRACT: allowlisted contract sections only.',
|
|
262
|
+
'',
|
|
263
|
+
' Everything not on the allowlist is ABSENT BY DEFAULT — including all',
|
|
264
|
+
' rationale. This file says WHAT to build, never why. Read the source',
|
|
265
|
+
' spec for the reasoning, the alternatives, and the accepted residuals',
|
|
266
|
+
' in their full form.',
|
|
267
|
+
` (${narrativeResidual} residual "round-N" reference(s) remain inline.)`,
|
|
268
|
+
'-->',
|
|
269
|
+
'',
|
|
270
|
+
].join('\n');
|
|
271
|
+
}
|
|
79
272
|
return [
|
|
80
273
|
'<!-- GENERATED FILE — DO NOT EDIT.',
|
|
81
274
|
` Source: ${specRel}`,
|
|
82
275
|
' Regenerate: node scripts/generate-spec-contract.mjs --spec ' + specRel,
|
|
83
|
-
' This is the IMPLEMENTATION CONTRACT
|
|
84
|
-
'
|
|
85
|
-
'
|
|
276
|
+
' This is the IMPLEMENTATION CONTRACT.',
|
|
277
|
+
'',
|
|
278
|
+
' REMOVED: history sections, delimited round-annotations, and blockquote',
|
|
279
|
+
' meta-blocks that talk about the document rather than the design.',
|
|
280
|
+
'',
|
|
281
|
+
' NOT REMOVED: narrative prose that states a rule and narrates its own',
|
|
282
|
+
' history in the same sentence. A transform cannot separate those without',
|
|
283
|
+
' judgment it deliberately does not have, so some review references remain',
|
|
284
|
+
` below (${narrativeResidual} occurrence(s) of "round-N" in this file).`,
|
|
285
|
+
' Where such a sentence describes what a design USED to be, the surrounding',
|
|
286
|
+
' normative statement governs. Read the source spec for full context.',
|
|
86
287
|
'-->',
|
|
87
288
|
'',
|
|
88
289
|
].join('\n');
|
|
@@ -99,12 +300,34 @@ function main() {
|
|
|
99
300
|
const check = args.includes('--check');
|
|
100
301
|
const specRel = path.relative(process.cwd(), specPath);
|
|
101
302
|
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
303
|
+
const strict = args.includes('--strict');
|
|
304
|
+
if (!fs.existsSync(specPath)) {
|
|
305
|
+
console.error(`ERROR: spec not found: ${specRel}`);
|
|
306
|
+
process.exit(2);
|
|
307
|
+
}
|
|
308
|
+
let markdown;
|
|
309
|
+
try {
|
|
310
|
+
markdown = fs.readFileSync(specPath, 'utf8');
|
|
311
|
+
} catch (err) {
|
|
312
|
+
console.error(`ERROR: cannot read ${specRel}: ${err instanceof Error ? err.message : err}`);
|
|
313
|
+
process.exit(2);
|
|
314
|
+
}
|
|
315
|
+
const res = strict ? splitStrictContract(markdown) : splitContract(markdown);
|
|
316
|
+
const { contract, narrativeResidual } = res;
|
|
317
|
+
const droppedSections = res.droppedSections ?? 0;
|
|
318
|
+
const droppedMetaBlocks = res.droppedMetaBlocks ?? 0;
|
|
319
|
+
const keptSections = res.keptSections ?? 0;
|
|
320
|
+
const output = banner(specRel, narrativeResidual, strict) + contract;
|
|
321
|
+
if (res.underCaptureWarning) {
|
|
322
|
+
console.error(`WARNING (strict): ${res.underCaptureWarning}`);
|
|
323
|
+
}
|
|
324
|
+
if (res.thinSectionWarning) {
|
|
325
|
+
console.error(`WARNING (strict): ${res.thinSectionWarning}`);
|
|
326
|
+
}
|
|
105
327
|
|
|
106
328
|
const slug = path.basename(specPath, '.md');
|
|
107
|
-
const
|
|
329
|
+
const suffix = strict ? '.contract.strict.md' : '.contract.md';
|
|
330
|
+
const outPath = path.join(path.dirname(specPath), 'generated', `${slug}${suffix}`);
|
|
108
331
|
|
|
109
332
|
if (check) {
|
|
110
333
|
if (!fs.existsSync(outPath)) {
|
|
@@ -119,7 +342,14 @@ function main() {
|
|
|
119
342
|
);
|
|
120
343
|
process.exit(1);
|
|
121
344
|
}
|
|
122
|
-
console.log(
|
|
345
|
+
console.log(
|
|
346
|
+
strict
|
|
347
|
+
? `OK: strict contract is current (${keptSections} allowlisted sections; ` +
|
|
348
|
+
`${narrativeResidual} narrative round-references remain).`
|
|
349
|
+
: `OK: contract is current (${droppedSections} history sections, ` +
|
|
350
|
+
`${droppedMetaBlocks} meta-blocks excluded; ${narrativeResidual} narrative ` +
|
|
351
|
+
`round-references remain).`,
|
|
352
|
+
);
|
|
123
353
|
return;
|
|
124
354
|
}
|
|
125
355
|
|
|
@@ -127,7 +357,14 @@ function main() {
|
|
|
127
357
|
fs.writeFileSync(outPath, output, 'utf8');
|
|
128
358
|
const pct = Math.round((1 - output.length / markdown.length) * 100);
|
|
129
359
|
console.log(
|
|
130
|
-
|
|
360
|
+
strict
|
|
361
|
+
? `wrote ${path.relative(process.cwd(), outPath)} — STRICT: ${keptSections} ` +
|
|
362
|
+
`allowlisted sections kept, ${pct}% smaller. RESIDUAL: ${narrativeResidual} ` +
|
|
363
|
+
`narrative round-reference(s).`
|
|
364
|
+
: `wrote ${path.relative(process.cwd(), outPath)} — ${droppedSections} history ` +
|
|
365
|
+
`sections + ${droppedMetaBlocks} meta-blocks excluded, ${pct}% smaller. ` +
|
|
366
|
+
`RESIDUAL: ${narrativeResidual} narrative round-reference(s) the transform ` +
|
|
367
|
+
`cannot remove.`,
|
|
131
368
|
);
|
|
132
369
|
}
|
|
133
370
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-07-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-07-25T21:09:48.944Z",
|
|
5
|
+
"instarVersion": "1.3.969",
|
|
6
6
|
"entryCount": 202,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -16,14 +16,18 @@ toolAllowlist: "*"
|
|
|
16
16
|
unrestrictedTools: true
|
|
17
17
|
mcpAccess: none
|
|
18
18
|
---
|
|
19
|
-
|
|
19
|
+
AUTH="${INSTAR_AUTH_TOKEN:-$(python3 -c "import json; v=json.load(open('.instar/config.json')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)}"
|
|
20
|
+
AGENT_ID="${INSTAR_AGENT_ID:-$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('projectName',''))" 2>/dev/null)}"
|
|
21
|
+
PORT="${INSTAR_PORT:-4042}"
|
|
22
|
+
|
|
23
|
+
Check for overdue commitments: curl -s -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID" http://localhost:$PORT/evolution/actions/overdue
|
|
20
24
|
|
|
21
25
|
For each overdue action:
|
|
22
26
|
1. Assess: Can this be completed now? Is it still relevant?
|
|
23
27
|
2. If actionable, attempt to complete it or advance it
|
|
24
|
-
3. If no longer relevant, cancel it: curl -s -X PATCH http://localhost:$
|
|
28
|
+
3. If no longer relevant, cancel it: curl -s -X PATCH -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID" http://localhost:$PORT/evolution/actions/ACT-XXX -H 'Content-Type: application/json' -d '{"status":"cancelled","resolution":"No longer relevant because..."}'
|
|
25
29
|
4. If blocked, escalate to the user via Telegram (if configured)
|
|
26
30
|
|
|
27
|
-
Also check pending actions (curl -s http://localhost:$
|
|
31
|
+
Also check pending actions (curl -s -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID" "http://localhost:$PORT/evolution/actions?status=pending") for items that have been pending more than 48 hours without a due date — these are forgotten commitments.
|
|
28
32
|
|
|
29
33
|
If no overdue or stale items, exit silently.
|
|
@@ -16,16 +16,20 @@ toolAllowlist: "*"
|
|
|
16
16
|
unrestrictedTools: true
|
|
17
17
|
mcpAccess: none
|
|
18
18
|
---
|
|
19
|
-
|
|
19
|
+
AUTH="${INSTAR_AUTH_TOKEN:-$(python3 -c "import json; v=json.load(open('.instar/config.json')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)}"
|
|
20
|
+
AGENT_ID="${INSTAR_AGENT_ID:-$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('projectName',''))" 2>/dev/null)}"
|
|
21
|
+
PORT="${INSTAR_PORT:-4042}"
|
|
22
|
+
|
|
23
|
+
Review pending evolution proposals: curl -s -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID" "http://localhost:$PORT/evolution/proposals?status=proposed"
|
|
20
24
|
|
|
21
25
|
For each proposal:
|
|
22
26
|
1. Read the title, description, type, and source
|
|
23
27
|
2. Evaluate: Is this a genuine improvement? Is the effort worth the impact? Does it align with our goals?
|
|
24
|
-
3. If approved, update status: curl -s -X PATCH http://localhost:$
|
|
28
|
+
3. If approved, update status: curl -s -X PATCH -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID" http://localhost:$PORT/evolution/proposals/EVO-XXX -H 'Content-Type: application/json' -d '{"status":"approved"}'
|
|
25
29
|
4. If rejected or deferred, update with reason.
|
|
26
30
|
|
|
27
31
|
Do NOT implement approved proposals — that's handled by the paired evolution-proposal-implement job.
|
|
28
32
|
|
|
29
|
-
Also check the dashboard: curl -s http://localhost:$
|
|
33
|
+
Also check the dashboard: curl -s -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID" http://localhost:$PORT/evolution — report any highlights to the user if they seem important.
|
|
30
34
|
|
|
31
35
|
If no proposals need attention, exit silently.
|
|
@@ -15,11 +15,15 @@ gate: "curl -sf -H \"Authorization: Bearer $INSTAR_AUTH_TOKEN\" -H \"X-Instar-Ag
|
|
|
15
15
|
toolAllowlist: "*"
|
|
16
16
|
unrestrictedTools: true
|
|
17
17
|
---
|
|
18
|
-
|
|
18
|
+
AUTH="${INSTAR_AUTH_TOKEN:-$(python3 -c "import json; v=json.load(open('.instar/config.json')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)}"
|
|
19
|
+
AGENT_ID="${INSTAR_AGENT_ID:-$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('projectName',''))" 2>/dev/null)}"
|
|
20
|
+
PORT="${INSTAR_PORT:-4042}"
|
|
21
|
+
|
|
22
|
+
Implement approved evolution proposals: curl -s -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID" "http://localhost:$PORT/evolution/proposals?status=approved"
|
|
19
23
|
|
|
20
24
|
For each approved proposal:
|
|
21
25
|
1. Read the full description and understand what needs to be built
|
|
22
26
|
2. Implement it: create the skill/hook/job/config change described
|
|
23
|
-
3. After implementation, mark complete: curl -s -X PATCH http://localhost:$
|
|
27
|
+
3. After implementation, mark complete: curl -s -X PATCH -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID" http://localhost:$PORT/evolution/proposals/EVO-XXX -H 'Content-Type: application/json' -d '{"status":"implemented","resolution":"What was done"}'
|
|
24
28
|
|
|
25
29
|
If no approved proposals exist, exit silently.
|
|
@@ -17,7 +17,7 @@ mcpAccess: none
|
|
|
17
17
|
---
|
|
18
18
|
Identity review — check your identity coherence and growth.
|
|
19
19
|
|
|
20
|
-
AUTH
|
|
20
|
+
AUTH="${INSTAR_AUTH_TOKEN:-$(python3 -c "import json; v=json.load(open('.instar/config.json')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)}"
|
|
21
21
|
AGENT_ID="${INSTAR_AGENT_ID:-$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('projectName',''))" 2>/dev/null)}"
|
|
22
22
|
|
|
23
23
|
1. **Check soul.md drift**: curl -s -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID" http://localhost:${INSTAR_PORT:-4042}/identity/soul/drift
|
|
@@ -14,7 +14,11 @@ toolAllowlist: "*"
|
|
|
14
14
|
unrestrictedTools: true
|
|
15
15
|
mcpAccess: none
|
|
16
16
|
---
|
|
17
|
-
|
|
17
|
+
AUTH="${INSTAR_AUTH_TOKEN:-$(python3 -c "import json; v=json.load(open('.instar/config.json')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)}"
|
|
18
|
+
AGENT_ID="${INSTAR_AGENT_ID:-$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('projectName',''))" 2>/dev/null)}"
|
|
19
|
+
PORT="${INSTAR_PORT:-4042}"
|
|
20
|
+
|
|
21
|
+
Harvest and synthesize learnings: curl -s -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID" "http://localhost:$PORT/evolution/learnings?applied=false"
|
|
18
22
|
|
|
19
23
|
Review unapplied learnings and look for:
|
|
20
24
|
1. **Patterns**: Multiple learnings pointing to the same conclusion
|
|
@@ -22,10 +26,10 @@ Review unapplied learnings and look for:
|
|
|
22
26
|
3. **Cross-domain connections**: Insights from one area that apply to another
|
|
23
27
|
|
|
24
28
|
For each actionable pattern found, create an evolution proposal:
|
|
25
|
-
curl -s -X POST http://localhost:$
|
|
29
|
+
curl -s -X POST -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID" http://localhost:$PORT/evolution/proposals -H 'Content-Type: application/json' -d '{"title":"...","source":"insight-harvest from LRN-XXX","description":"...","type":"...","impact":"...","effort":"..."}'
|
|
26
30
|
|
|
27
31
|
Then mark the relevant learnings as applied:
|
|
28
|
-
curl -s -X PATCH http://localhost:$
|
|
32
|
+
curl -s -X PATCH -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID" http://localhost:$PORT/evolution/learnings/LRN-XXX/apply -H 'Content-Type: application/json' -d '{"appliedTo":"EVO-XXX"}'
|
|
29
33
|
|
|
30
34
|
Also update MEMORY.md with any patterns worth preserving long-term.
|
|
31
35
|
|
|
@@ -36,6 +36,6 @@ echo ""
|
|
|
36
36
|
echo "If you find genuine learnings:"
|
|
37
37
|
echo "1. Update .instar/MEMORY.md with the insight (append to the file)"
|
|
38
38
|
echo "2. Be specific: include what was learned, why it matters, and how it should guide future work"
|
|
39
|
-
echo "3. Signal completion: curl -s -X POST http://localhost:${INSTAR_PORT:-4042}/reflection/record -H 'Content-Type: application/json' -d '{"type"
|
|
39
|
+
echo "3. Signal completion: curl -s -X POST -H \"Authorization: Bearer \$INSTAR_AUTH_TOKEN\" -H \"X-Instar-AgentId: \$INSTAR_AGENT_ID\" http://localhost:${INSTAR_PORT:-4042}/reflection/record -H 'Content-Type: application/json' -d '{\"type\":\"quick\"}'"
|
|
40
40
|
echo ""
|
|
41
41
|
echo "If nothing significant, do nothing. Silence means continuity is working as expected."
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Widened the history-heading pattern in `scripts/generate-spec-contract.mjs` by
|
|
9
|
+
one alternative (`Review record`), so a spec that consolidates its review history
|
|
10
|
+
into a single section is stripped like one that uses per-round `change log`
|
|
11
|
+
sections.
|
|
12
|
+
|
|
13
|
+
**Why it needed changing.** Exercising the generator on a second spec surfaced
|
|
14
|
+
the gap the first spec could not: it matched nothing, stripped nothing, reported
|
|
15
|
+
`0 history sections excluded` and **exited 0**. That is indistinguishable from a
|
|
16
|
+
document with no history to strip — a silent no-op presenting as success, in a
|
|
17
|
+
tool built to prevent exactly that class of problem.
|
|
18
|
+
|
|
19
|
+
The ELI16 and the side-effects review now both carry the warning: read what the
|
|
20
|
+
tool says it *did*, not just whether it succeeded.
|
|
21
|
+
|
|
22
|
+
Two fixes to `scripts/generate-spec-contract.mjs`, both found by reviewing the
|
|
23
|
+
tool's own output as a build artifact for the first time:
|
|
24
|
+
|
|
25
|
+
1. **Meta-blockquotes are stripped.** Blocks that talk *about* the document —
|
|
26
|
+
normative-boundary markers, "this file is rationale", scope-change notices —
|
|
27
|
+
are now removed alongside history sections, and counted separately.
|
|
28
|
+
|
|
29
|
+
2. **The banner stopped overclaiming.** It previously said review history "is
|
|
30
|
+
deliberately absent." That was false: narrative prose that states a rule and
|
|
31
|
+
narrates its own history in the same sentence cannot be separated by a
|
|
32
|
+
transform, and such sentences remained. The banner now states what is removed,
|
|
33
|
+
what is not, and prints the **count of `round-N` references still present**.
|
|
34
|
+
|
|
35
|
+
**Why it matters.** The first version produced a file claiming to be
|
|
36
|
+
normative-only that contained a `NON-NORMATIVE FROM HERE` marker — a boundary
|
|
37
|
+
marker inside a document denying it had boundaries. An implementer trusting the
|
|
38
|
+
banner would have been trusting a false statement. A tool that lies in its own
|
|
39
|
+
header is worse than one with a stated limit, and this tool exists specifically
|
|
40
|
+
to stop retired designs from being implemented.
|
|
41
|
+
|
|
42
|
+
## What to Tell Your User
|
|
43
|
+
|
|
44
|
+
None — internal change (no user-facing surface).
|
|
45
|
+
|
|
46
|
+
## Summary of New Capabilities
|
|
47
|
+
|
|
48
|
+
None — internal change (no user-facing surface).
|
|
49
|
+
|
|
50
|
+
## Evidence
|
|
51
|
+
|
|
52
|
+
- Before: `inbound-message-recording-gap` generated 5% smaller with **0 sections
|
|
53
|
+
excluded**. After: 12% smaller with **1 section excluded**.
|
|
54
|
+
- The large spec is unaffected — still 37 sections excluded, 37% smaller —
|
|
55
|
+
confirming the widening did not over-match.
|
|
56
|
+
- `--check` passes for both specs after regeneration.
|
|
57
|
+
- Side-effects review updated with the finding under "under-block" rather than
|
|
58
|
+
only fixing the code.
|
|
59
|
+
|
|
60
|
+
- Before: `inbound-message-recording-gap` generated with **3 meta-blocks
|
|
61
|
+
surviving**, banner claiming history absent, `NON-NORMATIVE` marker present in
|
|
62
|
+
the output.
|
|
63
|
+
- After: 1 history section + **3 meta-blocks** excluded, 13% smaller, zero
|
|
64
|
+
`NON-NORMATIVE` occurrences, banner reporting **14 narrative round-references
|
|
65
|
+
remain**.
|
|
66
|
+
- Regression check on the large spec: unchanged behaviour — still **37 history
|
|
67
|
+
sections, 0 meta-blocks, 37% smaller** — confirming the new pattern does not
|
|
68
|
+
over-match on a document that has none.
|
|
69
|
+
- `--check` passes for both specs after regeneration and reports the new counts.
|
|
70
|
+
|
|
71
|
+
## Second change in the same push: `--strict` (allowlist mode)
|
|
72
|
+
|
|
73
|
+
The default transform is a **denylist** — "remove what is definitely history,
|
|
74
|
+
keep everything else." For an implementation artifact that is the wrong default:
|
|
75
|
+
rationale, accepted residuals and self-correcting narrative are all "not
|
|
76
|
+
definitely history", so they survive. Seven consecutive review rounds (33-39)
|
|
77
|
+
said the generated contract still read as archaeology.
|
|
78
|
+
|
|
79
|
+
`--strict` inverts it to an **allowlist** of contract-bearing headings (final
|
|
80
|
+
contract, rollout, honest limits, decision points, test plan, dependencies).
|
|
81
|
+
Everything else is absent by default rather than by pattern-match, and the output
|
|
82
|
+
lands at `<slug>.contract.strict.md`.
|
|
83
|
+
|
|
84
|
+
**Evidence:** `inbound-message-recording-gap` 1487 → 874 lines (42% smaller, 11
|
|
85
|
+
allowlisted sections). `outbound-gate-advisory-override` 2765 → 270 lines (**90%
|
|
86
|
+
smaller**, 8 sections) — a spec that never converged in 33 rounds, and whose
|
|
87
|
+
history-stripped denylist version was still large enough to time a reviewer out,
|
|
88
|
+
now has a contract short enough to actually review.
|
|
89
|
+
|
|
90
|
+
Additive: the default mode and its output path are unchanged.
|
|
91
|
+
|
|
92
|
+
## And the predicted failure happened immediately
|
|
93
|
+
|
|
94
|
+
The over-block risk documented for `--strict` — dropping a normative section whose
|
|
95
|
+
heading is not on the allowlist — occurred on the first spec with a different
|
|
96
|
+
heading scheme. `outbound-gate-advisory-override` captured 8 of 66 sections and
|
|
97
|
+
lost its normative outcome table; the reviewer's first finding was "normative
|
|
98
|
+
behavior is missing from the strict contract."
|
|
99
|
+
|
|
100
|
+
A `WARNING (strict)` now fires when a spec with >=8 headings matches under 25% of
|
|
101
|
+
them, naming the ratio. It flags that spec at 12% and stays silent on the one that
|
|
102
|
+
captures correctly. It warns rather than refuses, because the ratio is a heuristic
|
|
103
|
+
and a rationale-heavy spec can legitimately capture low — a refusal on a heuristic
|
|
104
|
+
blocks correct output, a warning that names the number hands the judgment to a
|
|
105
|
+
human.
|
|
106
|
+
|
|
107
|
+
## A content-loss guard for `--strict`
|
|
108
|
+
|
|
109
|
+
The capture-ratio warning catches *too few sections matching*. It cannot see a
|
|
110
|
+
section that matched and captured **nothing** — which happened live: a
|
|
111
|
+
sub-heading added inside an allowlisted section ended that section's capture and
|
|
112
|
+
dropped the entire contract table from the output, while the section count stayed
|
|
113
|
+
identical and the guard stayed silent.
|
|
114
|
+
|
|
115
|
+
A second warning now compares captured bytes to source bytes per section. Three
|
|
116
|
+
implementations were needed: averaging hid the empty section behind healthy ones;
|
|
117
|
+
an absolute threshold false-positived permanently on legitimate container
|
|
118
|
+
headings; and the working version required the source measurement to stop using
|
|
119
|
+
the capture's own rule, which had made it shrink in lockstep with the truncation.
|
|
120
|
+
|
|
121
|
+
Each version was verified against the real regression and the correct file.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Five built-in job templates authenticated their `gate:` line but not their prompt bodies, so all 12 body calls to authenticated endpoints returned `401 Missing or invalid Authorization header` at runtime. The affected jobs — `evolution-proposal-evaluate`, `evolution-proposal-implement`, `evolution-overdue-check`, `insight-harvest`, `reflection-trigger` — woke on schedule, passed their gate, read nothing, and exited "silently". A silently-failing job and a healthy quiet job are indistinguishable, which is why this survived.
|
|
9
|
+
|
|
10
|
+
Each body now resolves the canonical env-first `AUTH` / `AGENT_ID` / `PORT` block and passes `Authorization: Bearer $AUTH` + `X-Instar-AgentId: $AGENT_ID` on every non-public call.
|
|
11
|
+
|
|
12
|
+
Two same-class defects found while in the files were fixed in the same change:
|
|
13
|
+
|
|
14
|
+
- `identity-review.md` defined `AUTH` by reading `.instar/config.json` only. On an agent whose stored token has drifted from the live server token, that read is rejected — confirmed on the dev agent, where the 16-char config value returns `Invalid auth token` and the 36-char `$INSTAR_AUTH_TOKEN` succeeds. It now prefers the environment value.
|
|
15
|
+
- `reflection-trigger.md` emitted its instruction through a double-quoted `echo` whose inner double quotes were consumed by the shell, rendering `-d '{type:quick}'` — invalid JSON — in addition to carrying no auth. Both fixed; the token name is left unexpanded so no secret enters the transcript.
|
|
16
|
+
|
|
17
|
+
A new CI lint (`tests/unit/job-template-auth-lint.test.ts`) scans every shipped template for curl calls to non-public endpoints missing an `Authorization` header, and for templates that use `$AUTH` without defining it. Its public-endpoint exemptions mirror `authMiddleware()` and a drift guard asserts each exempted path still appears there.
|
|
18
|
+
|
|
19
|
+
The fix is applied to the **source** templates, not deployed copies: `installBuiltinJobs()` unconditionally overwrites every deployed template on each update run (`InstallBuiltinJobs.ts:127`, reached from `PostUpdateMigrator.ts:4046`), so a hand-patched deployed copy would have been silently reverted at the next update. No separate migration is required — existing agents receive the fix through the normal update path.
|
|
20
|
+
|
|
21
|
+
## What to Tell Your User
|
|
22
|
+
|
|
23
|
+
Some scheduled jobs that have always been silent will start producing output — that is the repair working, not a new fault. If a user asks why their proposal-review or insight-harvest job has suddenly become active after months of quiet, the honest answer is that it was never running its body successfully; it was being refused at the door and exiting without saying so.
|
|
24
|
+
|
|
25
|
+
## Summary of New Capabilities
|
|
26
|
+
|
|
27
|
+
No new user-facing capability. Five existing scheduled jobs go from silently non-functional to functional, and a build-time lint prevents the class from returning.
|
|
28
|
+
|
|
29
|
+
## Evidence
|
|
30
|
+
|
|
31
|
+
- Run against the six pre-fix files extracted from `HEAD`, the lint reports exactly 12 unauthenticated calls — `evolution-overdue-check` 3, `evolution-proposal-evaluate` 3, `evolution-proposal-implement` 2, `insight-harvest` 3, `reflection-trigger` 1 — matching an independently-derived census. Against the fixed files: 0.
|
|
32
|
+
- Live: an unauthenticated call to `/evolution/proposals?status=approved` returns `{"error":"Missing or invalid Authorization header"}`; the fixed template's resolved command returns the real proposal list. Same confirmed for `/evolution/learnings?applied=false` and `/evolution/actions/overdue`.
|
|
33
|
+
- Executing the original `reflection-trigger.md` echo renders `-d '{type:quick}'`; the fixed line renders `-d '{"type":"quick"}'`.
|
|
34
|
+
- The lint's first draft falsely flagged three healthy templates (`initiative-digest-review`, `mentor-onboarding`, `org-intent-drift-audit`) that define `AUTH` inside backticks; the detector was corrected and the false positive pinned by a regression test.
|
|
35
|
+
- `tests/unit/job-template-auth-lint.test.ts` (9 tests) green; template-family tests (`refresh-jobs`, `default-jobs-valid`, `PostUpdateMigrator-templateResolution`) green.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Built-in job templates must authenticate their API calls — Plain-English Overview
|
|
2
|
+
|
|
3
|
+
> The one-line version: five of your scheduled background jobs have been quietly doing nothing for their entire existence, because their instructions told them to knock on a locked door without the key.
|
|
4
|
+
|
|
5
|
+
## The problem in one breath
|
|
6
|
+
|
|
7
|
+
Your agent runs scheduled background jobs — small recurring tasks like "review new improvement proposals" or "harvest lessons learned". Each job has a **doorman check** (does this job have anything to do right now?) and a **body** (the actual instructions). The doorman checks were carrying the agent's key. The instructions were not. So every one of those jobs woke up, correctly decided "yes, there's work here", walked to the door, got refused, and exited — reporting nothing wrong.
|
|
8
|
+
|
|
9
|
+
The reason nobody noticed for so long is the cruel part: these jobs are *designed* to be silent when there's nothing to do. A job that fails silently and a job that succeeds with nothing to report look **exactly the same** from the outside.
|
|
10
|
+
|
|
11
|
+
## What already exists
|
|
12
|
+
|
|
13
|
+
- **The scheduled jobs themselves** — small recurring tasks your agent runs on a timer. They already work as a system; the scheduler, the timing, and the reporting are all fine.
|
|
14
|
+
- **The lock on the agent's own API** — nearly every internal endpoint requires a key (a "bearer token"). A handful of endpoints are deliberately public, like the basic health check.
|
|
15
|
+
- **The key itself** — handed to every job through its environment. It was there the whole time. The instructions just never picked it up.
|
|
16
|
+
- **The update pipeline** — whenever your agent updates, it rewrites its job instruction files from the shipped originals. This matters enormously, and it's covered below.
|
|
17
|
+
|
|
18
|
+
## What this adds
|
|
19
|
+
|
|
20
|
+
**The instructions now pick up the key.** Each of the affected job bodies now starts by resolving the agent's key, its identity, and its port, and every call it makes carries them. That's the whole fix — about thirty lines of text across six files.
|
|
21
|
+
|
|
22
|
+
Alongside the fix, a **build-time check** now scans every shipped job instruction file and fails the build if any of them calls a locked endpoint without the key. Secondary changes:
|
|
23
|
+
|
|
24
|
+
- One more job (`identity-review`) was reading the key from a **stale source**. On this very agent, that stored copy has drifted out of date and is rejected by the server, while the live one works. It now prefers the live one.
|
|
25
|
+
- One job (`reflection-trigger`) had a **second, unrelated bug on the same line**: a quoting mistake that mangled the data it sends into invalid gibberish. Fixed too, since the line was being rewritten anyway.
|
|
26
|
+
|
|
27
|
+
## The new pieces
|
|
28
|
+
|
|
29
|
+
- **The build-time check** — reads every shipped job instruction file, finds every call it makes, and asks two questions: is this endpoint locked, and if so, is the key present? It also refuses instructions that *use* a key they never *fetch*. What it is explicitly **not** allowed to do: anything at runtime. It cannot block your agent, delay a message, or stop a job. It is a test. Its only power is to fail a build before broken instructions ever reach you. That line matters — a check this simple must never be given authority over a live agent.
|
|
30
|
+
|
|
31
|
+
## The safeguards
|
|
32
|
+
|
|
33
|
+
**Prevents the same bug from shipping again.** The check was proven honest before being trusted: run against the *old* files, it reports exactly the twelve broken calls that actually shipped — not eleven, not thirteen. Run against the fixed files, zero. A check that has never been shown to catch the real bug is just decoration.
|
|
34
|
+
|
|
35
|
+
**Prevents the check from crying wolf.** The first draft of the check falsely accused three perfectly healthy job files, because they fetch their key in a slightly different but entirely valid style. That was caught, fixed, and locked down with its own test so it can't come back. A check that fails on healthy files gets switched off by the next frustrated person, which is worse than having no check.
|
|
36
|
+
|
|
37
|
+
**Prevents a fix that only looks like a fix.** This is the trap this change deliberately avoided. Your agent rewrites its job files from the shipped originals on **every update**. So "fixing" the copies sitting on this machine would have worked perfectly — until the next update silently wiped them. That's a fix indistinguishable from a real one, which is exactly the kind of illusion this family of bugs keeps producing. The change is made to the shipped originals, so it reaches every agent through the normal update path and stays fixed.
|
|
38
|
+
|
|
39
|
+
**Prevents guessing about which doors are locked.** The check doesn't keep its own private list of public endpoints — that would drift out of date. It mirrors the real list and asserts the real one still contains those entries, so if a public endpoint ever becomes locked, the check fails loudly instead of quietly waving it through.
|
|
40
|
+
|
|
41
|
+
## What ships when
|
|
42
|
+
|
|
43
|
+
All of it ships together, in one change — the six corrected instruction files and the check that keeps them correct. There's no phased rollout because there's nothing to roll out gradually: the files are either right or wrong, and they're currently wrong.
|
|
44
|
+
|
|
45
|
+
## What you'll actually notice
|
|
46
|
+
|
|
47
|
+
Some jobs that have always been silent will **start producing output**. That's the repair working, not a new problem — but it's worth knowing in advance so a suddenly-chatty proposal-review job doesn't read as a malfunction.
|
|
48
|
+
|
|
49
|
+
## If it goes wrong
|
|
50
|
+
|
|
51
|
+
Undo the six files and ship the patch; agents pick up the reversal on their next update. Nothing is stored, nothing is migrated, nothing needs repairing. The worst case is a return to exactly today's behaviour — quiet jobs that don't do anything. That asymmetry is the honest argument for making this change: there's almost nothing to lose, and five broken jobs to gain back.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Side-Effects Review — Built-in job templates must authenticate their API calls
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `evo007-job-template-auth`
|
|
4
|
+
**Date:** `2026-07-25`
|
|
5
|
+
**Author:** `Echo (instar-dev agent)`
|
|
6
|
+
**Second-pass reviewer:** `not required`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Five shipped built-in job templates carried a `gate:` line that authenticates correctly, but **prompt bodies** that called authenticated endpoints with a bare `curl -s http://localhost:.../evolution/...`. All 12 of those calls return `401 Missing or invalid Authorization header` at runtime. The gate passing is precisely what hid the failure: the job would wake up, correctly conclude "there is work to do", then read nothing and exit "silently" — indistinguishable from a healthy no-op. A sixth template (`identity-review.md`) defined `AUTH` by reading `.instar/config.json` only, which is broken on any agent whose config token has drifted from the live server token (observed first-hand on this agent: the 16-char config value is rejected, the 36-char `$INSTAR_AUTH_TOKEN` is accepted).
|
|
11
|
+
|
|
12
|
+
Files touched: six templates under `src/scaffold/templates/jobs/instar/` (`evolution-overdue-check`, `evolution-proposal-implement`, `evolution-proposal-evaluate`, `insight-harvest`, `reflection-trigger`, `identity-review`), plus a new CI lint at `tests/unit/job-template-auth-lint.test.ts`. Each fixed body now resolves the canonical `AUTH` / `AGENT_ID` / `PORT` block (env-first, config fallback) and passes `Authorization: Bearer $AUTH` + `X-Instar-AgentId: $AGENT_ID` on every non-public call.
|
|
13
|
+
|
|
14
|
+
Two additional defects were found and fixed while in the file, both on the same `reflection-trigger.md` line: the echoed instruction rendered `-d '{type:quick}'` (unescaped inner double quotes were eaten by the enclosing double-quoted `echo`, producing invalid JSON), and it carried no auth. Both are fixed with escaped quoting, verified by executing the echo.
|
|
15
|
+
|
|
16
|
+
## Decision-point inventory
|
|
17
|
+
|
|
18
|
+
- `tests/unit/job-template-auth-lint.test.ts` — **add** — a CI lint (build-time signal). It has no runtime surface and cannot gate, delay, or block any agent action.
|
|
19
|
+
- Job template bodies — **modify** — these are LLM instructions, not code. They do not make block/allow decisions; they instruct the reading agent which HTTP calls to make.
|
|
20
|
+
- `src/server/middleware.ts` `authMiddleware()` — **pass-through** — not modified. The lint mirrors its public-path exemptions and asserts (drift guard) that each exempted path still appears there.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 1. Over-block
|
|
25
|
+
|
|
26
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
27
|
+
|
|
28
|
+
The only rejecting surface is the CI lint, and its over-block risk is a false CI failure on a healthy template. This risk **materialized during development and was fixed**: the first draft anchored the `AUTH=` definition check to line-start (`/^\s*AUTH=/m`) and falsely accused three healthy templates (`initiative-digest-review.md`, `mentor-onboarding.md`, `org-intent-drift-audit.md`) that legitimately define `AUTH` inside backticks as a prose step (`0. **Set auth context:** \`AUTH="..."\``). The detector now matches `AUTH=` not preceded by `$` or a word character, and a regression test (`accepts an AUTH definition presented as a prose step in backticks`) pins the fix.
|
|
29
|
+
|
|
30
|
+
Residual over-block risk: a future template that intentionally calls a non-public endpoint *without* auth (e.g. to demonstrate a 401 in documentation prose) would be flagged. No such template exists today; the fix would be to exempt it explicitly rather than weaken the rule.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 2. Under-block
|
|
35
|
+
|
|
36
|
+
**What failure modes does this still miss?**
|
|
37
|
+
|
|
38
|
+
Concrete and acknowledged:
|
|
39
|
+
|
|
40
|
+
- **Multi-line curl continuations.** The detector scopes a curl invocation to its line. A template using `curl \` + newline + `-H "Authorization: ..."` would have its header on the next line and be flagged (over-block) — or, if the URL is on a later line, the call would not be seen at all (under-block). No shipped template uses continuations; all are single-line.
|
|
41
|
+
- **Non-curl HTTP clients.** A template instructing `wget`, `httpie`, or a `node -e` fetch is not scanned at all.
|
|
42
|
+
- **Correct header, wrong token.** The lint verifies an `Authorization: Bearer` header is *present*; it cannot verify the token *resolves*. That is exactly the `identity-review.md` failure mode (header present, value stale) — caught here by a separate assertion requiring the env-first `${INSTAR_AUTH_TOKEN:-` form in the six known templates, not by the generic rule.
|
|
43
|
+
- **Runtime drift.** A template can be correct at CI time and still 401 at runtime if `$INSTAR_AUTH_TOKEN` is absent from the job environment *and* config.json is stale. The lint cannot see runtime env.
|
|
44
|
+
- **Endpoints that become non-public later.** If a currently-public path (say `/ping`) later requires auth, the lint would keep exempting it. The drift guard only catches *removal* of the path from middleware.ts, not a change in its auth posture.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 3. Level-of-abstraction fit
|
|
49
|
+
|
|
50
|
+
This is a **build-time lint** — the correct layer. The defect is static text in a shipped artifact, fully determinable from source, so it belongs in CI rather than at runtime.
|
|
51
|
+
|
|
52
|
+
Considered and rejected: a runtime check that inspects job bodies before dispatch. That would be the wrong layer — it would pay a cost on every job run to detect a fault that cannot change after the template ships, and it would need blocking authority to be useful, which the signal-vs-authority principle forbids for logic this brittle.
|
|
53
|
+
|
|
54
|
+
The lint extends the existing template-validation family (`default-jobs-valid.test.ts`, `PostUpdateMigrator-templateResolution.test.ts`) rather than running parallel to it. It reads its exemption set *from* the real authority (`middleware.ts`) rather than re-deriving one.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 4. Signal vs authority compliance
|
|
59
|
+
|
|
60
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
61
|
+
|
|
62
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
63
|
+
|
|
64
|
+
- [x] No — this change has no block/allow surface.
|
|
65
|
+
|
|
66
|
+
The lint is a test. Its only power is failing a build. It holds no authority over any agent at runtime: it cannot block a message, gate a job, delay a session, or constrain a decision. The template edits are instructions to an LLM, not control flow. Nothing in this change can refuse an agent action.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 4b. Judgment-point check (Judgment Within Floors standard)
|
|
71
|
+
|
|
72
|
+
**No new static heuristic at a competing-signals decision point.** The question "does this curl carry an Authorization header, and is its endpoint public?" is fully enumerable from source: the public-path set is a finite list read from `authMiddleware()`, and header presence is a textual fact. There are no competing live signals to weigh — no work evidence, liveness, recency, or ownership inputs. This is an invariant check, not a judgment point.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 5. Interactions
|
|
77
|
+
|
|
78
|
+
- **Shadowing:** None. The lint is an independent test file; vitest runs it alongside the existing template tests with no ordering dependency. It does not shadow `default-jobs-valid.test.ts` (which validates job *structure* from `init.ts`, not template *body content*) — the two cover disjoint surfaces.
|
|
79
|
+
- **Double-fire:** None. No runtime component acts on this.
|
|
80
|
+
- **Races:** None. The lint only reads files at test time.
|
|
81
|
+
- **Feedback loops:** One worth naming explicitly. `evolution-proposal-implement.md` is the template that runs the job that produced this change — the fix is self-referential. It is *not* a loop: the template is data read by a scheduled job, and repairing it changes which HTTP calls a future run makes, not whether this change is correct. Verified by executing the fixed commands directly against the live server rather than trusting the job's own behavior.
|
|
82
|
+
- **Migration parity:** `installBuiltinJobs()` (`src/scheduler/InstallBuiltinJobs.ts:127`) unconditionally `writeFileSync`-es every shipped template over the deployed copy, and is called from `PostUpdateMigrator.ts:4046` on every update run. Verified first-hand by reading both call sites. **No separate `PostUpdateMigrator` entry is needed** — and, critically, patching the deployed copies by hand would have been *reverted* at the next update, producing a transient fix indistinguishable from a real one. The durable fix is the source templates, which is what this change edits.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 6. External surfaces
|
|
87
|
+
|
|
88
|
+
- **Other agents on the same machine:** No change. Templates are per-agent state; each agent receives the fixed copy at its next install/update.
|
|
89
|
+
- **Other users of the install base:** Yes, positively — five scheduled jobs that have been silently no-op-ing on every install will begin functioning. Anyone who had these jobs enabled gets working evolution/insight/reflection/identity jobs. Worth calling out in release notes: behavior that appeared "quiet and healthy" will start producing real output.
|
|
90
|
+
- **External systems:** None. All calls are to `localhost`.
|
|
91
|
+
- **Persistent state:** None added. The jobs will now successfully write to existing stores (evolution proposals/actions/learnings, reflection records) that they were previously failing to reach — that is the intended repair, not a new surface.
|
|
92
|
+
- **Timing / runtime conditions:** The fix depends on `$INSTAR_AUTH_TOKEN` being present in the job environment (verified present, 36 chars) with config.json as fallback.
|
|
93
|
+
- **Operator surface (Mobile-Complete Operator Actions):** No operator-facing actions added or touched. This change adds no route, form, approval, or grant.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 6b. Operator-surface quality (Operator-Surface Quality standard)
|
|
98
|
+
|
|
99
|
+
**No operator surface — not applicable.** No dashboard renderer, markup file, approval page, or grant/revoke/secret-drop form is touched. The change is confined to shipped job-template markdown and a test file.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
104
|
+
|
|
105
|
+
**Machine-local BY DESIGN**, with reason: job templates are installed onto each machine's own `.instar/jobs/instar/` by that machine's own `installBuiltinJobs()` run, and the auth token they resolve (`$INSTAR_AUTH_TOKEN` / that machine's `config.json`) is **necessarily** machine-local — each install holds its own `authToken`, and a shared token cannot work cross-machine. Replicating template content or tokens across machines would be actively wrong.
|
|
106
|
+
|
|
107
|
+
Every machine converges on the same fixed template content through the same update path, so there is no divergence risk: this is not "machine-local state that should have been replicated", it is identical shipped content resolved against per-machine credentials.
|
|
108
|
+
|
|
109
|
+
- **User-facing notices:** None emitted by this change. (The *jobs* may now emit notices they previously failed to produce, but each job's own one-voice/topic-routing behavior is unchanged by this fix.)
|
|
110
|
+
- **Durable state on topic transfer:** None held. Nothing strands on a topic move.
|
|
111
|
+
- **Generated URLs:** None generated. All URLs are `localhost` calls made by the job on its own machine, which is correct — they are never shared or sent to a user.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 8. Rollback cost
|
|
116
|
+
|
|
117
|
+
- **Hot-fix release:** Revert the six template files and the test; ship as a patch. Existing agents pick up the reverted content at their next update via the same `installBuiltinJobs()` overwrite path.
|
|
118
|
+
- **Data migration:** None. No schema, column, or persistent state introduced.
|
|
119
|
+
- **Agent state repair:** None required.
|
|
120
|
+
- **User visibility during rollback:** Reverting restores the *previous broken behavior* (jobs silently no-op again). No error state, no crash, no data loss — the pre-fix behavior was quiet failure, so a rollback is quiet too. That asymmetry is itself the argument for why this class of bug survived so long.
|
|
121
|
+
|
|
122
|
+
Rollback risk is minimal: worst case is a return to the status quo ante.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Conclusion
|
|
127
|
+
|
|
128
|
+
This review produced two changes to the work in progress. First, the lint's `AUTH=` detector was rewritten after it falsely accused three healthy templates — a real over-block caught before commit, now pinned by a regression test. Second, the scope grew by two genuine same-class defects found by reading rather than assuming: `identity-review.md`'s config-only token read (broken on this very agent, proven by executing both tokens against the live server) and `reflection-trigger.md`'s quote-mangled JSON body.
|
|
129
|
+
|
|
130
|
+
The change is clear to ship. It has no runtime authority, no persistent state, no operator surface, and a trivial rollback. The verification is first-hand rather than proxy: the lint was proven to report **exactly** the 12 pre-fix violations when run against the original content from `HEAD` and zero after, and the fixed commands were executed against the live server returning real data instead of 401.
|
|
131
|
+
|
|
132
|
+
One honest limitation, stated rather than buried: the lint verifies an auth header is *present*, not that its token *resolves*. Token-resolution correctness is covered for the six known templates by the env-first assertion, but a future template could pass the lint and still 401 with a stale token. Closing that would require a runtime check, which is the wrong layer for the reasons in §3.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Second-pass review (if required)
|
|
137
|
+
|
|
138
|
+
**Reviewer:** not required.
|
|
139
|
+
|
|
140
|
+
Phase 5 triggers on changes touching block/allow decisions on messaging or dispatch, session lifecycle, context/compaction, coherence gates, trust levels, or any sentinel/guard/gate/watchdog. This change touches none of them: it is shipped instruction text plus a CI lint with no runtime surface. The word "gate" appears in the templates' `gate:` frontmatter field, but those lines are **not modified** by this change.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Evidence pointers
|
|
145
|
+
|
|
146
|
+
- **Lint bites (pre-fix):** running the detector against the six original files extracted from `HEAD` reported exactly 12 unauthenticated calls — `evolution-overdue-check` 3, `evolution-proposal-evaluate` 3, `evolution-proposal-implement` 2, `insight-harvest` 3, `reflection-trigger` 1 — matching the independently-derived census. Post-fix: 0.
|
|
147
|
+
- **Live 401 → 200:** `curl` to `/evolution/proposals?status=approved` with no header returns `{"error":"Missing or invalid Authorization header"}`; the fixed template's resolved command returns the real proposal list. Same confirmed for `/evolution/learnings?applied=false` and `/evolution/actions/overdue`.
|
|
148
|
+
- **Stale-config proof:** the 16-char `config.json` token returns `{"error":"Invalid auth token"}`; the 36-char `$INSTAR_AUTH_TOKEN` succeeds — the concrete reason `identity-review.md` needed the env-first form.
|
|
149
|
+
- **Quoting proof:** executing the original `reflection-trigger.md` echo renders `-d '{type:quick}'` (invalid JSON); the fixed line renders `-d '{"type":"quick"}'` with the token name left unexpanded so no secret enters the transcript.
|
|
150
|
+
- **Migration parity:** `InstallBuiltinJobs.ts:127` unconditional overwrite, reached from `PostUpdateMigrator.ts:4046`.
|
|
151
|
+
- **Tests:** `tests/unit/job-template-auth-lint.test.ts` (9 tests) green; template-family tests (`refresh-jobs`, `default-jobs-valid`, `PostUpdateMigrator-templateResolution`) green.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Class-Closure Declaration (display-only mirror)
|
|
156
|
+
|
|
157
|
+
- **`defectClass`** — `novel` is *not* claimed. This is an instance of the existing **unverified-claim / proxy-signal** family already tracked by EVO-004 (ACT-930), EVO-005, and EVO-006 (ACT-985): a signal adjacent to the real terminal state (the `gate:` passing, "the job ran quietly") was treated as evidence of the terminal state (the job actually did its work). Recorded here as `defectClass: proxy-signal-substitution`.
|
|
158
|
+
- **`closure`** — `guard`.
|
|
159
|
+
- **`guardEvidence`** — `{enforcementType: lint, citation: tests/unit/job-template-auth-lint.test.ts#"every shipped template authenticates its non-public API calls", howCaught: the lint scans every shipped template body for curl calls to non-public endpoints lacking an Authorization header and fails the build; run against the pre-fix content it reports exactly the 12 violations that shipped, so this defect could not have reached main with the guard in place}`.
|
|
160
|
+
- **`gap`** — none for the template-text class. The broader "a job that silently no-ops looks identical to a healthy quiet job" class is **not** closed by this change and remains tracked under the EVO-005/EVO-006 family; this fix closes only the specific mechanism (missing auth header in shipped template text).
|
|
@@ -33,6 +33,38 @@ silent.
|
|
|
33
33
|
|
|
34
34
|
## 2. Under-block — what does it still miss?
|
|
35
35
|
|
|
36
|
+
**Found in practice, not in review:** the first version recognised only per-round
|
|
37
|
+
`## Round-N … change log` headings. Pointed at a second spec that consolidates its
|
|
38
|
+
history into a single `## Review record` section, it matched nothing, stripped
|
|
39
|
+
nothing, reported `0 history sections excluded` and **exited 0**. A silent no-op
|
|
40
|
+
that presents as success — the same species as a check that cannot run.
|
|
41
|
+
|
|
42
|
+
Widened by one alternative (`Review record`). The residual is unchanged and now
|
|
43
|
+
explicit: the pattern is a closed list of heading shapes, so a document using a
|
|
44
|
+
convention not on the list is silently unstripped. The mitigation is the output
|
|
45
|
+
line itself — it always reports how many sections it excluded, so *reading* it
|
|
46
|
+
catches this. The ELI16 now says so directly.
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
**Found again, worse, by pointing the tool at its own output (round-37, codex).**
|
|
50
|
+
The generated contract was reviewed as the build artifact for the first time, and
|
|
51
|
+
it was **not contract-only**: blockquote meta-blocks survived, including a
|
|
52
|
+
"NON-NORMATIVE FROM HERE" marker sitting inside a file whose banner claimed
|
|
53
|
+
review history was "deliberately absent." A boundary marker inside a document
|
|
54
|
+
that denies having boundaries is worse than no marker — and the banner was
|
|
55
|
+
straightforwardly false.
|
|
56
|
+
|
|
57
|
+
Two fixes, one mechanical and one honest:
|
|
58
|
+
|
|
59
|
+
1. **Meta-blockquotes are now stripped** — blocks that talk *about* the document
|
|
60
|
+
(normative-boundary markers, "this file is rationale", scope notices) rather
|
|
61
|
+
than about the design. Counted and reported separately from history sections.
|
|
62
|
+
2. **The banner no longer overclaims.** It now states what is removed, what is
|
|
63
|
+
*not* removed, and prints the **count of narrative round-references remaining**
|
|
64
|
+
in the file. The previous wording promised absence it could not deliver, which
|
|
65
|
+
is exactly the class of overclaim the spec under review kept being corrected
|
|
66
|
+
for. A tool that lies in its own header is worse than one with a stated limit.
|
|
67
|
+
|
|
36
68
|
- **Narrative history inside normative prose.** The generator strips history
|
|
37
69
|
*sections* and *inline annotations*; it cannot strip a paragraph of normative
|
|
38
70
|
prose that happens to narrate how a decision evolved. Those remain in the
|
|
@@ -44,6 +76,121 @@ silent.
|
|
|
44
76
|
contradictory. The separate spec lint (required by the outbound spec) is what
|
|
45
77
|
addresses that; this is not it.
|
|
46
78
|
|
|
79
|
+
### `--strict` (allowlist mode) — over/under-block
|
|
80
|
+
|
|
81
|
+
**Over-block risk, and it is real:** an allowlist drops a genuinely normative
|
|
82
|
+
section whose heading is not on the list. That is a *silent* omission — the
|
|
83
|
+
opposite failure from the denylist's silent retention. Mitigated by the run
|
|
84
|
+
reporting `N allowlisted sections kept`: a spec that should yield 10 sections and
|
|
85
|
+
reports 2 is visibly wrong, which is exactly how the missing-trailing-period bug
|
|
86
|
+
in the heading pattern was caught (§4 "Honest limits" and §5 "Test plan" were
|
|
87
|
+
silently dropped until the count was read).
|
|
88
|
+
|
|
89
|
+
That mitigation is a human reading a number, which is weaker than a check. The
|
|
90
|
+
honest position: **`--strict` trades a silent-retention failure for a
|
|
91
|
+
silent-omission failure**, and the omission is the one that could cause a missing
|
|
92
|
+
requirement rather than a confusing one. It is therefore additive and opt-in —
|
|
93
|
+
the default denylist mode and its output path are unchanged, and no build gate
|
|
94
|
+
requires strict mode.
|
|
95
|
+
|
|
96
|
+
**The over-block risk was confirmed within an hour of shipping it.** Pointed at
|
|
97
|
+
`outbound-gate-advisory-override`, strict mode kept 8 of 66 sections and dropped
|
|
98
|
+
the normative outcome table, whose headings are not on the allowlist. The
|
|
99
|
+
reviewer's *first* finding was "normative behavior is missing from the strict
|
|
100
|
+
contract" — the exact silent-omission failure predicted above, on the first spec
|
|
101
|
+
with a different heading scheme.
|
|
102
|
+
|
|
103
|
+
**Guard added:** strict mode now computes the kept-section ratio and prints a
|
|
104
|
+
`WARNING (strict)` when a spec with >=8 headings matches under 25% of them,
|
|
105
|
+
naming the ratio and saying the output may be missing normative sections. It
|
|
106
|
+
fires on `outbound-gate-advisory-override` (8/66, 12%) and stays silent on
|
|
107
|
+
`inbound-message-recording-gap` (11 sections, 41% capture) — a real signal on the
|
|
108
|
+
real failure, no noise on the working case.
|
|
109
|
+
|
|
110
|
+
The guard is a warning, not a refusal, and deliberately: the ratio is a heuristic,
|
|
111
|
+
and a genuinely rationale-heavy spec can legitimately capture low. A refusal on a
|
|
112
|
+
heuristic would block correct output; a warning that names the number lets a human
|
|
113
|
+
apply the judgment the tool does not have.
|
|
114
|
+
|
|
115
|
+
### A third failure mode, found by shipping it (2026-07-25)
|
|
116
|
+
|
|
117
|
+
The capture-ratio warning guards against *too few sections matching*. It does not
|
|
118
|
+
see **a matched section that captured nothing** — and that happened live: adding a
|
|
119
|
+
`### Normative checklist` heading inside the allowlisted `### 3.0 Final contract`
|
|
120
|
+
ended that section's capture, dropping the entire contract table **and the new
|
|
121
|
+
checklist** from the output. The section count stayed at 10, so the existing guard
|
|
122
|
+
was silent, and the emptied contract would have shipped.
|
|
123
|
+
|
|
124
|
+
A second guard now compares **captured bytes against source bytes per section**
|
|
125
|
+
and warns when a section captures under 25% of a source section over 1 KB.
|
|
126
|
+
|
|
127
|
+
**Getting it right took three attempts, each verified against the real regression
|
|
128
|
+
rather than assumed:**
|
|
129
|
+
|
|
130
|
+
1. *Average bytes per section* — nine healthy sections masked one empty one.
|
|
131
|
+
2. *Absolute per-section threshold* — permanently false-positived on legitimate
|
|
132
|
+
container headings (`## 4. Honest limits` immediately followed by an allowlisted
|
|
133
|
+
`### 4.0 …`). **A guard that always warns is a guard nobody reads.**
|
|
134
|
+
3. *Captured vs source, per section* — worked only after the source measurement
|
|
135
|
+
stopped using the capture's own stopping rule, which had made it shrink in
|
|
136
|
+
lockstep with the truncation it was meant to detect.
|
|
137
|
+
|
|
138
|
+
Verified in both directions each time: fires on the regression, silent on the
|
|
139
|
+
correct file, large-spec behaviour unchanged.
|
|
140
|
+
|
|
141
|
+
### The guard's own false-positive mode (found before merge)
|
|
142
|
+
|
|
143
|
+
The captured-vs-source guard fired permanently on every **numbered** spec. Cause:
|
|
144
|
+
the source measure deliberately does not use the capture's stopping rule (or it
|
|
145
|
+
would shrink in lockstep with the truncation it detects), so it ran past the next
|
|
146
|
+
*sibling* section and counted that sibling's bytes as "missing".
|
|
147
|
+
|
|
148
|
+
Both shapes look identical structurally — a same-level heading follows an
|
|
149
|
+
allowlisted one:
|
|
150
|
+
|
|
151
|
+
- `### 3.0 Final contract` → `### Normative checklist` — meant as a **child**;
|
|
152
|
+
its content belongs to 3.0 and losing it is a real defect.
|
|
153
|
+
- `### 3.10 Test plan` → `### 3.11 Judgeable-record` — a genuine **sibling**;
|
|
154
|
+
its content was never 3.10's and excluding it is correct.
|
|
155
|
+
|
|
156
|
+
Separated by a numbered-sibling test. Verified in both directions: silent on the
|
|
157
|
+
numbered spec, still warning on the child-heading regression.
|
|
158
|
+
|
|
159
|
+
**This is the third time this guard needed correcting, and the second time the
|
|
160
|
+
failure was "it warns constantly".** That mode is worth naming as its own defect
|
|
161
|
+
class: a permanently-firing warning is not a conservative guard, it is a disabled
|
|
162
|
+
one, because the reader stops looking.
|
|
163
|
+
|
|
164
|
+
**Also fixed before merge:** a nonexistent `--spec` path threw a raw Node stack
|
|
165
|
+
trace; it now exits 2 with a one-line error.
|
|
166
|
+
|
|
167
|
+
### The allowlist was too narrow to be useful on a second spec
|
|
168
|
+
|
|
169
|
+
Pointed at `outbound-gate-advisory-override`, strict mode produced a contract
|
|
170
|
+
**that began at the test plan** — no design section, no schema, no outcome table.
|
|
171
|
+
That is why an external reviewer reading it said *"normative behavior is missing
|
|
172
|
+
from the strict contract"*: it genuinely was. The capture-ratio warning fired
|
|
173
|
+
(8/66 sections, 12%) and was noted at the time **and not acted on**, so a
|
|
174
|
+
contract that could not be built from sat in the repo for three hours looking
|
|
175
|
+
like an artifact.
|
|
176
|
+
|
|
177
|
+
Cause: the allowlist held one spec's section names. The other spec's
|
|
178
|
+
contract-bearing sections are called `0.0 What an implementer builds`,
|
|
179
|
+
`0.2 Current design overview`, `0. Glossary` and `3.8.1 Normative outcome table`
|
|
180
|
+
— all genuinely normative, none matched.
|
|
181
|
+
|
|
182
|
+
Widened by those four plus `Fail directions`, and the numbering pattern relaxed
|
|
183
|
+
to multi-level (`3.8.1`). Capture went 8→13 sections and the contract now opens
|
|
184
|
+
with what an implementer builds. Verified no change to the other spec (10
|
|
185
|
+
sections, byte-identical behaviour) and the child-heading regression still warns.
|
|
186
|
+
|
|
187
|
+
**The warning was working and I treated it as information rather than a defect.**
|
|
188
|
+
A guard that fires and gets noted is only half a guard; the other half is
|
|
189
|
+
someone acting on it.
|
|
190
|
+
|
|
191
|
+
**Under-block:** unchanged. A rule narrated inside an allowlisted section still
|
|
192
|
+
carries its own history; strict mode reduces the surface, it does not clean prose.
|
|
193
|
+
|
|
47
194
|
## 3. Level-of-abstraction fit
|
|
48
195
|
|
|
49
196
|
Correct layer: it is a build-time document transform, alongside the other
|
|
@@ -124,3 +271,43 @@ without the generator).
|
|
|
124
271
|
**Not required.** The change touches no block/allow decision, no session
|
|
125
272
|
lifecycle, no coherence gate, and nothing named sentinel/guard/gate/watchdog. It
|
|
126
273
|
is a standalone document transform.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Addendum (2026-07-25 15:5xZ) — hierarchical capture for sub-headings
|
|
278
|
+
|
|
279
|
+
**What changed.** `splitStrictContract` reset `keeping` on *every* H1–H3, so an
|
|
280
|
+
allowlisted `## 3. Design` was closed by its own first child `### 3.1`. Capture
|
|
281
|
+
is now hierarchical: a NON-allowlisted heading DEEPER than the open section
|
|
282
|
+
rides along; a sibling or an allowlisted heading still closes it. `Design` was
|
|
283
|
+
also added to the heading allowlist (`## 3. Design` previously matched nothing).
|
|
284
|
+
|
|
285
|
+
**Why it matters.** Measured on the outbound-gate spec: the design section
|
|
286
|
+
shipped as **14 bytes of 86,314**. The section COUNT was correct, so the
|
|
287
|
+
contract looked complete. Reviews run against it produced four objections the
|
|
288
|
+
missing text already answered, and I relayed them to the operator as reasons to
|
|
289
|
+
withhold approval; all four were later withdrawn.
|
|
290
|
+
|
|
291
|
+
1. **Over-block** — none. The change only ADDS content that was being dropped.
|
|
292
|
+
Verified: outbound-gate +1212 lines / −1; inbound +2 / −0. No section lost.
|
|
293
|
+
2. **Under-block** — a spec whose normative heading is neither allowlisted nor a
|
|
294
|
+
child of an allowlisted section is still dropped. The existing ratio warning
|
|
295
|
+
plus the under-capture guard remain the detection for that.
|
|
296
|
+
3. **Level-of-abstraction fit** — right layer. The defect was in the capture
|
|
297
|
+
loop; nothing downstream could have recovered the missing bytes.
|
|
298
|
+
4. **Signal vs authority** — unchanged. The generator emits an artifact and
|
|
299
|
+
warnings; it blocks nothing.
|
|
300
|
+
5. **Interactions** — the pre-pass source-sizing loop is untouched, so the
|
|
301
|
+
under-capture guard still measures against the same baseline and correctly
|
|
302
|
+
went quiet for §3 once the capture was fixed.
|
|
303
|
+
6. **External surfaces** — regenerated contracts are committed alongside, so no
|
|
304
|
+
consumer sees a stale artifact.
|
|
305
|
+
7. **Multi-machine posture** — machine-local BY DESIGN: a build-time script over
|
|
306
|
+
files in the repo checkout. No runtime state, no replication surface.
|
|
307
|
+
8. **Rollback cost** — revert the commit and regenerate; contracts return to
|
|
308
|
+
their prior bytes. No migration, no persisted state.
|
|
309
|
+
|
|
310
|
+
**Tests.** `tests/unit/generate-spec-contract-nesting.test.ts` — the first
|
|
311
|
+
coverage this tool has had. Verified failing against the pre-fix code (2 of 3)
|
|
312
|
+
and passing after, so it is a real regression guard rather than a restatement
|
|
313
|
+
of current behaviour.
|