let-them-talk 5.3.0 → 5.4.0

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 (166) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/README.md +158 -592
  3. package/SECURITY.md +3 -3
  4. package/USAGE.md +151 -0
  5. package/agent-contracts.js +447 -0
  6. package/api-agents.js +760 -0
  7. package/autonomy/decision-v2.js +380 -0
  8. package/autonomy/watchdog-policy.js +572 -0
  9. package/cli.js +454 -298
  10. package/conversation-templates/autonomous-feature.json +83 -22
  11. package/conversation-templates/code-review.json +69 -21
  12. package/conversation-templates/debug-squad.json +69 -21
  13. package/conversation-templates/feature-build.json +69 -21
  14. package/conversation-templates/research-write.json +69 -21
  15. package/dashboard.html +3148 -174
  16. package/dashboard.js +823 -786
  17. package/data-dir.js +58 -0
  18. package/docs/architecture/branch-semantics.md +157 -0
  19. package/docs/architecture/canonical-event-schema.md +88 -0
  20. package/docs/architecture/markdown-workspace.md +183 -0
  21. package/docs/architecture/runtime-contract.md +459 -0
  22. package/docs/architecture/runtime-migration-hardening.md +64 -0
  23. package/events/hooks.js +154 -0
  24. package/events/log.js +457 -0
  25. package/events/replay.js +33 -0
  26. package/events/schema.js +432 -0
  27. package/managed-team-integration.js +261 -0
  28. package/office/agents.js +704 -597
  29. package/office/animation.js +1 -1
  30. package/office/assets/arcade-cabinet.js +141 -0
  31. package/office/assets/archway.js +77 -0
  32. package/office/assets/bar-counter.js +91 -0
  33. package/office/assets/bar-stool.js +71 -0
  34. package/office/assets/beanbag.js +64 -0
  35. package/office/assets/bench.js +99 -0
  36. package/office/assets/bollard.js +87 -0
  37. package/office/assets/cactus.js +100 -0
  38. package/office/assets/carpet-tile.js +46 -0
  39. package/office/assets/chair.js +123 -0
  40. package/office/assets/chandelier.js +107 -0
  41. package/office/assets/coffee-machine.js +95 -0
  42. package/office/assets/coffee-table.js +81 -0
  43. package/office/assets/column.js +95 -0
  44. package/office/assets/desk-lamp.js +102 -0
  45. package/office/assets/desk.js +76 -0
  46. package/office/assets/dining-table.js +105 -0
  47. package/office/assets/door.js +70 -0
  48. package/office/assets/dual-monitor.js +72 -0
  49. package/office/assets/fence.js +76 -0
  50. package/office/assets/filing-cabinet.js +111 -0
  51. package/office/assets/floor-lamp.js +69 -0
  52. package/office/assets/floor-tile.js +54 -0
  53. package/office/assets/flower-pot.js +76 -0
  54. package/office/assets/foosball.js +95 -0
  55. package/office/assets/fridge.js +99 -0
  56. package/office/assets/gaming-chair.js +154 -0
  57. package/office/assets/gaming-desk.js +105 -0
  58. package/office/assets/glass-door.js +72 -0
  59. package/office/assets/glass-wall.js +64 -0
  60. package/office/assets/half-wall.js +49 -0
  61. package/office/assets/hanging-plant.js +112 -0
  62. package/office/assets/index.js +151 -0
  63. package/office/assets/indoor-tree.js +90 -0
  64. package/office/assets/l-sofa.js +153 -0
  65. package/office/assets/marble-floor.js +64 -0
  66. package/office/assets/materials.js +40 -0
  67. package/office/assets/meeting-table.js +88 -0
  68. package/office/assets/microwave.js +94 -0
  69. package/office/assets/monitor.js +67 -0
  70. package/office/assets/neon-strip.js +73 -0
  71. package/office/assets/painting.js +84 -0
  72. package/office/assets/palm-tree.js +108 -0
  73. package/office/assets/pc-tower.js +91 -0
  74. package/office/assets/pendant-light.js +67 -0
  75. package/office/assets/ping-pong.js +114 -0
  76. package/office/assets/plant.js +72 -0
  77. package/office/assets/planter-box.js +95 -0
  78. package/office/assets/pool-table.js +94 -0
  79. package/office/assets/printer.js +113 -0
  80. package/office/assets/reception-desk.js +133 -0
  81. package/office/assets/rug.js +78 -0
  82. package/office/assets/sculpture.js +85 -0
  83. package/office/assets/server-rack.js +98 -0
  84. package/office/assets/sink.js +109 -0
  85. package/office/assets/sofa.js +106 -0
  86. package/office/assets/speaker.js +83 -0
  87. package/office/assets/spotlight.js +83 -0
  88. package/office/assets/street-lamp.js +97 -0
  89. package/office/assets/trash-can.js +83 -0
  90. package/office/assets/treadmill.js +126 -0
  91. package/office/assets/trophy.js +89 -0
  92. package/office/assets/tv-screen.js +79 -0
  93. package/office/assets/vase.js +84 -0
  94. package/office/assets/wall-clock.js +84 -0
  95. package/office/assets/wall.js +53 -0
  96. package/office/assets/water-cooler.js +146 -0
  97. package/office/assets/whiteboard.js +115 -0
  98. package/office/assets.js +3 -431
  99. package/office/builder.js +791 -355
  100. package/office/campus-env.js +1012 -1119
  101. package/office/environment.js +2 -0
  102. package/office/gallery.js +997 -0
  103. package/office/index.js +165 -61
  104. package/office/navigation.js +173 -152
  105. package/office/player.js +178 -68
  106. package/office/robot-character.js +272 -0
  107. package/office/spectator-camera.js +33 -10
  108. package/office/state.js +2 -0
  109. package/office/world-save.js +35 -4
  110. package/package.json +57 -3
  111. package/providers/comfyui.js +383 -0
  112. package/providers/dalle.js +79 -0
  113. package/providers/gemini.js +181 -0
  114. package/providers/ollama.js +184 -0
  115. package/providers/replicate.js +115 -0
  116. package/providers/zai.js +183 -0
  117. package/runtime-descriptor.js +270 -0
  118. package/scripts/check-agent-contract-advisory.js +132 -0
  119. package/scripts/check-api-agent-parity.js +277 -0
  120. package/scripts/check-autonomy-v2-decision.js +207 -0
  121. package/scripts/check-autonomy-v2-execution.js +588 -0
  122. package/scripts/check-autonomy-v2-watchdog.js +224 -0
  123. package/scripts/check-branch-fork-snapshot.js +337 -0
  124. package/scripts/check-branch-isolation.js +787 -0
  125. package/scripts/check-branch-semantics.js +139 -0
  126. package/scripts/check-dashboard-control-plane.js +1304 -0
  127. package/scripts/check-docs-onboarding.js +490 -0
  128. package/scripts/check-event-schema.js +276 -0
  129. package/scripts/check-evidence-completion.js +239 -0
  130. package/scripts/check-invariants.js +992 -0
  131. package/scripts/check-lifecycle-hooks.js +525 -0
  132. package/scripts/check-managed-team-integration.js +166 -0
  133. package/scripts/check-markdown-workspace-export.js +548 -0
  134. package/scripts/check-markdown-workspace-safety.js +347 -0
  135. package/scripts/check-markdown-workspace.js +136 -0
  136. package/scripts/check-message-replay.js +429 -0
  137. package/scripts/check-migration-hardening.js +300 -0
  138. package/scripts/check-performance-indexing.js +272 -0
  139. package/scripts/check-provider-capabilities.js +316 -0
  140. package/scripts/check-runtime-contract.js +109 -0
  141. package/scripts/check-session-aware-context.js +172 -0
  142. package/scripts/check-session-lifecycle.js +210 -0
  143. package/scripts/export-markdown-workspace.js +84 -0
  144. package/scripts/fixtures/message-replay/clean.jsonl +2 -0
  145. package/scripts/fixtures/message-replay/corrupt-correction-payload.jsonl +1 -0
  146. package/scripts/fixtures/message-replay/corrupt-jsonl.jsonl +1 -0
  147. package/scripts/fixtures/message-replay/corrupt-payload.jsonl +1 -0
  148. package/scripts/fixtures/message-replay/out-of-order.jsonl +2 -0
  149. package/scripts/migrate-legacy-to-canonical.js +201 -0
  150. package/scripts/run-verification-suite.js +242 -0
  151. package/scripts/sync-packaged-docs.js +69 -0
  152. package/server.js +9546 -7216
  153. package/state/agents.js +161 -0
  154. package/state/canonical.js +3068 -0
  155. package/state/dashboard-queries.js +441 -0
  156. package/state/evidence.js +56 -0
  157. package/state/io.js +69 -0
  158. package/state/markdown-workspace.js +951 -0
  159. package/state/messages.js +669 -0
  160. package/state/sessions.js +683 -0
  161. package/state/tasks-workflows.js +92 -0
  162. package/templates/debate.json +2 -2
  163. package/templates/managed.json +4 -4
  164. package/templates/pair.json +2 -2
  165. package/templates/review.json +2 -2
  166. package/templates/team.json +3 -3
