instar 1.3.196 → 1.3.198

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.196",
3
+ "version": "1.3.198",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * validate-retro-harvest.mjs — the schema validator for Apprenticeship Step 0
4
+ * retro-harvest artifacts (`apprenticeship-retro-harvest/v1`).
5
+ *
6
+ * Per APPRENTICESHIP-STEP0-RETRO-HARVEST-SPEC.md §9, this is the structural
7
+ * SIGNAL — not the authority. It checks the artifact's SHAPE deterministically;
8
+ * the LLM fidelity review (recorded in frontmatter) is the AUTHORITY for whether
9
+ * the harvest is faithful. (Signal vs Authority / The Body and the Mind.)
10
+ *
11
+ * Two layers:
12
+ * - validateRetroHarvest(text, opts) — PURE, offline, unit-tested. Structure,
13
+ * reconciliation, redaction backstop, path safety, completeness, scope rules.
14
+ * - checkLiveLedger(frontmatter, fetchImpl) — OPTIONAL live cross-check that
15
+ * seeded ids resolve in the running ledger. Network; run by the integration
16
+ * test / `--check-live`, never by the pure unit path.
17
+ *
18
+ * CLI: node scripts/validate-retro-harvest.mjs <artifact.md> [--prior] [--check-live]
19
+ * exit 0 = valid, exit 1 = invalid (errors to stderr).
20
+ */
21
+
22
+ import { readFileSync } from 'node:fs';
23
+ import { fileURLToPath } from 'node:url';
24
+ import path from 'node:path';
25
+ import yaml from 'js-yaml';
26
+
27
+ export const SCHEMA_ID = 'apprenticeship-retro-harvest/v1';
28
+ export const INSTANCE_TYPES = ['mentorship', 'apprenticeship'];
29
+ export const SCOPE_MODES = ['full', 'incremental'];
30
+ export const COMPLETENESS = ['complete', 'partial-accepted'];
31
+ export const FIDELITY_VERDICTS = ['faithful', 'partial', 'rejected'];
32
+ export const HARVEST_DIR = 'docs/apprenticeship/retro-harvests';
33
+
34
+ /** Scrubbers Step 0 trusts. A missing/unknown/failed scrub refuses the artifact (§7). */
35
+ export const APPROVED_SCRUBBERS = ['correction-scrub', 'instar-pii-scrub'];
36
+
37
+ /** Evidence-pointer URI schemes (§6). */
38
+ export const POINTER_PATTERNS = {
39
+ ledger: /^ledger:[0-9a-f-]{6,}$/i, // validator-resolvable
40
+ pr: /^pr:\d+$/, // validator-resolvable
41
+ thread: /^thread:\d+#[\w.-]+$/, // immutable msg locator; fidelity-review-only
42
+ memory: /^memory:[a-z0-9_-]+@[0-9a-f]{6,}#[\w.-]+$/i, // slug@hash#anchor; fidelity-review-only
43
+ };
44
+
45
+ /** Secret-shaped strings the backstop rejects (§7). A LIMITED backstop, not a scrub. */
46
+ const SECRET_PATTERNS = [
47
+ { name: 'bearer-token', re: /\bBearer\s+[A-Za-z0-9._-]{8,}/ },
48
+ { name: 'tunnel-sig', re: /[?&]sig=[0-9a-f]{16,}/i },
49
+ { name: 'long-hex-blob', re: /\b[0-9a-f]{40,}\b/i },
50
+ { name: 'long-base64-blob', re: /\b[A-Za-z0-9+/]{50,}={0,2}\b/ },
51
+ { name: 'email', re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/ },
52
+ ];
53
+
54
+ /**
55
+ * Split a markdown doc into { frontmatter (parsed), body (string) }.
56
+ * Throws if there is no `---`-delimited YAML frontmatter.
57
+ */
58
+ export function parseArtifact(text) {
59
+ const m = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(text);
60
+ if (!m) throw new Error('no YAML frontmatter (--- … ---) found');
61
+ const frontmatter = yaml.load(m[1]) || {};
62
+ return { frontmatter, body: m[2] || '' };
63
+ }
64
+
65
+ /** Count top-level `- ` bullet items directly under a `## <heading>` section. */
66
+ export function countSectionItems(body, heading) {
67
+ const lines = body.split('\n');
68
+ let inSection = false;
69
+ let count = 0;
70
+ const headingRe = new RegExp('^##\\s+' + heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
71
+ for (const line of lines) {
72
+ if (/^##\s+/.test(line)) inSection = headingRe.test(line);
73
+ else if (inSection && /^-\s+\S/.test(line)) count++;
74
+ }
75
+ return count;
76
+ }
77
+
78
+ /** True when the body contains a secret-shaped string (returns the matched pattern name or null). */
79
+ export function findSecret(body) {
80
+ for (const { name, re } of SECRET_PATTERNS) {
81
+ if (re.test(body)) return name;
82
+ }
83
+ return null;
84
+ }
85
+
86
+ /**
87
+ * Resolve + confine an artifact path to HARVEST_DIR. Returns the safe relative
88
+ * path or throws on traversal / bad component (§6 security).
89
+ */
90
+ export function safeArtifactPath(from, to, instanceType) {
91
+ for (const [k, v] of Object.entries({ from, to, instanceType })) {
92
+ if (typeof v !== 'string' || !/^[a-z0-9-]+$/.test(v)) {
93
+ throw new Error(`unsafe path component ${k}="${v}" (must match ^[a-z0-9-]+$)`);
94
+ }
95
+ }
96
+ const rel = `${HARVEST_DIR}/${from}-to-${to}-${instanceType}.md`;
97
+ const resolved = path.normalize(rel);
98
+ if (resolved.includes('..') || !resolved.startsWith(HARVEST_DIR + path.sep) && !resolved.startsWith(HARVEST_DIR + '/')) {
99
+ throw new Error(`path escapes ${HARVEST_DIR}: ${resolved}`);
100
+ }
101
+ return rel;
102
+ }
103
+
104
+ /**
105
+ * PURE structural validation of a retro-harvest artifact.
106
+ * @param {string} text - full artifact markdown
107
+ * @param {{ priorHarvestExists?: boolean }} [opts]
108
+ * @returns {{ valid: boolean, errors: string[] }}
109
+ */
110
+ export function validateRetroHarvest(text, opts = {}) {
111
+ const errors = [];
112
+ const priorHarvestExists = !!opts.priorHarvestExists;
113
+
114
+ let parsed;
115
+ try {
116
+ parsed = parseArtifact(text);
117
+ } catch (e) {
118
+ return { valid: false, errors: [`parse: ${e.message}`] };
119
+ }
120
+ const { frontmatter: fm, body } = parsed;
121
+
122
+ // --- schema id
123
+ if (fm.schema !== SCHEMA_ID) errors.push(`schema must be "${SCHEMA_ID}" (got "${fm.schema}")`);
124
+
125
+ // --- required scalar fields
126
+ for (const f of ['instanceType', 'from', 'to', 'framework', 'harvestedAt', 'scopeMode', 'completeness']) {
127
+ if (fm[f] == null || fm[f] === '') errors.push(`missing required field: ${f}`);
128
+ }
129
+ if (fm.instanceType && !INSTANCE_TYPES.includes(fm.instanceType))
130
+ errors.push(`instanceType must be one of ${INSTANCE_TYPES.join('|')}`);
131
+ if (fm.scopeMode && !SCOPE_MODES.includes(fm.scopeMode))
132
+ errors.push(`scopeMode must be one of ${SCOPE_MODES.join('|')}`);
133
+ if (fm.completeness && !COMPLETENESS.includes(fm.completeness))
134
+ errors.push(`completeness must be one of ${COMPLETENESS.join('|')}`);
135
+
136
+ // --- path safety on the identity components
137
+ if (fm.from && fm.to && fm.instanceType) {
138
+ try { safeArtifactPath(fm.from, fm.to, fm.instanceType); }
139
+ catch (e) { errors.push(`path: ${e.message}`); }
140
+ }
141
+
142
+ // --- scope rule: first harvest (no prior baseline) MUST be full (§8)
143
+ if (fm.scopeMode === 'incremental' && !priorHarvestExists)
144
+ errors.push('scopeMode "incremental" requires a prior harvest baseline; the first harvest must be "full"');
145
+
146
+ // --- sourcesCovered: coverage EXTENT, not bare booleans (§8)
147
+ const sc = fm.sourcesCovered;
148
+ if (!sc || typeof sc !== 'object') {
149
+ errors.push('missing sourcesCovered');
150
+ } else {
151
+ if (!sc.ledger || typeof sc.ledger.issueCount !== 'number')
152
+ errors.push('sourcesCovered.ledger must carry { read, issueCount }');
153
+ if (!sc.playbook || typeof sc.playbook.entryCount !== 'number')
154
+ errors.push('sourcesCovered.playbook must carry { read, entryCount }');
155
+ if (!Array.isArray(sc.threads))
156
+ errors.push('sourcesCovered.threads must be an array of { id, messagesRead, truncated }');
157
+ else for (const t of sc.threads) {
158
+ if (typeof t.messagesRead !== 'number' || typeof t.truncated !== 'boolean')
159
+ errors.push(`sourcesCovered.threads[${t && t.id}] needs numeric messagesRead + boolean truncated (coverage extent, not presence)`);
160
+ }
161
+ }
162
+
163
+ // --- completeness vs truncation (§8): complete ⇒ nothing truncated
164
+ if (fm.completeness === 'complete' && sc && Array.isArray(sc.threads)) {
165
+ if (sc.threads.some((t) => t.truncated === true))
166
+ errors.push('completeness "complete" is invalid when a source is truncated; use "partial-accepted" with named gaps');
167
+ }
168
+
169
+ // --- counts reconcile with body (§9)
170
+ const counts = fm.counts || {};
171
+ const bodyLessons = countSectionItems(body, 'Lessons');
172
+ const bodyMeta = countSectionItems(body, 'Meta-lessons');
173
+ const bodyProc = countSectionItems(body, 'Process-insights');
174
+ const bodyNeeds = countSectionItems(body, 'What the program needs');
175
+ if (counts.lessons !== bodyLessons)
176
+ errors.push(`counts.lessons=${counts.lessons} but body has ${bodyLessons} "## Lessons" items`);
177
+ if (counts.metaLessons !== bodyMeta)
178
+ errors.push(`counts.metaLessons=${counts.metaLessons} but body has ${bodyMeta} "## Meta-lessons" items`);
179
+ if (counts.processInsights !== bodyProc)
180
+ errors.push(`counts.processInsights=${counts.processInsights} but body has ${bodyProc} "## Process-insights" items`);
181
+ if (fm.programNeeds !== bodyNeeds)
182
+ errors.push(`programNeeds=${fm.programNeeds} but body has ${bodyNeeds} "## What the program needs" items`);
183
+
184
+ // --- redaction: approved scrubber, not failed (§7)
185
+ const r = fm.redaction;
186
+ if (!r || typeof r !== 'object') {
187
+ errors.push('missing redaction { scrubber, findingsRemoved, scrubbedAt }');
188
+ } else {
189
+ const scrubberName = String(r.scrubber || '').split('@')[0];
190
+ if (!APPROVED_SCRUBBERS.includes(scrubberName))
191
+ errors.push(`redaction.scrubber "${r.scrubber}" not in approved list (${APPROVED_SCRUBBERS.join(', ')})`);
192
+ if (r.status === 'failed') errors.push('redaction.status is "failed" — scrub must succeed before write');
193
+ if (typeof r.findingsRemoved !== 'number') errors.push('redaction.findingsRemoved must be a number');
194
+ }
195
+
196
+ // --- fidelity review: the authority's stamp (§9)
197
+ const fr = fm.fidelityReview;
198
+ if (!fr || typeof fr !== 'object') {
199
+ errors.push('missing fidelityReview { reviewer, verdict, at }');
200
+ } else {
201
+ if (!FIDELITY_VERDICTS.includes(fr.verdict))
202
+ errors.push(`fidelityReview.verdict must be one of ${FIDELITY_VERDICTS.join('|')}`);
203
+ if (fr.verdict === 'rejected') errors.push('fidelityReview.verdict is "rejected" — harvest is not faithful');
204
+ if (fr.verdict === 'partial' && !fr.gaps && !(fr.audit && fr.audit.gaps))
205
+ errors.push('fidelityReview "partial" must name the gaps (fidelityReview.gaps)');
206
+ if (!fr.reviewer) errors.push('fidelityReview.reviewer required (must be independent of the harvesting pass)');
207
+ }
208
+
209
+ // --- evidence pointers well-formed (§6). Strip trailing sentence punctuation
210
+ // so a pointer at a clause boundary ("ledger:abc123, priority…") still validates.
211
+ const pointerTokens = (body.match(/\b(?:ledger|pr|thread|memory):[^\s)\]]+/g) || [])
212
+ .map((t) => t.replace(/[.,;:]+$/, ''));
213
+ for (const tok of pointerTokens) {
214
+ const scheme = tok.split(':')[0];
215
+ const re = POINTER_PATTERNS[scheme];
216
+ if (re && !re.test(tok)) errors.push(`malformed evidence pointer: ${tok}`);
217
+ }
218
+
219
+ // --- secret backstop (§7)
220
+ const secret = findSecret(body);
221
+ if (secret) errors.push(`secret-shaped string in body (${secret}) — pointers-not-payloads (§7)`);
222
+
223
+ return { valid: errors.length === 0, errors };
224
+ }
225
+
226
+ /**
227
+ * OPTIONAL live cross-check: every seededToPlaybook id resolves at candidate+ in
228
+ * the running ledger. Network — never part of the pure unit path. Inject fetch.
229
+ * @returns {Promise<{ ok: boolean, errors: string[] }>}
230
+ */
231
+ export async function checkLiveLedger(frontmatter, fetchImpl) {
232
+ const errors = [];
233
+ const seeded = Array.isArray(frontmatter.seededToPlaybook) ? frontmatter.seededToPlaybook : [];
234
+ for (const s of seeded) {
235
+ const id = s && s.id;
236
+ if (!id) { errors.push('seededToPlaybook entry without id'); continue; }
237
+ try {
238
+ const res = await fetchImpl(id);
239
+ if (!res || !res.found) errors.push(`seeded id ${id} not found in live ledger`);
240
+ else if (!['candidate', 'extracted'].includes(res.playbookStatus))
241
+ errors.push(`seeded id ${id} is "${res.playbookStatus}", expected candidate+`);
242
+ } catch (e) {
243
+ errors.push(`live check for ${id} failed: ${e.message}`);
244
+ }
245
+ }
246
+ return { ok: errors.length === 0, errors };
247
+ }
248
+
249
+ // --- CLI entry (guarded so importing for tests is side-effect-free)
250
+ const isMain = (() => {
251
+ try { return process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]); }
252
+ catch { return false; }
253
+ })();
254
+
255
+ if (isMain) {
256
+ const args = process.argv.slice(2);
257
+ const file = args.find((a) => !a.startsWith('--'));
258
+ const priorHarvestExists = args.includes('--prior');
259
+ if (!file) {
260
+ console.error('usage: validate-retro-harvest.mjs <artifact.md> [--prior] [--check-live]');
261
+ process.exit(1);
262
+ }
263
+ const text = readFileSync(file, 'utf8');
264
+ const { valid, errors } = validateRetroHarvest(text, { priorHarvestExists });
265
+ if (!valid) {
266
+ console.error(`INVALID: ${file}`);
267
+ for (const e of errors) console.error(' - ' + e);
268
+ process.exit(1);
269
+ }
270
+ console.log(`valid: ${file}`);
271
+ process.exit(0);
272
+ }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-06-02T02:41:08.992Z",
5
- "instarVersion": "1.3.196",
4
+ "generatedAt": "2026-06-02T05:25:38.499Z",
5
+ "instarVersion": "1.3.198",
6
6
  "entryCount": 193,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -11,7 +11,7 @@
