instar 1.3.883 → 1.3.885

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.
Files changed (29) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +6 -0
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  5. package/dist/core/PostUpdateMigrator.js +12 -0
  6. package/dist/core/PostUpdateMigrator.js.map +1 -1
  7. package/dist/threadline/ThreadLog.d.ts +2 -0
  8. package/dist/threadline/ThreadLog.d.ts.map +1 -1
  9. package/dist/threadline/ThreadLog.js +2 -0
  10. package/dist/threadline/ThreadLog.js.map +1 -1
  11. package/dist/threadline/ThreadlineReapRecovery.d.ts +4 -0
  12. package/dist/threadline/ThreadlineReapRecovery.d.ts.map +1 -1
  13. package/dist/threadline/ThreadlineReapRecovery.js +58 -4
  14. package/dist/threadline/ThreadlineReapRecovery.js.map +1 -1
  15. package/dist/threadline/ThreadlineReplyValidation.d.ts.map +1 -1
  16. package/dist/threadline/ThreadlineReplyValidation.js.map +1 -1
  17. package/dist/threadline/recordThreadMessage.d.ts.map +1 -1
  18. package/dist/threadline/recordThreadMessage.js +1 -0
  19. package/dist/threadline/recordThreadMessage.js.map +1 -1
  20. package/package.json +2 -2
  21. package/scripts/lint-migration-consumer-completeness.js +301 -0
  22. package/src/data/builtin-manifest.json +20 -20
  23. package/src/templates/scripts/telegram-reply.sh +15 -0
  24. package/upgrades/1.3.884.md +24 -0
  25. package/upgrades/1.3.885.md +25 -0
  26. package/upgrades/migration-consumer-completeness.eli16.md +9 -0
  27. package/upgrades/side-effects/migration-consumer-completeness.md +82 -0
  28. package/upgrades/side-effects/telegram-reply-bounded-outcome.md +76 -0
  29. package/upgrades/telegram-reply-bounded-outcome.eli16.md +9 -0
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env node
2
+ // safe-git-allow: pre-commit bootstrap uses read-only diff/show before TypeScript is compiled.
3
+ /**
4
+ * Structural ratchet for the Migration-Consumer Completeness standard.
5
+ *
6
+ * The manifest names every enrolled canonical migration producer, every
7
+ * authorization/validation consumer of that authority, and the tests that
8
+ * validate the compatibility boundary. Role markers bind those declarations to
9
+ * actual files. Staged/CI diff mode treats any producer or consumer edit as a
10
+ * contract revision: the manifest revision must increase and every declared
11
+ * producer, consumer, and validator must acknowledge that revision in lockstep.
12
+ *
13
+ * This lint is deliberately mechanical. It proves that the declared dependency
14
+ * set moved in lockstep; review remains the semantic authority for deciding
15
+ * whether the declaration itself is complete.
16
+ */
17
+
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import { execFileSync } from 'node:child_process';
21
+ import { fileURLToPath } from 'node:url';
22
+
23
+ const SCRIPT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
24
+ const MANIFEST_PATH = 'docs/canonical-migration-contracts.json';
25
+ const MARKER_RE = /canonical-migration-(producer|consumer|validator):\s*([a-z0-9][a-z0-9-]*)@(\d+)/g;
26
+ const SCAN_ROOTS = ['src', 'tests', 'docs'];
27
+ const SCAN_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.mjs', '.cjs', '.md']);
28
+
29
+ /** @typedef {'producer'|'consumer'|'validator'} MigrationRole */
30
+ /**
31
+ * @typedef MigrationContract
32
+ * @property {string} id
33
+ * @property {number} revision
34
+ * @property {string[]} producers
35
+ * @property {string[]} consumers
36
+ * @property {string[]} validators
37
+ */
38
+ /** @typedef {{role: MigrationRole, id: string, revision: number, path: string}} MigrationMarker */
39
+ /** @typedef {{rule: string, message: string, id?: string, path?: string}} Finding */
40
+
41
+ const roleField = /** @type {const} */ ({
42
+ producer: 'producers',
43
+ consumer: 'consumers',
44
+ validator: 'validators',
45
+ });
46
+
47
+ /** @param {unknown} manifest @returns {Finding[]} */
48
+ export function validateMigrationManifest(manifest) {
49
+ if (!manifest || typeof manifest !== 'object') {
50
+ return [{ rule: 'MCC0-manifest-shape', message: 'manifest must be an object' }];
51
+ }
52
+ const candidate = /** @type {{schemaVersion?: unknown, contracts?: unknown}} */ (manifest);
53
+ if (candidate.schemaVersion !== 1) {
54
+ return [{ rule: 'MCC0-manifest-shape', message: 'schemaVersion must equal 1' }];
55
+ }
56
+ if (!Array.isArray(candidate.contracts) || candidate.contracts.length === 0) {
57
+ return [{ rule: 'MCC0-manifest-shape', message: 'contracts must be a non-empty array' }];
58
+ }
59
+ return [];
60
+ }
61
+
62
+ /**
63
+ * @param {{contracts: MigrationContract[], baseContracts?: MigrationContract[], markers: MigrationMarker[], changedFiles: Set<string>, pathExists: (path: string) => boolean}} input
64
+ * @returns {Finding[]}
65
+ */
66
+ export function auditMigrationConsumerCompleteness(input) {
67
+ const findings = [];
68
+ const contractsById = new Map();
69
+
70
+ for (const contract of input.contracts) {
71
+ if (!contract?.id || contractsById.has(contract.id)) {
72
+ findings.push({
73
+ rule: 'MCC0-invalid-contract-id',
74
+ id: contract?.id,
75
+ message: `contract id must be non-empty and unique: ${contract?.id ?? '(missing)'}`,
76
+ });
77
+ continue;
78
+ }
79
+ contractsById.set(contract.id, contract);
80
+ if (!Number.isInteger(contract.revision) || contract.revision < 1) {
81
+ findings.push({ rule: 'MCC0-invalid-revision', id: contract.id, message: `${contract.id}.revision must be a positive integer` });
82
+ }
83
+ for (const field of ['producers', 'consumers', 'validators']) {
84
+ if (!Array.isArray(contract[field])) {
85
+ findings.push({ rule: 'MCC0-invalid-contract-shape', id: contract.id, message: `${contract.id}.${field} must be an array` });
86
+ }
87
+ }
88
+ if (!Array.isArray(contract.producers) || contract.producers.length === 0) {
89
+ findings.push({ rule: 'MCC0-producer-required', id: contract.id, message: `${contract.id} must declare at least one producer` });
90
+ }
91
+ if (!Array.isArray(contract.consumers) || contract.consumers.length === 0) {
92
+ findings.push({ rule: 'MCC2-consumer-required', id: contract.id, message: `${contract.id} must declare at least one consumer` });
93
+ }
94
+ if (!Array.isArray(contract.validators) || contract.validators.length === 0) {
95
+ findings.push({ rule: 'MCC3-validator-required', id: contract.id, message: `${contract.id} must declare at least one validator` });
96
+ }
97
+ }
98
+
99
+ const markersByKey = new Map();
100
+ for (const marker of input.markers) {
101
+ const key = `${marker.id}\0${marker.role}\0${marker.path}`;
102
+ markersByKey.set(key, marker);
103
+ const contract = contractsById.get(marker.id);
104
+ if (!contract) {
105
+ findings.push({
106
+ rule: 'MCC1-unregistered-marker', id: marker.id, path: marker.path,
107
+ message: `${marker.path} declares ${marker.role} for unregistered migration ${marker.id}`,
108
+ });
109
+ continue;
110
+ }
111
+ const field = roleField[marker.role];
112
+ if (!contract[field]?.includes(marker.path)) {
113
+ findings.push({
114
+ rule: 'MCC4-marker-contract-mismatch', id: marker.id, path: marker.path,
115
+ message: `${marker.path} carries a ${marker.role} marker but is absent from ${marker.id}.${field}`,
116
+ });
117
+ }
118
+ if (marker.revision !== contract.revision) {
119
+ findings.push({
120
+ rule: 'MCC5-stale-role-revision', id: marker.id, path: marker.path,
121
+ message: `${marker.path} acknowledges ${marker.id}@${marker.revision}, expected @${contract.revision}`,
122
+ });
123
+ }
124
+ }
125
+
126
+ for (const contract of input.contracts) {
127
+ for (const role of /** @type {MigrationRole[]} */ (['producer', 'consumer', 'validator'])) {
128
+ const field = roleField[role];
129
+ for (const declaredPath of contract[field] ?? []) {
130
+ if (!input.pathExists(declaredPath)) {
131
+ findings.push({
132
+ rule: 'MCC4-declared-path-missing', id: contract.id, path: declaredPath,
133
+ message: `${contract.id}.${field} names missing path ${declaredPath}`,
134
+ });
135
+ }
136
+ const key = `${contract.id}\0${role}\0${declaredPath}`;
137
+ if (!markersByKey.has(key)) {
138
+ findings.push({
139
+ rule: 'MCC5-missing-role-marker', id: contract.id, path: declaredPath,
140
+ message: `${declaredPath} must carry canonical-migration-${role}: ${contract.id}@${contract.revision}`,
141
+ });
142
+ }
143
+ }
144
+ }
145
+
146
+ }
147
+
148
+ if (input.baseContracts) {
149
+ const baseById = new Map(input.baseContracts.map((contract) => [contract.id, contract]));
150
+ for (const base of input.baseContracts) {
151
+ if (!contractsById.has(base.id)) {
152
+ findings.push({
153
+ rule: 'MCC8-contract-removal-forbidden', id: base.id,
154
+ message: `${base.id} existed at the diff base and cannot be silently removed`,
155
+ });
156
+ }
157
+ const head = contractsById.get(base.id);
158
+ if (!head) continue;
159
+ for (const role of /** @type {MigrationRole[]} */ (['producer', 'consumer', 'validator'])) {
160
+ const field = roleField[role];
161
+ for (const basePath of base[field] ?? []) {
162
+ if (!(head[field] ?? []).includes(basePath)) {
163
+ findings.push({
164
+ rule: 'MCC8-role-removal-forbidden', id: base.id, path: basePath,
165
+ message: `${base.id}.${field} cannot silently remove ${basePath}; retire the contract in a separately reviewed migration`,
166
+ });
167
+ }
168
+ }
169
+ }
170
+ }
171
+ for (const contract of input.contracts) {
172
+ const base = baseById.get(contract.id);
173
+ const authorityChanged = [...(contract.producers ?? []), ...(contract.consumers ?? [])]
174
+ .some((file) => input.changedFiles.has(file));
175
+ const revisionChanged = !base || contract.revision !== base.revision;
176
+ if (base && authorityChanged && contract.revision <= base.revision) {
177
+ findings.push({
178
+ rule: 'MCC6-revision-bump-required', id: contract.id,
179
+ message: `${contract.id} producer/consumer surface changed without increasing revision above ${base.revision}`,
180
+ });
181
+ }
182
+ if (base && contract.revision < base.revision) {
183
+ findings.push({
184
+ rule: 'MCC6-revision-regression', id: contract.id,
185
+ message: `${contract.id} revision regressed from ${base.revision} to ${contract.revision}`,
186
+ });
187
+ }
188
+ if (!revisionChanged) continue;
189
+ for (const role of /** @type {MigrationRole[]} */ (['producer', 'consumer', 'validator'])) {
190
+ for (const declaredPath of contract[roleField[role]] ?? []) {
191
+ if (!input.changedFiles.has(declaredPath)) {
192
+ findings.push({
193
+ rule: role === 'validator' ? 'MCC7-lockstep-validator' : 'MCC6-lockstep-authority',
194
+ id: contract.id,
195
+ path: declaredPath,
196
+ message: `${contract.id}@${contract.revision} revision changed without lockstep ${role} acknowledgement in ${declaredPath}`,
197
+ });
198
+ }
199
+ }
200
+ }
201
+ }
202
+ }
203
+
204
+ return findings;
205
+ }
206
+
207
+ /** @param {string} root */
208
+ function scanMarkers(root) {
209
+ /** @type {MigrationMarker[]} */
210
+ const markers = [];
211
+ const visit = (relativeDir) => {
212
+ const absoluteDir = path.join(root, relativeDir);
213
+ if (!fs.existsSync(absoluteDir)) return;
214
+ for (const entry of fs.readdirSync(absoluteDir, { withFileTypes: true })) {
215
+ const relativePath = path.posix.join(relativeDir, entry.name);
216
+ if (entry.isDirectory()) {
217
+ if (entry.name !== 'node_modules' && !entry.name.startsWith('.')) visit(relativePath);
218
+ continue;
219
+ }
220
+ if (!SCAN_EXTENSIONS.has(path.extname(entry.name))) continue;
221
+ const text = fs.readFileSync(path.join(root, relativePath), 'utf8');
222
+ MARKER_RE.lastIndex = 0;
223
+ let match;
224
+ while ((match = MARKER_RE.exec(text)) !== null) {
225
+ markers.push({ role: /** @type {MigrationRole} */ (match[1]), id: match[2], revision: Number(match[3]), path: relativePath });
226
+ }
227
+ }
228
+ };
229
+ for (const scanRoot of SCAN_ROOTS) visit(scanRoot);
230
+ return markers;
231
+ }
232
+
233
+ /** @param {string} root @param {string[]} args */
234
+ function diffContext(root, args) {
235
+ let gitArgs = null;
236
+ let base = null;
237
+ const baseIndex = args.indexOf('--diff-base');
238
+ if (args.includes('--staged')) {
239
+ base = 'HEAD';
240
+ gitArgs = ['diff', '--cached', '--name-only', '--diff-filter=ACMR'];
241
+ } else if (baseIndex >= 0 && args[baseIndex + 1]) {
242
+ base = args[baseIndex + 1];
243
+ gitArgs = ['diff', '--name-only', '--diff-filter=ACMR', base, 'HEAD'];
244
+ }
245
+ if (!gitArgs) return { changedFiles: new Set(), baseContracts: undefined };
246
+ const output = execFileSync('git', gitArgs, { cwd: root, encoding: 'utf8' });
247
+ let baseContracts = [];
248
+ try {
249
+ const raw = execFileSync('git', ['show', `${base}:${MANIFEST_PATH}`], {
250
+ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
251
+ });
252
+ const parsed = JSON.parse(raw);
253
+ if (parsed?.schemaVersion === 1 && Array.isArray(parsed.contracts)) baseContracts = parsed.contracts;
254
+ } catch { /* first registry ship: no base manifest */ }
255
+ return {
256
+ changedFiles: new Set(output.split('\n').map((line) => line.trim()).filter(Boolean)),
257
+ baseContracts,
258
+ };
259
+ }
260
+
261
+ function main() {
262
+ const args = process.argv.slice(2);
263
+ const rootIndex = args.indexOf('--root');
264
+ const root = rootIndex >= 0 && args[rootIndex + 1] ? path.resolve(args[rootIndex + 1]) : SCRIPT_ROOT;
265
+ const manifestAbsolute = path.join(root, MANIFEST_PATH);
266
+ if (!fs.existsSync(manifestAbsolute)) {
267
+ console.error(`MCC0-manifest-missing: ${MANIFEST_PATH}`);
268
+ process.exit(1);
269
+ }
270
+ let manifest;
271
+ try {
272
+ manifest = JSON.parse(fs.readFileSync(manifestAbsolute, 'utf8'));
273
+ } catch (error) {
274
+ console.error(`MCC0-manifest-invalid: ${error instanceof Error ? error.message : String(error)}`);
275
+ process.exit(1);
276
+ }
277
+ const shapeFindings = validateMigrationManifest(manifest);
278
+ if (shapeFindings.length > 0) {
279
+ console.error(`${shapeFindings[0].rule}: ${shapeFindings[0].message}`);
280
+ process.exit(1);
281
+ }
282
+ const diff = diffContext(root, args);
283
+ const findings = auditMigrationConsumerCompleteness({
284
+ contracts: manifest.contracts,
285
+ baseContracts: diff.baseContracts,
286
+ markers: scanMarkers(root),
287
+ changedFiles: diff.changedFiles,
288
+ pathExists: (relativePath) => fs.existsSync(path.join(root, relativePath)),
289
+ });
290
+ if (findings.length === 0) {
291
+ console.log('lint-migration-consumer-completeness: clean');
292
+ return;
293
+ }
294
+ console.error(`lint-migration-consumer-completeness: ${findings.length} finding(s)`);
295
+ for (const finding of findings) {
296
+ console.error(` ${finding.rule}${finding.path ? ` ${finding.path}` : ''}: ${finding.message}`);
297
+ }
298
+ process.exit(1);
299
+ }
300
+
301
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main();
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-07-19T21:20:00.388Z",
5
- "instarVersion": "1.3.883",
4
+ "generatedAt": "2026-07-19T23:24:44.134Z",
5
+ "instarVersion": "1.3.885",
6
6
  "entryCount": 202,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -11,7 +11,7 @@