@@ -0,0 +1,429 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const {
8
+ CANONICAL_REPLAY_ERROR_CODES,
9
+ isCanonicalReplayError,
10
+ } = require(path.resolve(__dirname, '..', 'events', 'replay.js'));
11
+ const { createCanonicalEventLog } = require(path.resolve(__dirname, '..', 'events', 'log.js'));
12
+ const { createCanonicalState } = require(path.resolve(__dirname, '..', 'state', 'canonical.js'));
13
+
14
+ const FIXTURE_ROOT = path.resolve(__dirname, 'fixtures', 'message-replay');
15
+ const FIXTURE_DISPLAY_ROOT = 'agent-bridge/scripts/fixtures/message-replay';
16
+ const USAGE = 'Usage: node agent-bridge/scripts/check-message-replay.js [--scenario healthy|clean|corrupt-jsonl|corrupt-payload|corrupt-correction-payload|out-of-order]';
17
+
18
+ const FAILURE_SCENARIOS = Object.freeze({
19
+ 'corrupt-jsonl': Object.freeze({
20
+ fixtureName: 'corrupt-jsonl',
21
+ expectedCode: CANONICAL_REPLAY_ERROR_CODES.INVALID_JSONL,
22
+ expectedMessageFragments: ['invalid JSONL'],
23
+ }),
24
+ 'corrupt-payload': Object.freeze({
25
+ fixtureName: 'corrupt-payload',
26
+ expectedCode: CANONICAL_REPLAY_ERROR_CODES.INVALID_EVENT,
27
+ expectedMessageFragments: ['payload.message to be an object'],
28
+ }),
29
+ 'corrupt-correction-payload': Object.freeze({
30
+ fixtureName: 'corrupt-correction-payload',
31
+ expectedCode: CANONICAL_REPLAY_ERROR_CODES.INVALID_EVENT,
32
+ expectedMessageFragments: ['message.corrected events require payload.content to be a string'],
33
+ }),
34
+ 'out-of-order': Object.freeze({
35
+ fixtureName: 'out-of-order',
36
+ expectedCode: CANONICAL_REPLAY_ERROR_CODES.INVALID_SEQUENCE,
37
+ expectedMessageFragments: ['strictly increasing seq values'],
38
+ }),
39
+ });
40
+
41
+ function fail(lines, exitCode = 1) {
42
+ fs.writeSync(2, lines.join('\n') + '\n');
43
+ process.exit(exitCode);
44
+ }
45
+
46
+ function parseArgs(argv) {
47
+ if (argv.length === 0) {
48
+ return { scenario: 'healthy' };
49
+ }
50
+
51
+ if (argv.length === 2 && argv[0] === '--scenario') {
52
+ const scenario = argv[1];
53
+ const supportedScenarios = ['healthy', 'clean', ...Object.keys(FAILURE_SCENARIOS)];
54
+ if (!supportedScenarios.includes(scenario)) {
55
+ fail([
56
+ `Unknown scenario: ${scenario}`,
57
+ `Supported scenarios: ${supportedScenarios.join(', ')}`,
58
+ USAGE,
59
+ ], 2);
60
+ }
61
+
62
+ return { scenario };
63
+ }
64
+
65
+ fail([USAGE], 2);
66
+ }
67
+
68
+ function readFileText(filePath) {
69
+ if (!fs.existsSync(filePath)) return '';
70
+ return fs.readFileSync(filePath, 'utf8');
71
+ }
72
+
73
+ function deleteFile(filePath) {
74
+ if (fs.existsSync(filePath)) {
75
+ fs.unlinkSync(filePath);
76
+ }
77
+ }
78
+
79
+ function expectEqual(problems, label, actual, expected) {
80
+ if (actual !== expected) {
81
+ problems.push(`${label} did not match expected content.`);
82
+ }
83
+ }
84
+
85
+ function expect(problems, condition, message) {
86
+ if (!condition) {
87
+ problems.push(message);
88
+ }
89
+ }
90
+
91
+ function toJsonl(messages) {
92
+ return messages.map((message) => JSON.stringify(message)).join('\n') + (messages.length ? '\n' : '');
93
+ }
94
+
95
+ function writeJson(filePath, value) {
96
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
97
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
98
+ }
99
+
100
+ function getFixtureSpec(fixtureName) {
101
+ return {
102
+ path: path.join(FIXTURE_ROOT, `${fixtureName}.jsonl`),
103
+ display: `${FIXTURE_DISPLAY_ROOT}/${fixtureName}.jsonl`,
104
+ };
105
+ }
106
+
107
+ function ensureFixtureExists(fixtureSpec) {
108
+ if (!fs.existsSync(fixtureSpec.path)) {
109
+ fail([
110
+ 'Canonical message replay validation failed.',
111
+ `Missing fixture: ${fixtureSpec.display}`,
112
+ ]);
113
+ }
114
+ }
115
+
116
+ function parseFixtureObjects(fixtureSpec) {
117
+ ensureFixtureExists(fixtureSpec);
118
+ const raw = fs.readFileSync(fixtureSpec.path, 'utf8');
119
+ if (!raw.trim()) return [];
120
+
121
+ return raw
122
+ .split(/\r?\n/)
123
+ .filter(Boolean)
124
+ .map((line, index) => {
125
+ try {
126
+ return JSON.parse(line);
127
+ } catch (error) {
128
+ throw new Error(`Fixture ${fixtureSpec.display} contains invalid JSONL at line ${index + 1}: ${error.message}`);
129
+ }
130
+ });
131
+ }
132
+
133
+ function stageFixtureAsBranchEventLog(dataDir, branchName, fixtureSpec) {
134
+ ensureFixtureExists(fixtureSpec);
135
+ const branchEventsFile = path.join(dataDir, 'runtime', 'branches', branchName, 'events.jsonl');
136
+ fs.mkdirSync(path.dirname(branchEventsFile), { recursive: true });
137
+ fs.writeFileSync(branchEventsFile, fs.readFileSync(fixtureSpec.path, 'utf8'));
138
+ return branchEventsFile;
139
+ }
140
+
141
+ function buildReplayErrorLines(scenario, fixtureSpec, error) {
142
+ return [
143
+ 'Canonical message replay rejected fixture.',
144
+ `Scenario: ${scenario}`,
145
+ `Fixture: ${fixtureSpec.display}`,
146
+ `Replay error code: ${isCanonicalReplayError(error) ? error.code : 'canonical_replay.unclassified'}`,
147
+ `Replay error message: ${error && error.message ? error.message : String(error)}`,
148
+ ];
149
+ }
150
+
151
+ function runHealthyScenario() {
152
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'letthemtalk-task3b-'));
153
+ const dataDir = path.join(tempRoot, '.agent-bridge');
154
+ const branchName = 'feature_task3b';
155
+ const canonicalState = createCanonicalState({ dataDir, processPid: 4242 });
156
+ const eventLog = createCanonicalEventLog({ dataDir });
157
+ const problems = [];
158
+
159
+ const mainMessages = [
160
+ {
161
+ id: 'msg-task3b-main-1',
162
+ from: 'alpha',
163
+ to: 'beta',
164
+ content: 'Main branch hello',
165
+ timestamp: '2026-04-15T19:00:00.000Z',
166
+ reply_to: null,
167
+ system: false,
168
+ },
169
+ {
170
+ id: 'msg-task3b-main-2',
171
+ from: 'beta',
172
+ to: 'alpha',
173
+ content: 'Main branch reply',
174
+ timestamp: '2026-04-15T19:00:05.000Z',
175
+ reply_to: 'msg-task3b-main-1',
176
+ system: false,
177
+ },
178
+ ];
179
+
180
+ const featureMessages = [
181
+ {
182
+ id: 'msg-task3b-feature-1',
183
+ from: 'gamma',
184
+ to: 'delta',
185
+ content: 'Feature branch hello',
186
+ timestamp: '2026-04-15T19:01:00.000Z',
187
+ reply_to: null,
188
+ system: false,
189
+ },
190
+ ];
191
+
192
+ const mainChannelMessages = [
193
+ {
194
+ id: 'msg-task3b-main-ops-1',
195
+ from: 'alpha',
196
+ to: 'beta',
197
+ channel: 'ops',
198
+ content: 'Main branch ops message',
199
+ timestamp: '2026-04-15T19:00:03.000Z',
200
+ reply_to: null,
201
+ system: false,
202
+ },
203
+ ];
204
+
205
+ const featureChannelMessages = [
206
+ {
207
+ id: 'msg-task3b-feature-lab-1',
208
+ from: 'gamma',
209
+ to: 'delta',
210
+ channel: 'lab',
211
+ content: 'Feature branch lab message',
212
+ timestamp: '2026-04-15T19:01:03.000Z',
213
+ reply_to: null,
214
+ system: false,
215
+ },
216
+ ];
217
+
218
+ try {
219
+ for (const message of mainMessages) {
220
+ canonicalState.appendMessage(message);
221
+ }
222
+ for (const message of mainChannelMessages) {
223
+ canonicalState.appendScopedMessage(message, { branch: 'main', channel: 'ops' });
224
+ }
225
+
226
+ for (const message of featureMessages) {
227
+ canonicalState.appendMessage(message, { branch: branchName });
228
+ }
229
+ for (const message of featureChannelMessages) {
230
+ canonicalState.appendScopedMessage(message, { branch: branchName, channel: 'lab' });
231
+ }
232
+
233
+ writeJson(path.join(dataDir, 'channels.json'), {
234
+ general: { description: 'General channel' },
235
+ ops: { description: 'Ops channel' },
236
+ });
237
+ writeJson(path.join(dataDir, `branch-${branchName}-channels.json`), {
238
+ general: { description: 'General channel' },
239
+ lab: { description: 'Lab channel' },
240
+ });
241
+
242
+ const mainEvents = eventLog.readBranchEvents('main');
243
+ const featureEvents = eventLog.readBranchEvents(branchName);
244
+ const mainEventFile = eventLog.getBranchEventsFile('main');
245
+ const featureEventFile = eventLog.getBranchEventsFile(branchName);
246
+
247
+ expect(problems, fs.existsSync(mainEventFile), `Missing canonical event log for main branch: ${mainEventFile}`);
248
+ expect(problems, fs.existsSync(featureEventFile), `Missing canonical event log for ${branchName}: ${featureEventFile}`);
249
+ expect(problems, mainEvents.length === mainMessages.length + mainChannelMessages.length, 'Main branch canonical event count was incorrect.');
250
+ expect(problems, featureEvents.length === featureMessages.length + featureChannelMessages.length, `${branchName} canonical event count was incorrect.`);
251
+ expect(problems, mainEvents.every((event) => event.type === 'message.sent' && event.stream === 'branch' && event.branch_id === 'main'), 'Main branch events were not persisted as branch-local message.sent events.');
252
+ expect(problems, featureEvents.every((event) => event.type === 'message.sent' && event.stream === 'branch' && event.branch_id === branchName), `${branchName} events were not persisted as branch-local message.sent events.`);
253
+ expect(problems, mainEvents.map((event) => event.seq).join(',') === '1,2,3', 'Main branch canonical event sequence should be 1,2,3.');
254
+ expect(problems, featureEvents.map((event) => event.seq).join(',') === '1,2', `${branchName} canonical event sequence should reset to 1,2.`);
255
+
256
+ const mainMessagesFile = path.join(dataDir, 'messages.jsonl');
257
+ const mainHistoryFile = path.join(dataDir, 'history.jsonl');
258
+ const mainChannelMessagesFile = path.join(dataDir, 'channel-ops-messages.jsonl');
259
+ const mainChannelHistoryFile = path.join(dataDir, 'channel-ops-history.jsonl');
260
+ const featureMessagesFile = path.join(dataDir, `branch-${branchName}-messages.jsonl`);
261
+ const featureHistoryFile = path.join(dataDir, `branch-${branchName}-history.jsonl`);
262
+ const featureChannelMessagesFile = path.join(dataDir, `branch-${branchName}-channel-lab-messages.jsonl`);
263
+ const featureChannelHistoryFile = path.join(dataDir, `branch-${branchName}-channel-lab-history.jsonl`);
264
+
265
+ deleteFile(mainMessagesFile);
266
+ deleteFile(mainHistoryFile);
267
+ deleteFile(mainChannelMessagesFile);
268
+ deleteFile(mainChannelHistoryFile);
269
+ deleteFile(featureMessagesFile);
270
+ deleteFile(featureHistoryFile);
271
+ deleteFile(featureChannelMessagesFile);
272
+ deleteFile(featureChannelHistoryFile);
273
+
274
+ canonicalState.rebuildMessageProjections();
275
+ canonicalState.rebuildMessageProjections({ branch: branchName });
276
+
277
+ const expectedMainJsonl = toJsonl(mainMessages);
278
+ const expectedMainChannelJsonl = toJsonl(mainChannelMessages);
279
+ const expectedFeatureJsonl = toJsonl(featureMessages);
280
+ const expectedFeatureChannelJsonl = toJsonl(featureChannelMessages);
281
+
282
+ expectEqual(problems, 'Rebuilt main messages.jsonl', readFileText(mainMessagesFile), expectedMainJsonl);
283
+ expectEqual(problems, 'Rebuilt main history.jsonl', readFileText(mainHistoryFile), expectedMainJsonl);
284
+ expectEqual(problems, 'Rebuilt main channel messages.jsonl', readFileText(mainChannelMessagesFile), expectedMainChannelJsonl);
285
+ expectEqual(problems, 'Rebuilt main channel history.jsonl', readFileText(mainChannelHistoryFile), expectedMainChannelJsonl);
286
+ expectEqual(problems, `Rebuilt ${branchName} messages.jsonl`, readFileText(featureMessagesFile), expectedFeatureJsonl);
287
+ expectEqual(problems, `Rebuilt ${branchName} history.jsonl`, readFileText(featureHistoryFile), expectedFeatureJsonl);
288
+ expectEqual(problems, `Rebuilt ${branchName} channel messages.jsonl`, readFileText(featureChannelMessagesFile), expectedFeatureChannelJsonl);
289
+ expectEqual(problems, `Rebuilt ${branchName} channel history.jsonl`, readFileText(featureChannelHistoryFile), expectedFeatureChannelJsonl);
290
+ } finally {
291
+ fs.rmSync(tempRoot, { recursive: true, force: true });
292
+ }
293
+
294
+ if (problems.length > 0) {
295
+ fail([
296
+ 'Canonical message replay validation failed.',
297
+ 'Scenario: healthy',
298
+ ...problems.map((problem) => `- ${problem}`),
299
+ ]);
300
+ }
301
+
302
+ console.log([
303
+ 'Canonical message replay validation passed.',
304
+ 'Scenario: healthy',
305
+ 'Validated branch-local message.sent canonical event append.',
306
+ 'Validated replay/materialization preserves general-vs-channel message scope across branch-global and per-channel projections on main + feature_task3b branches.',
307
+ ].join('\n'));
308
+ }
309
+
310
+ function runCleanFixtureScenario() {
311
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'letthemtalk-task3c-clean-'));
312
+ const dataDir = path.join(tempRoot, '.agent-bridge');
313
+ const branchName = 'main';
314
+ const fixtureSpec = getFixtureSpec('clean');
315
+ const canonicalState = createCanonicalState({ dataDir, processPid: 4343 });
316
+ const eventLog = createCanonicalEventLog({ dataDir });
317
+ const problems = [];
318
+
319
+ try {
320
+ stageFixtureAsBranchEventLog(dataDir, branchName, fixtureSpec);
321
+ const replayResult = canonicalState.rebuildMessageProjections({ branch: branchName });
322
+ const fixtureEvents = parseFixtureObjects(fixtureSpec);
323
+ const expectedMessages = fixtureEvents.map((event) => event.payload.message);
324
+ const expectedJsonl = toJsonl(expectedMessages);
325
+ const messagesFile = path.join(dataDir, 'messages.jsonl');
326
+ const historyFile = path.join(dataDir, 'history.jsonl');
327
+ const replayedEvents = eventLog.readBranchEvents(branchName, { typePrefix: 'message.' });
328
+
329
+ expect(problems, replayResult.events_applied === expectedMessages.length, `Expected ${expectedMessages.length} replayed message events, received ${replayResult.events_applied}.`);
330
+ expect(problems, replayResult.message_count === expectedMessages.length, `Expected ${expectedMessages.length} replayed messages, received ${replayResult.message_count}.`);
331
+ expect(problems, replayResult.history_count === expectedMessages.length, `Expected ${expectedMessages.length} replayed history entries, received ${replayResult.history_count}.`);
332
+ expect(problems, replayedEvents.length === expectedMessages.length, `Expected ${expectedMessages.length} fixture events in the branch log, received ${replayedEvents.length}.`);
333
+ expectEqual(problems, 'Replayed clean fixture messages.jsonl', readFileText(messagesFile), expectedJsonl);
334
+ expectEqual(problems, 'Replayed clean fixture history.jsonl', readFileText(historyFile), expectedJsonl);
335
+ } finally {
336
+ fs.rmSync(tempRoot, { recursive: true, force: true });
337
+ }
338
+
339
+ if (problems.length > 0) {
340
+ fail([
341
+ 'Canonical message replay validation failed.',
342
+ 'Scenario: clean',
343
+ `Fixture: ${fixtureSpec.display}`,
344
+ ...problems.map((problem) => `- ${problem}`),
345
+ ]);
346
+ }
347
+
348
+ console.log([
349
+ 'Canonical message replay fixture validation passed.',
350
+ 'Scenario: clean',
351
+ `Fixture: ${fixtureSpec.display}`,
352
+ 'Validated deterministic replay/materialization from a clean canonical message-event fixture.',
353
+ ].join('\n'));
354
+ }
355
+
356
+ function runFailureFixtureScenario(scenario) {
357
+ const scenarioConfig = FAILURE_SCENARIOS[scenario];
358
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), `letthemtalk-task3c-${scenario}-`));
359
+ const dataDir = path.join(tempRoot, '.agent-bridge');
360
+ const branchName = 'main';
361
+ const fixtureSpec = getFixtureSpec(scenarioConfig.fixtureName);
362
+ const canonicalState = createCanonicalState({ dataDir, processPid: 4444 });
363
+ let outcome = null;
364
+
365
+ try {
366
+ stageFixtureAsBranchEventLog(dataDir, branchName, fixtureSpec);
367
+
368
+ try {
369
+ canonicalState.rebuildMessageProjections({ branch: branchName });
370
+ outcome = {
371
+ exitCode: 2,
372
+ lines: [
373
+ 'Canonical message replay failure validation did not match expectations.',
374
+ `Scenario: ${scenario}`,
375
+ `Fixture: ${fixtureSpec.display}`,
376
+ 'Replay unexpectedly succeeded for an invalid fixture.',
377
+ ],
378
+ };
379
+ } catch (error) {
380
+ const errorLines = buildReplayErrorLines(scenario, fixtureSpec, error);
381
+ const matchesExpectedCode = isCanonicalReplayError(error) && error.code === scenarioConfig.expectedCode;
382
+ const matchesExpectedMessage = scenarioConfig.expectedMessageFragments.every((fragment) =>
383
+ error && typeof error.message === 'string' && error.message.includes(fragment)
384
+ );
385
+
386
+ if (!matchesExpectedCode || !matchesExpectedMessage) {
387
+ outcome = {
388
+ exitCode: 2,
389
+ lines: [
390
+ 'Canonical message replay failure validation did not match expectations.',
391
+ `Scenario: ${scenario}`,
392
+ `Expected replay error code: ${scenarioConfig.expectedCode}`,
393
+ `Expected replay error fragments: ${scenarioConfig.expectedMessageFragments.join(' | ')}`,
394
+ ...errorLines,
395
+ ],
396
+ };
397
+ } else {
398
+ outcome = { exitCode: 1, lines: errorLines };
399
+ }
400
+ }
401
+ } finally {
402
+ fs.rmSync(tempRoot, { recursive: true, force: true });
403
+ }
404
+
405
+ fail(outcome.lines, outcome.exitCode);
406
+ }
407
+
408
+ function main() {
409
+ const { scenario } = parseArgs(process.argv.slice(2));
410
+
411
+ if (scenario === 'healthy') {
412
+ runHealthyScenario();
413
+ return;
414
+ }
415
+
416
+ if (scenario === 'clean') {
417
+ runCleanFixtureScenario();
418
+ return;
419
+ }
420
+
421
+ if (FAILURE_SCENARIOS[scenario]) {
422
+ runFailureFixtureScenario(scenario);
423
+ return;
424
+ }
425
+
426
+ fail([`Unsupported scenario: ${scenario}`, USAGE], 2);
427
+ }
428
+
429
+ main();