11
11
  "domain": "identity",
12
12
  "sourcePath": "src/core/PostUpdateMigrator.ts",
13
13
  "installedPath": ".instar/hooks/instar/session-start.sh",
14
- "contentHash": "1cd3546e774a6045c3b8c1481a1aa2acfbbb330232f7d3f1353d0f92aca6b1bc",
14
+ "contentHash": "600eb489f11a1b72daa4d5845e2cb0b3d24b08d868c7ebc65e090b028d833b67",
15
15
  "since": "2025-01-01"
16
16
  },
17
17
  "hook:dangerous-command-guard": {
@@ -20,7 +20,7 @@
20
20
  "domain": "safety",
21
21
  "sourcePath": "src/core/PostUpdateMigrator.ts",
22
22
  "installedPath": ".instar/hooks/instar/dangerous-command-guard.sh",
23
- "contentHash": "1cd3546e774a6045c3b8c1481a1aa2acfbbb330232f7d3f1353d0f92aca6b1bc",
23
+ "contentHash": "600eb489f11a1b72daa4d5845e2cb0b3d24b08d868c7ebc65e090b028d833b67",
24
24
  "since": "2025-01-01"
25
25
  },
26
26
  "hook:grounding-before-messaging": {
@@ -29,7 +29,7 @@
29
29
  "domain": "safety",
30
30
  "sourcePath": "src/core/PostUpdateMigrator.ts",
31
31
  "installedPath": ".instar/hooks/instar/grounding-before-messaging.sh",
32
- "contentHash": "1cd3546e774a6045c3b8c1481a1aa2acfbbb330232f7d3f1353d0f92aca6b1bc",
32
+ "contentHash": "600eb489f11a1b72daa4d5845e2cb0b3d24b08d868c7ebc65e090b028d833b67",
33
33
  "since": "2025-01-01"
34
34
  },