11
11
  "domain": "identity",
12
12
  "sourcePath": "src/core/PostUpdateMigrator.ts",
13
13
  "installedPath": ".instar/hooks/instar/session-start.sh",
14
- "contentHash": "642833df6bd9099d8b35dbc5da5d1d5d17f0c2a4cd93145693854dc028dbaec5",
14
+ "contentHash": "68624dacc4e2bb0f43676a5bd8509a53c1cf37aa09f80f678c66c37f8a2168e2",
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": "642833df6bd9099d8b35dbc5da5d1d5d17f0c2a4cd93145693854dc028dbaec5",
23
+ "contentHash": "68624dacc4e2bb0f43676a5bd8509a53c1cf37aa09f80f678c66c37f8a2168e2",
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": "642833df6bd9099d8b35dbc5da5d1d5d17f0c2a4cd93145693854dc028dbaec5",
32
+ "contentHash": "68624dacc4e2bb0f43676a5bd8509a53c1cf37aa09f80f678c66c37f8a2168e2",
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": "642833df6bd9099d8b35dbc5da5d1d5d17f0c2a4cd93145693854dc028dbaec5",
41
+ "contentHash": "68624dacc4e2bb0f43676a5bd8509a53c1cf37aa09f80f678c66c37f8a2168e2",
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": "642833df6bd9099d8b35dbc5da5d1d5d17f0c2a4cd93145693854dc028dbaec5",
50
+ "contentHash": "68624dacc4e2bb0f43676a5bd8509a53c1cf37aa09f80f678c66c37f8a2168e2",
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": "642833df6bd9099d8b35dbc5da5d1d5d17f0c2a4cd93145693854dc028dbaec5",
59
+ "contentHash": "68624dacc4e2bb0f43676a5bd8509a53c1cf37aa09f80f678c66c37f8a2168e2",
60
60
  "since": "2025-01-01"