35
35
  "hook:compaction-recovery": {
@@ -38,7 +38,7 @@
38
38
  "domain": "identity",
39
39
  "sourcePath": "src/core/PostUpdateMigrator.ts",
40
40
  "installedPath": ".instar/hooks/instar/compaction-recovery.sh",
41
- "contentHash": "1cd3546e774a6045c3b8c1481a1aa2acfbbb330232f7d3f1353d0f92aca6b1bc",
41
+ "contentHash": "600eb489f11a1b72daa4d5845e2cb0b3d24b08d868c7ebc65e090b028d833b67",
42
42
  "since": "2025-01-01"
43
43
  },
44
44
  "hook:external-operation-gate": {
@@ -47,7 +47,7 @@
47
47
  "domain": "safety",
48
48
  "sourcePath": "src/core/PostUpdateMigrator.ts",
49
49
  "installedPath": ".instar/hooks/instar/external-operation-gate.js",
50
- "contentHash": "1cd3546e774a6045c3b8c1481a1aa2acfbbb330232f7d3f1353d0f92aca6b1bc",
50
+ "contentHash": "600eb489f11a1b72daa4d5845e2cb0b3d24b08d868c7ebc65e090b028d833b67",
51
51
  "since": "2025-01-01"
52
52
  },
53
53
  "hook:deferral-detector": {
@@ -56,7 +56,7 @@
56
56
  "domain": "safety",
57
57
  "sourcePath": "src/core/PostUpdateMigrator.ts",
58
58
  "installedPath": ".instar/hooks/instar/deferral-detector.js",
59
- "contentHash": "1cd3546e774a6045c3b8c1481a1aa2acfbbb330232f7d3f1353d0f92aca6b1bc",
59
+ "contentHash": "600eb489f11a1b72daa4d5845e2cb0b3d24b08d868c7ebc65e090b028d833b67",
60
60
  "since": "2025-01-01"
61
61
  },