61
61
  },
62
62
  "hook:self-stop-guard": {
@@ -65,7 +65,7 @@
65
65
  "domain": "coherence",
66
66
  "sourcePath": "src/core/PostUpdateMigrator.ts",
67
67
  "installedPath": ".instar/hooks/instar/self-stop-guard.js",
68
- "contentHash": "642833df6bd9099d8b35dbc5da5d1d5d17f0c2a4cd93145693854dc028dbaec5",
68
+ "contentHash": "68624dacc4e2bb0f43676a5bd8509a53c1cf37aa09f80f678c66c37f8a2168e2",
69
69
  "since": "2025-01-01"
70
70
  },
71
71
  "hook:post-action-reflection": {
@@ -74,7 +74,7 @@
74
74
  "domain": "evolution",
75
75
  "sourcePath": "src/core/PostUpdateMigrator.ts",
76
76
  "installedPath": ".instar/hooks/instar/post-action-reflection.js",
77
- "contentHash": "642833df6bd9099d8b35dbc5da5d1d5d17f0c2a4cd93145693854dc028dbaec5",
77
+ "contentHash": "68624dacc4e2bb0f43676a5bd8509a53c1cf37aa09f80f678c66c37f8a2168e2",
78
78
  "since": "2025-01-01"
79
79
  },