62
62
  "hook:post-action-reflection": {
@@ -65,7 +65,7 @@
65
65
  "domain": "evolution",
66
66
  "sourcePath": "src/core/PostUpdateMigrator.ts",
67
67
  "installedPath": ".instar/hooks/instar/post-action-reflection.js",
68
- "contentHash": "1cd3546e774a6045c3b8c1481a1aa2acfbbb330232f7d3f1353d0f92aca6b1bc",
68
+ "contentHash": "600eb489f11a1b72daa4d5845e2cb0b3d24b08d868c7ebc65e090b028d833b67",
69
69
  "since": "2025-01-01"
70
70
  },
71
71
  "hook:external-communication-guard": {
@@ -74,7 +74,7 @@
74
74
  "domain": "safety",
75
75
  "sourcePath": "src/core/PostUpdateMigrator.ts",
76
76
  "installedPath": ".instar/hooks/instar/external-communication-guard.js",
77
- "contentHash": "1cd3546e774a6045c3b8c1481a1aa2acfbbb330232f7d3f1353d0f92aca6b1bc",
77
+ "contentHash": "600eb489f11a1b72daa4d5845e2cb0b3d24b08d868c7ebc65e090b028d833b67",
78
78
  "since": "2025-01-01"
79
79
  },
80
80
  "hook:scope-coherence-collector": {
@@ -83,7 +83,7 @@
83
83
  "domain": "coherence",
84
84
  "sourcePath": "src/core/PostUpdateMigrator.ts",
85
85
  "installedPath": ".instar/hooks/instar/scope-coherence-collector.js",
86
- "contentHash": "1cd3546e774a6045c3b8c1481a1aa2acfbbb330232f7d3f1353d0f92aca6b1bc",
86
+ "contentHash": "600eb489f11a1b72daa4d5845e2cb0b3d24b08d868c7ebc65e090b028d833b67",
87
87
  "since": "2025-01-01"
88
88
  },
89
89
  "hook:scope-coherence-checkpoint": {
@@ -92,7 +92,7 @@
92
92
  "domain": "coherence",
93
93
  "sourcePath": "src/core/PostUpdateMigrator.ts",
94
94
  "installedPath": ".instar/hooks/instar/scope-coherence-checkpoint.js",
95
- "contentHash": "1cd3546e774a6045c3b8c1481a1aa2acfbbb330232f7d3f1353d0f92aca6b1bc",
95
+ "contentHash": "600eb489f11a1b72daa4d5845e2cb0b3d24b08d868c7ebc65e090b028d833b67",
96
96
  "since": "2025-01-01"
97
97
  },
98
98
  "hook:free-text-guard": {
@@ -101,7 +101,7 @@
101
101
  "domain": "safety",
102
102
  "sourcePath": "src/core/PostUpdateMigrator.ts",
103
103
  "installedPath": ".instar/hooks/instar/free-text-guard.sh",
104
- "contentHash": "1cd3546e774a6045c3b8c1481a1aa2acfbbb330232f7d3f1353d0f92aca6b1bc",
104
+ "contentHash": "600eb489f11a1b72daa4d5845e2cb0b3d24b08d868c7ebc65e090b028d833b67",
105
105
  "since": "2025-01-01"
106
106
  },
107
107
  "hook:claim-intercept": {
@@ -110,7 +110,7 @@
110
110
  "domain": "coherence",
111
111
  "sourcePath": "src/core/PostUpdateMigrator.ts",
112
112
  "installedPath": ".instar/hooks/instar/claim-intercept.js",
113
- "contentHash": "1cd3546e774a6045c3b8c1481a1aa2acfbbb330232f7d3f1353d0f92aca6b1bc",
113
+ "contentHash": "600eb489f11a1b72daa4d5845e2cb0b3d24b08d868c7ebc65e090b028d833b67",
114
114
  "since": "2025-01-01"
115
115
  },
116
116
  "hook:claim-intercept-response": {
@@ -119,7 +119,7 @@
119
119
  "domain": "coherence",
120
120
  "sourcePath": "src/core/PostUpdateMigrator.ts",
121
121
  "installedPath": ".instar/hooks/instar/claim-intercept-response.js",
122
- "contentHash": "1cd3546e774a6045c3b8c1481a1aa2acfbbb330232f7d3f1353d0f92aca6b1bc",
122
+ "contentHash": "600eb489f11a1b72daa4d5845e2cb0b3d24b08d868c7ebc65e090b028d833b67",
123
123
  "since": "2025-01-01"
124
124
  },
125
125
  "hook:stop-gate-router": {
@@ -128,7 +128,7 @@
128
128
  "domain": "safety",
129
129
  "sourcePath": "src/core/PostUpdateMigrator.ts",
130
130
  "installedPath": ".instar/hooks/instar/stop-gate-router.js",
131
- "contentHash": "1cd3546e774a6045c3b8c1481a1aa2acfbbb330232f7d3f1353d0f92aca6b1bc",
131
+ "contentHash": "600eb489f11a1b72daa4d5845e2cb0b3d24b08d868c7ebc65e090b028d833b67",
132
132
  "since": "2025-01-01"
133
133
  },
134
134
  "hook:auto-approve-permissions": {
@@ -137,7 +137,7 @@
137
137
  "domain": "safety",
138
138
  "sourcePath": "src/core/PostUpdateMigrator.ts",
139
139
  "installedPath": ".instar/hooks/instar/auto-approve-permissions.js",
140
- "contentHash": "1cd3546e774a6045c3b8c1481a1aa2acfbbb330232f7d3f1353d0f92aca6b1bc",
140
+ "contentHash": "600eb489f11a1b72daa4d5845e2cb0b3d24b08d868c7ebc65e090b028d833b67",
141
141
  "since": "2025-01-01"
142
142
  },