80
80
  "hook:external-communication-guard": {
@@ -83,7 +83,7 @@
83
83
  "domain": "safety",
84
84
  "sourcePath": "src/core/PostUpdateMigrator.ts",
85
85
  "installedPath": ".instar/hooks/instar/external-communication-guard.js",
86
- "contentHash": "642833df6bd9099d8b35dbc5da5d1d5d17f0c2a4cd93145693854dc028dbaec5",
86
+ "contentHash": "68624dacc4e2bb0f43676a5bd8509a53c1cf37aa09f80f678c66c37f8a2168e2",
87
87
  "since": "2025-01-01"
88
88
  },
89
89
  "hook:scope-coherence-collector": {
@@ -92,7 +92,7 @@
92
92
  "domain": "coherence",
93
93
  "sourcePath": "src/core/PostUpdateMigrator.ts",
94
94
  "installedPath": ".instar/hooks/instar/scope-coherence-collector.js",
95
- "contentHash": "642833df6bd9099d8b35dbc5da5d1d5d17f0c2a4cd93145693854dc028dbaec5",
95
+ "contentHash": "68624dacc4e2bb0f43676a5bd8509a53c1cf37aa09f80f678c66c37f8a2168e2",
96
96
  "since": "2025-01-01"
97
97
  },
98
98
  "hook:scope-coherence-checkpoint": {
@@ -101,7 +101,7 @@
101
101
  "domain": "coherence",
102
102
  "sourcePath": "src/core/PostUpdateMigrator.ts",
103
103
  "installedPath": ".instar/hooks/instar/scope-coherence-checkpoint.js",
104
- "contentHash": "642833df6bd9099d8b35dbc5da5d1d5d17f0c2a4cd93145693854dc028dbaec5",
104
+ "contentHash": "68624dacc4e2bb0f43676a5bd8509a53c1cf37aa09f80f678c66c37f8a2168e2",
105
105
  "since": "2025-01-01"
106
106
  },
107
107
  "hook:free-text-guard": {
@@ -110,7 +110,7 @@
110
110
  "domain": "safety",
111
111
  "sourcePath": "src/core/PostUpdateMigrator.ts",
112
112
  "installedPath": ".instar/hooks/instar/free-text-guard.sh",
113
- "contentHash": "642833df6bd9099d8b35dbc5da5d1d5d17f0c2a4cd93145693854dc028dbaec5",
113
+ "contentHash": "68624dacc4e2bb0f43676a5bd8509a53c1cf37aa09f80f678c66c37f8a2168e2",
114
114
  "since": "2025-01-01"
115
115
  },
116
116
  "hook:claim-intercept": {
@@ -119,7 +119,7 @@
119
119
  "domain": "coherence",
120
120
  "sourcePath": "src/core/PostUpdateMigrator.ts",
121
121
  "installedPath": ".instar/hooks/instar/claim-intercept.js",
122
- "contentHash": "642833df6bd9099d8b35dbc5da5d1d5d17f0c2a4cd93145693854dc028dbaec5",
122
+ "contentHash": "68624dacc4e2bb0f43676a5bd8509a53c1cf37aa09f80f678c66c37f8a2168e2",
123
123
  "since": "2025-01-01"
124
124
  },
125
125
  "hook:claim-intercept-response": {
@@ -128,7 +128,7 @@
128
128
  "domain": "coherence",
129
129
  "sourcePath": "src/core/PostUpdateMigrator.ts",
130
130
  "installedPath": ".instar/hooks/instar/claim-intercept-response.js",
131
- "contentHash": "642833df6bd9099d8b35dbc5da5d1d5d17f0c2a4cd93145693854dc028dbaec5",
131
+ "contentHash": "68624dacc4e2bb0f43676a5bd8509a53c1cf37aa09f80f678c66c37f8a2168e2",
132
132
  "since": "2025-01-01"
133
133
  },
134
134
  "hook:stop-gate-router": {
@@ -137,7 +137,7 @@
137
137
  "domain": "safety",
138
138
  "sourcePath": "src/core/PostUpdateMigrator.ts",
139
139
  "installedPath": ".instar/hooks/instar/stop-gate-router.js",
140
- "contentHash": "642833df6bd9099d8b35dbc5da5d1d5d17f0c2a4cd93145693854dc028dbaec5",
140
+ "contentHash": "68624dacc4e2bb0f43676a5bd8509a53c1cf37aa09f80f678c66c37f8a2168e2",
141
141
  "since": "2025-01-01"
142
142
  },