143
143
  "job:health-check": {
@@ -1489,7 +1489,7 @@
1489
1489
  "type": "subsystem",
1490
1490
  "domain": "updates",
1491
1491
  "sourcePath": "src/core/PostUpdateMigrator.ts",
1492
- "contentHash": "1cd3546e774a6045c3b8c1481a1aa2acfbbb330232f7d3f1353d0f92aca6b1bc",
1492
+ "contentHash": "600eb489f11a1b72daa4d5845e2cb0b3d24b08d868c7ebc65e090b028d833b67",
1493
1493
  "since": "2025-01-01"
1494
1494
  },
1495
1495
  "subsystem:scheduler": {
@@ -0,0 +1,24 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Agents are no longer assigned a network port that the multi-machine mesh secretly can't reach.
9
+
10
+ The mesh talks between machines using Node's built-in web-request function, which refuses a short standard list of "bad ports" (for example 4045, the old file-lock service port). That list includes a port inside the default range agents were picked from — so an agent could be handed a port on which it would silently fail to pair or fail over, with no error in normal tools. The port picker now skips those bad ports, and any agent already running on one prints a clear startup warning telling you to change its port.
11
+
12
+ ## What to Tell Your User
13
+
14
+ Nothing to do for new agents — they'll automatically avoid the handful of reserved ports that break multi-machine networking. If you run multiple machines and one happens to be on a bad port, the agent now warns you at startup with the exact fix (change its port and restart). This removes a silent, very-hard-to-spot failure where an agent looked healthy but could never join the mesh.
15
+
16
+ ## Summary of New Capabilities
17
+
18
+ - The agent port allocator skips WHATWG fetch "bad ports" (4045, 6000, 6566, 6697, …), so a new agent never lands on a port the mesh's node-fetch comms can't reach.
19
+ - A new exported helper, isFetchBlockedPort, plus an unconditional server-startup warning when an existing agent's configured port is on that list (the migration path for already-running agents).
20
+
21
+ ## Evidence
22
+
23
+ - Root cause of a real 2026-06-02 failure: a paired test agent on port 4045 could never be joined ("fetch failed: bad port") even though curl reached its pairing endpoint; moving it off 4045 fixed the join immediately.
24
+ - `tsc --noEmit` clean; 34 agent-registry unit tests green, including 3 new ones covering the blocklist helper (both sides of the boundary) and the allocator skipping a blocked port. Side-effects review: upgrades/side-effects/fix-fetch-blocked-ports.md.
@@ -0,0 +1,24 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ An agent that loses its private code copy can now actually reinstall itself at boot.
9
+
10
+ When the boot wrapper detects a missing "shadow install" it runs `npm install` to self-heal. Some packages run native setup scripts that shell out to `node`/`npm` by name — and under macOS launchd the boot process often has a stripped `PATH` without them, so the reinstall died with "command not found" and the agent stayed stuck (looping on "Shadow install missing"). The reinstall now puts the resolved node directory on PATH and sets npm's scripts-prepend-node-path, so those setup scripts resolve node/npm and the self-heal completes. Existing agents get the fixed wrapper regenerated automatically on update.
11
+
12
+ ## What to Tell Your User
13
+
14
+ Nothing to do. This makes an agent's automatic self-repair actually work when its private code copy goes missing — the case where it most needs to recover on its own, unattended. A healthy agent is completely unaffected; this only changes the recovery path.
15
+
16
+ ## Summary of New Capabilities
17
+
18
+ - The boot-wrapper reinstall (`instar-boot.cjs` and `instar-boot.sh`) prepends the resolved node directory to PATH and sets `npm_config_scripts_prepend_node_path` so native postinstall scripts (e.g. sharp) resolve node/npm under a launchd-spawned boot child instead of failing with "command not found".
19
+ - The post-update migration regenerates older boot wrappers that predate this fix (it now requires the install-path marker to consider a wrapper current), so deployed agents receive the fix on their next update — not just new installs.
20
+
21
+ ## Evidence
22
+
23
+ - Root cause of a real 2026-06-02 failure: a paired test agent could not start; `instar-boot` looped on "Shadow install missing" and a manual reinstall failed on sharp's postinstall with `sh: node: command not found`; re-running with node on PATH fixed it immediately.
24
+ - `tsc --noEmit` clean. `PostUpdateMigrator-bootWrapperAbiCheck` extended to 8 green tests (3-marker idempotency, regen-when-install-path-marker-absent, generated `.cjs` passes `node --check` + carries the env, `.sh` carries the PATH export); 30 sibling boot-wrapper tests still green. Side-effects review: upgrades/side-effects/fix-bootwrapper-install-path.md.
@@ -0,0 +1,78 @@
1
+ # Side-Effects Review — Apprenticeship Step 0: Retro-Harvest
2
+
3
+ **Version / slug:** `APPRENTICESHIP-STEP0-RETRO-HARVEST-SPEC`
4
+ **Date:** `2026-06-01`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required` (converged via /spec-converge: 5 internal reviewers + codex gpt-5.5, 3 rounds)
7
+
8
+ ## Summary of the change
9
+
10
+ Builds Step 0 of the Apprenticeship Program (Tier-3 umbrella #675). Ships:
11
+ - `scripts/validate-retro-harvest.mjs` — the pure, offline schema validator (the structural
12
+ SIGNAL) for `apprenticeship-retro-harvest/v1` artifacts, plus an optional injected-fetch
13
+ live-ledger cross-check.
14
+ - `tests/unit/validate-retro-harvest.test.ts` (34 cases, both sides of every boundary) +
15
+ `tests/integration/retro-harvest-artifact.test.ts` (the real artifact as a fixture-of-record).
16
+ - `docs/apprenticeship/RETRO-HARVEST-PROCEDURE.md` — the repeatable how-to.
17
+ - `docs/apprenticeship/retro-harvests/echo-to-codey-mentorship.md` — the first real harvest,
18
+ grounded in the live ledger (28 issues), reviewed by an independent fidelity reviewer.
19
+ - `docs/apprenticeship/retro-harvests/INDEX.json` — the latest-harvest pointer.
20
+
21
+ No `src/` is touched. The validator is a standalone `scripts/` tool not imported by `src/`.
22
+
23
+ ## Decision-point inventory
24
+
25
+ One: the validator's pass/fail boundary (`validateRetroHarvest`). It is a **signal**, not an
26
+ authority (spec §9) — it asserts artifact SHAPE (schema, required fields, count/programNeeds
27
+ reconciliation, coverage-extent, scope/completeness rules, evidence-pointer well-formedness,
28
+ a limited secret-pattern backstop, path confinement, an approved+succeeded scrub, and a
29
+ non-`rejected` fidelity verdict). It does NOT judge harvest fidelity — that is the independent
30
+ LLM fidelity review, recorded in the artifact frontmatter.
31
+
32
+ ## 1. Over-block
33
+
34
+ **What legitimate inputs does this reject?** A structurally malformed artifact (missing fields,
35
+ counts that disagree with the body, an incremental first harvest, a `complete` artifact with a
36
+ truncated source, an unapproved/failed scrub, a `rejected` fidelity verdict, a secret-shaped
37
+ string, a malformed pointer). All are genuine defects the gate is meant to refuse. Trailing
38
+ sentence punctuation after a pointer is tolerated (stripped before validation) so a clause-
39
+ boundary pointer is not falsely rejected.
40
+
41
+ ## 2. Under-block
42
+
43
+ **What does this still miss?** By design, the validator does NOT judge whether the harvest is
44
+ *truthful or complete* — a well-shaped but hollow harvest passes the validator; the LLM fidelity
45
+ review is the authority that catches that (Signal vs Authority). The secret-pattern scan is an
46
+ explicit *limited backstop*, not a scrub guarantee (it misses API keys, hostnames, sig-less
47
+ URLs, names, phone numbers, contextual PII); the authoritative defense is the approved scrubber.
48
+ Live-ledger resolution of seeded ids runs only under `--check-live` / the integration path, not
49
+ the pure unit path.
50
+
51
+ ## 3. Level-of-abstraction fit
52
+
53
+ **Right layer?** Yes. The validator is a pure function with a thin CLI guard, in `scripts/`
54
+ beside the other repo-invariant validators, unit-testable with no git/fs/network mocking. The
55
+ harvest artifact + procedure + index are plain repo docs under `docs/apprenticeship/`. The gate
56
+ that will *require* a valid harvest before an instance starts is deliberately deferred to Step 1
57
+ (spec §12–13) — this step ships the artifact + the validator the gate will call.
58
+
59
+ ## 4. Testing
60
+
61
+ - **Unit** (`tests/unit/validate-retro-harvest.test.ts`): 34 cases — valid baseline + a targeted
62
+ failing mutation per decision boundary + `checkLiveLedger` with an injected fetch.
63
+ - **Integration** (`tests/integration/retro-harvest-artifact.test.ts`): the REAL
64
+ `echo-to-codey-mentorship.md` validates against the REAL validator; it is a full first harvest
65
+ with an independent (non-Echo) fidelity reviewer; seeds nothing (#50-independent); is INDEX-
66
+ registered.
67
+ - **E2E:** genuinely n/a — pure CLI validator, no HTTP route, no DI (spec §9, §11).
68
+
69
+ ## 5. Migration / rollback / Agent-Awareness
70
+
71
+ - **Migration Parity:** N/A. No agent-installed files (`.claude/`, `.instar/config.json`, hooks,
72
+ skills, CLAUDE.md template), no config defaults, no `src/`. Pure repo docs + one `scripts/`
73
+ helper + tests.
74
+ - **Rollback:** revert the commit; nothing is deployed to agents, no state is written.
75
+ - **Agent-Awareness:** explicitly deferred to Step 1 (spec §12) — the awareness surface ("a valid
76
+ harvest is required before an instance starts") belongs with the gate Step 1 wires, not before
77
+ it exists.
78
+ - **NEXT.md / publish:** no `src/` change → no fleet-release/publish concern.
@@ -0,0 +1,83 @@
1
+ # Side-Effects Review — Boot-wrapper reinstall: put node on PATH for native postinstalls
2
+
3
+ **Version / slug:** `fix-bootwrapper-install-path`
4
+ **Date:** `2026-06-02`
5
+ **Author:** `echo`
6
+ **Tier:** 1 (small, well-understood bugfix; ELI16 + side-effects, no converged spec) — but HIGH blast radius (boot shim), reviewed accordingly
7
+ **Parent principle:** Cross-Machine Coherence / Bootstrap Robustness — an agent must be able to self-heal its own install
8
+
9
+ ## Summary of the change
10
+
11
+ The boot wrapper (`instar-boot.cjs` / `instar-boot.sh`, generated by `installBootWrapper`
12
+ in `src/commands/setup.ts`) self-heals a missing shadow install by running `npm install`.
13
+ That `npm install` was spawned with **no `env` / no PATH augmentation**, so native
14
+ postinstall scripts (e.g. `sharp`: `sh -c "node install/check.js || npm run build"`) ran in
15
+ a subshell whose PATH — under a launchd-spawned boot child — has no `node`/`npm`, dying with
16
+ `command not found`. The reinstall then aborts and the shadow install never heals.
17
+
18
+ **Observed live (2026-06-02):** a throwaway mini agent could not start — `instar-boot`
19
+ looped on "Shadow install missing", and a manual reinstall reproduced it: it failed on
20
+ `sharp`'s postinstall (`sh: node: command not found`). Re-running with `node` on PATH
21
+ succeeded immediately.
22
+
23
+ ### What changed
24
+ - `src/commands/setup.ts` — `installBootWrapper`:
25
+ - **`.cjs` (jsWrapper):** the reinstall `execFileSync(nodeBin, [npmCli, 'install', …])` now
26
+ passes `env: installEnv` where `installEnv` prepends `path.dirname(nodeBin)` to `PATH` and
27
+ sets `npm_config_scripts_prepend_node_path: 'true'`.
28
+ - **`.sh` (bashWrapper):** the reinstall now runs with
29
+ `PATH="$(dirname "$NODE_BIN"):$PATH" npm_config_scripts_prepend_node_path=true` prefixed.
30
+ - `src/core/PostUpdateMigrator.ts` — `migrateBootWrapperAbiCheck`: the "already current" guard
31
+ now ALSO requires the `npm_config_scripts_prepend_node_path` marker, so existing agents whose
32
+ wrapper predates this fix are regenerated on update (Migration Parity).
33
+
34
+ ## Decision-point inventory
35
+ - reinstall env: augment vs inherit → augment (prepend node dir + npm flag). Inheriting the
36
+ launchd PATH is the bug.
37
+ - migration: add a 3rd required marker vs leave 2 → add the marker (otherwise deployed agents
38
+ with the 2 prior markers never regenerate and never get the fix).
39
+ - auto-re-port / auto-anything else: none — purely the reinstall env.
40
+
41
+ ## 1. Over-correction risk
42
+ The env only AUGMENTS PATH (prepends node dir; keeps the rest) and sets one npm flag. A healthy
43
+ agent never enters the reinstall branch at all (`if (!fs.existsSync(SHADOW))`), so the change is
44
+ inert unless the shadow install is actually missing. `npm_config_scripts_prepend_node_path` is
45
+ npm's own documented option for exactly this case.
46
+
47
+ ## 2. Under-correction risk
48
+ The migration regenerates only wrappers missing the new marker; agents already regenerated stay
49
+ put (idempotent). The `.sh` variant is for manual/Linux use; the launchd-critical path is `.cjs`.
50
+
51
+ ## 3. Level-of-abstraction fit
52
+ The fix lives in the single boot-wrapper generator (`installBootWrapper`) that both `init` and the
53
+ migration call — one source, both consumers. No scattering.
54
+
55
+ ## 4. Signal vs Authority
56
+ Pure deterministic env construction (no LLM/heuristic). Appropriate.
57
+
58
+ ## 5. External surfaces
59
+ None. No route/config-schema change. The generated wrapper gains an env on one spawn.
60
+
61
+ ## 6. Interactions with existing primitives
62
+ The reinstall's debounce marker, the ABI-aware node self-heal, and the version-managed-node
63
+ candidate discovery are all unchanged; this only adds an env to the npm spawn and a 3rd marker
64
+ to the migration's idempotency sniff (composes with the existing two).
65
+
66
+ ## 7. Rollback cost
67
+ Trivial: drop the `env`/PATH additions and the 3rd marker clause. No persistent state.
68
+
69
+ ## Migration parity
70
+ - New agents: `installBootWrapper` (via `init`) writes the fixed wrapper.
71
+ - Existing agents: `migrateBootWrapperAbiCheck` regenerates `instar-boot.cjs` because the new
72
+ `npm_config_scripts_prepend_node_path` marker is absent. Idempotent — re-running skips once the
73
+ marker is present.
74
+
75
+ ## Tests
76
+ `tsc --noEmit` clean. `PostUpdateMigrator-bootWrapperAbiCheck` extended to 8 tests (all green):
77
+ the "skip" idempotency case now requires all THREE markers; a new regen case for a wrapper with
78
+ the two prior markers but missing the install-path marker; a generated-content test asserting the
79
+ `.cjs` reinstall passes `env: installEnv` with `npm_config_scripts_prepend_node_path` + the PATH
80
+ prepend AND that the generated `.cjs` passes `node --check` (guards against template-escaping
81
+ bricking the boot shim); a `.sh` generated-content test. 30 sibling boot-wrapper tests
82
+ (`bootWrapperCjs`, `plist-coherence`, `lifeline-shadow-install-self-heal`, `init-join-launchd-handoff`)
83
+ all still green — no regressions.