143
143
  "hook:auto-approve-permissions": {
@@ -146,7 +146,7 @@
146
146
  "domain": "safety",
147
147
  "sourcePath": "src/core/PostUpdateMigrator.ts",
148
148
  "installedPath": ".instar/hooks/instar/auto-approve-permissions.js",
149
- "contentHash": "642833df6bd9099d8b35dbc5da5d1d5d17f0c2a4cd93145693854dc028dbaec5",
149
+ "contentHash": "68624dacc4e2bb0f43676a5bd8509a53c1cf37aa09f80f678c66c37f8a2168e2",
150
150
  "since": "2025-01-01"
151
151
  },
152
152
  "job:health-check": {
@@ -1242,7 +1242,7 @@
1242
1242
  "type": "template",
1243
1243
  "domain": "operations",
1244
1244
  "sourcePath": "src/templates/scripts/telegram-reply.sh",
1245
- "contentHash": "d55feb9a203c7835c36b6bf0e23972c79a1e26fe6ea29683f31f831fb956c0f3",
1245
+ "contentHash": "1182b2c7e3779a9c37355e7317962ea48122a5f4a42425d7f3f9973e8127aa19",
1246
1246
  "since": "2025-01-01"
1247
1247
  },
1248
1248
  "template:whatsapp-reply.sh": {
@@ -1562,7 +1562,7 @@
1562
1562
  "type": "subsystem",
1563
1563
  "domain": "updates",
1564
1564
  "sourcePath": "src/core/PostUpdateMigrator.ts",
1565
- "contentHash": "642833df6bd9099d8b35dbc5da5d1d5d17f0c2a4cd93145693854dc028dbaec5",
1565
+ "contentHash": "68624dacc4e2bb0f43676a5bd8509a53c1cf37aa09f80f678c66c37f8a2168e2",
1566
1566
  "since": "2025-01-01"
1567
1567
  },
1568
1568
  "subsystem:scheduler": {
@@ -373,6 +373,8 @@ ATTEMPTED_AT=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)
373
373
  # from config — the server uses it to reject wrong-port requests before
374
374
  # evaluating the token.
375
375
  CURL_ARGS=(-s -w "\n%{http_code}" -X POST "http://localhost:${PORT}/telegram/reply/${TOPIC_ID}"
376
+ --connect-timeout 3
377
+ --max-time 125
376
378
  -H 'Content-Type: application/json'
377
379
  -d "$JSON_BODY")
378
380
  if [ -n "$AUTH_TOKEN" ]; then
@@ -386,6 +388,19 @@ if [ -n "$DELIVERY_ID" ]; then
386
388
  fi
387
389
 
388
390
  RESPONSE=$(curl "${CURL_ARGS[@]}")
391
+ CURL_STATUS=$?
392
+
393
+ # A transport failure after request start is inherently ambiguous: the server
394
+ # may still finish its tone review or Telegram send after our bounded client
395
+ # window closes. Always render a terminal outcome so a yielded/reattached tool
396
+ # call cannot complete silently, and never auto-enqueue/retry an unknown send.
397
+ if [ "$CURL_STATUS" -ne 0 ]; then
398
+ echo "AMBIGUOUS: Telegram relay transport ended without an HTTP outcome (curl ${CURL_STATUS})." >&2
399
+ echo " The message MAY still be delivered. Do NOT retry blindly; verify the conversation first." >&2
400
+ [ -n "$DELIVERY_ID" ] && echo " Delivery id: ${DELIVERY_ID}" >&2
401
+ echo "AMBIGUOUS: no HTTP outcome — verify delivery before retrying"
402
+ exit 0
403
+ fi
389
404
 
390
405
  HTTP_CODE=$(echo "$RESPONSE" | tail -1)
391
406
  BODY=$(echo "$RESPONSE" | sed '$d')
@@ -0,0 +1,24 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ - Telegram reply requests now have finite connection and total deadlines.
9
+ - Transport failures always return an explicit ambiguous outcome with a delivery id and safe retry guidance.
10
+ - Stock installed relay scripts upgrade in place; locally customized scripts remain protected.
11
+
12
+ ## What to Tell Your User
13
+
14
+ If a Telegram reply loses its connection before Instar can return a result, the agent now sees a clear uncertain-delivery warning instead of an empty tool outcome. It will verify before retrying rather than risk sending the message twice.
15
+
16
+ ## Summary of New Capabilities
17
+
18
+ - Bounded, observable Telegram reply outcomes even when the transport fails before an HTTP response.
19
+
20
+ ## Evidence
21
+
22
+ - Feedback: `fb-9c139a25-11e`
23
+ - Behavioral test: `tests/unit/telegram-reply-bounded-outcome.test.ts`
24
+ - Existing recovery/status regression suite remains green.
@@ -0,0 +1,25 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ - Added Migration-Consumer Completeness as a constitutional standard.
9
+ - Added a contract registry and lint that require canonical migration producers, consumers, and validators to move together.
10
+ - Enrolled Threadline's canonical inbound store and reply-validation boundary as the first protected contract.
11
+
12
+ ## What to Tell Your User
13
+
14
+ Instar's internal migrations now carry a stronger completion check. When one component becomes the new source of truth, the development process verifies that every declared authorization and validation consumer moves with it, reducing failures where half the system uses the new authority while another half silently uses the old one.
15
+
16
+ ## Summary of New Capabilities
17
+
18
+ - Canonical migrations have an explicit, reviewable producer/consumer/validator contract.
19
+ - Local commits and CI refuse producer-only migrations that leave declared consumers or validators behind.
20
+
21
+ ## Evidence
22
+
23
+ - `docs/STANDARDS-REGISTRY.md` — Migration-Consumer Completeness.
24
+ - `scripts/lint-migration-consumer-completeness.js`.
25
+ - `tests/unit/migration-consumer-completeness-lint.test.ts`.
@@ -0,0 +1,9 @@
1
+ # Migration-consumer completeness
2
+
3
+ A system migration is not finished merely because a new canonical store exists and its own tests pass. Other parts of the system may still consult the old store when they authorize a reply, validate a request, route work, or decide whether evidence is genuine. That creates two competing truths: the producer believes the new authority is canonical, while a consumer still behaves as if the retired authority is canonical. PR #1523 repaired one such Threadline instance, but it only named the general rule in prose; the repository had no standard or guard capable of catching the next occurrence.
4
+
5
+ This change makes that class explicit and enforceable. A new Migration-Consumer Completeness entry in the standards registry says that a canonical migration must enumerate its producers, every authorization and validation consumer, and the tests that prove the compatibility boundary. A machine-readable contract registry binds those roles to real repository paths. Each enrolled file carries a matching role marker, so a manifest cannot quietly claim coverage for a file that never acknowledged the migration.
6
+
7
+ The lint checks two layers. On every normal lint run it verifies that contracts are well formed, paths exist, role markers match, and every producer has at least one consumer and validator. During a staged commit and in the full pull-request diff, a producer or consumer edit must increment the contract revision, and every declared producer, consumer, and validator must acknowledge that revision in the same unit of work. This would have stopped the original ThreadLog migration from shipping while reply authorization and recovery still read only the legacy listener inbox.
8
+
9
+ The guard is deliberately honest about its boundary. It can prove that the declared set moved together, but a static parser cannot know whether a developer omitted an unknown consumer from the declaration. Independent review remains responsible for semantic completeness. The structural guard makes the dependency set explicit, reviewable, and mechanically synchronized instead of leaving it to memory.
@@ -0,0 +1,82 @@
1
+ # Side-Effects Review — Migration-consumer completeness
2
+
3
+ **Version / slug:** `migration-consumer-completeness`
4
+ **Date:** `2026-07-19`
5
+ **Author:** `Instar Agent (instar-codey)`
6
+ **Second-pass reviewer:** independent sub-agent review — concurred after two correction rounds
7
+
8
+ ## Summary of the change
9
+
10
+ This change registers Migration-Consumer Completeness in `docs/STANDARDS-REGISTRY.md`, adds the machine-readable contract registry at `docs/canonical-migration-contracts.json`, and adds `scripts/lint-migration-consumer-completeness.js` to normal lint, staged commit, and pull-request-diff gates. The existing Threadline canonical-store migration is enrolled as the first contract. It also closes two real compatibility consumers: reply authorization already accepts legacy-plus-ThreadLog evidence, and reap recovery now accepts the same union, resolving inline bodies directly and store-backed bodies only after MessageStore identity and content-digest verification.
11
+
12
+ ## Decision-point inventory
13
+
14
+ - Migration lockstep gate — added hard structural invariant — a changed canonical producer or consumer requires a revision bump acknowledged by every declared producer, consumer, and validator in the same diff.
15
+ - Contract completeness review — passed through — the lint reports declared structure; independent review remains semantic authority over whether the declaration includes every real consumer.
16
+ - Reap recovery authority — changed from legacy-inbox-only to the canonical legacy-plus-ThreadLog union; modern store references resolve through MessageStore and fail closed on absence, identity mismatch, or digest mismatch.
17
+
18
+ ## 1. Over-block
19
+
20
+ A producer or consumer file changed for a reason unrelated to its canonical authority still requires a contract revision and an explicit marker acknowledgement across the declared boundary. This is deliberate conservative friction at an authority boundary, but it makes the review event visible rather than pretending that a same-diff touch proves semantic behavior. The contract can be narrowed later by extracting the authority into smaller dedicated modules; there is no bypass flag.
21
+
22
+ ## 2. Under-block
23
+
24
+ The lint cannot infer an undeclared semantic consumer. A developer could omit a consumer from both the manifest and markers and still pass the mechanical gate. The registry and marker set make that omission visible to review, but semantic completeness remains an independent-review responsibility. Revision acknowledgement also does not prove that a changed validator meaningfully exercises the behavior; test review and CI remain responsible for that. Store-backed recovery intentionally remains unavailable when MessageStore evidence is missing, corrupt, identity-inconsistent, or digest-inconsistent; the resume item stays retryable rather than executing unverified content.
25
+
26
+ ## 3. Level-of-abstraction fit
27
+
28
+ The manifest is the right deterministic layer for dependency declaration, and the lint is the right layer for path, marker, and diff invariants. A static parser is not promoted into semantic authority: it never guesses which files are consumers. Review judges the declaration; the gate enforces the declared contract.
29
+
30
+ ## 4. Signal vs authority compliance
31
+
32
+ Per `docs/signal-vs-authority.md`, the hard block is a fully enumerable repository invariant: declared paths and markers either exist and move together or they do not. The non-enumerable question—whether the declaration covers every semantic consumer—remains with contextual review. No brittle keyword detector decides semantic completeness.
33
+
34
+ ## 4b. Judgment-point check
35
+
36
+ No static heuristic is added at a competing-signals decision point. Lockstep membership is an invariant over an explicit contract. Semantic consumer discovery remains a judgment candidate owned by review rather than this lint.
37
+
38
+ ## 5. Interactions
39
+
40
+ - **Shadowing:** normal `npm run lint` checks registry integrity before CI; the staged and PR-diff invocations add lockstep evidence. They are complementary rather than shadowing.
41
+ - **Double-fire:** a malformed registry may be reported by both local lint and the staged invocation. Both are read-only and deterministic; duplicate diagnostics have no side effect.
42
+ - **Races:** the lint reads immutable checkout/staging state and writes nothing. Recovery continues to use the existing durable reply-claim ledger; MessageStore resolution occurs before the claim, then the existing claim/transfer/release sequence prevents double execution.
43
+ - **Feedback loops:** none. It does not mutate contracts or source markers.
44
+
45
+ ## 6. External surfaces
46
+
47
+ Instar developers see actionable commit/CI failures when a canonical producer or consumer moves without its declared dependency set. At runtime, an interrupted modern Threadline inbound can now be redriven even when the legacy inbox has no copy. No new external service, credential, operator action, or URL is introduced; recovery reads only local canonical ThreadLog and MessageStore state.
48
+
49
+ ## 6b. Operator-surface quality
50
+
51
+ No operator surface — not applicable.
52
+
53
+ ## 7. Multi-machine posture
54
+
55
+ Replicated through git: the standard, manifest, markers, and lint are repository artifacts and therefore identical in every worktree and CI runner. Threadline evidence remains machine-local under the existing single-holder model; recovery reads the local ThreadLog and MessageStore associated with the interrupted worker and does not invent cross-machine lookup or replication. The change adds no user-facing notice or URL.
56
+
57
+ ## 8. Rollback cost
58
+
59
+ Revert the policy/guard and recovery union, then ship a patch. No data migration or agent-state repair is required because the runtime change is read-only over existing ThreadLog/MessageStore records and writes only through the pre-existing reply-claim and router paths. Rolling back restores legacy-only reap recovery and therefore reopens the modern-only stranded-work defect.
60
+
61
+ ## Conclusion
62
+
63
+ The original #1523 CLASS review named Migration-Consumer Completeness but did not add it to the constitution or enforce it structurally. This change closes that meta-gap with an explicit standard and a real lockstep guard, while preserving review as the semantic authority. Independent review is required because the change adds a blocking repository guard.
64
+
65
+ ## Second-pass review
66
+
67
+ **Reviewer:** `/root/migration_guard_review`
68
+ **Independent read:** Concurred. The first pass required enrollment of the real append/recovery consumers, revision-based acknowledgements, root/removal guards, and honest runtime documentation. The second pass found role-member removal and store-backed recovery bypasses. Final review verified both adversarial closures, MessageStore identity/digest binding, production wiring, retry posture, and the updated side-effects analysis; no merge blocker remains.
69
+
70
+ ## Evidence pointers
71
+
72
+ - `tests/unit/migration-consumer-completeness-lint.test.ts`
73
+ - `tests/unit/threadline/ThreadlineReplyValidation.test.ts`
74
+ - `tests/integration/threadline-relay-send-priority.test.ts`
75
+ - `tests/integration/threadline-reap-recovery-wiring.test.ts`
76
+ - `tests/integration/threadline/canonical-history-wiring.test.ts`
77
+ - `tests/e2e/threadline-reap-mid-processing.test.ts`
78
+ - PR #1523
79
+
80
+ ## Class-Closure Declaration (display-only mirror)
81
+
82
+ `defectClass: claim-vs-evidence`, `closure: guard`, `guardEvidence: { enforcementType: lint, citation: scripts/lint-migration-consumer-completeness.js#auditMigrationConsumerCompleteness, howCaught: a canonical migration producer cannot substantiate completeness without registered consumer and validator paths, matching role markers, and same-diff lockstep evidence }`.