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,210 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const { createCanonicalEventLog } = require(path.resolve(__dirname, '..', 'events', 'log.js'));
8
+ const { createBranchPathResolvers } = require(path.resolve(__dirname, '..', 'state', 'canonical.js'));
9
+ const { createStateIo } = require(path.resolve(__dirname, '..', 'state', 'io.js'));
10
+ const { createSessionsState } = require(path.resolve(__dirname, '..', 'state', 'sessions.js'));
11
+
12
+ function fail(lines, exitCode = 1) {
13
+ fs.writeSync(2, lines.join('\n') + '\n');
14
+ process.exit(exitCode);
15
+ }
16
+
17
+ function readJson(filePath) {
18
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
19
+ }
20
+
21
+ function assert(condition, message, problems) {
22
+ if (!condition) problems.push(message);
23
+ }
24
+
25
+ function sessionTypesForAgent(eventLog, branchName, agentName) {
26
+ return eventLog
27
+ .readBranchEvents(branchName, { typePrefix: 'session.' })
28
+ .filter((event) => event.actor_agent === agentName)
29
+ .map((event) => event.type);
30
+ }
31
+
32
+ function main() {
33
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'letthemtalk-session-lifecycle-'));
34
+ const dataDir = path.join(tempDir, '.agent-bridge');
35
+ const branchPaths = createBranchPathResolvers(dataDir);
36
+ const io = createStateIo({ dataDir });
37
+ const eventLog = createCanonicalEventLog({ dataDir });
38
+ const sessionsState = createSessionsState({ io, branchPaths, canonicalEventLog: eventLog });
39
+
40
+ const times = {
41
+ alphaRegister: '2026-04-16T01:00:00.000Z',
42
+ alphaHeartbeat: '2026-04-16T01:00:10.000Z',
43
+ alphaSwitchAway: '2026-04-16T01:00:20.000Z',
44
+ alphaFeatureStart: '2026-04-16T01:00:30.000Z',
45
+ alphaFeatureTouch: '2026-04-16T01:00:40.000Z',
46
+ alphaDeadSnapshot: '2026-04-16T01:00:50.000Z',
47
+ alphaResume: '2026-04-16T01:01:00.000Z',
48
+ alphaGracefulExit: '2026-04-16T01:01:10.000Z',
49
+ alphaFreshStart: '2026-04-16T01:01:20.000Z',
50
+ betaStart: '2026-04-16T01:01:30.000Z',
51
+ betaResume: '2026-04-16T01:01:40.000Z',
52
+ rebuildIndex: '2026-04-16T01:01:50.000Z',
53
+ };
54
+
55
+ const problems = [];
56
+
57
+ try {
58
+ const alphaMain = sessionsState.activateSession({
59
+ agentName: 'alpha',
60
+ branchName: 'main',
61
+ provider: 'claude',
62
+ reason: 'register',
63
+ at: times.alphaRegister,
64
+ });
65
+ const alphaMainId = alphaMain.session.session_id;
66
+
67
+ sessionsState.touchSession({
68
+ sessionId: alphaMainId,
69
+ branchName: 'main',
70
+ at: times.alphaHeartbeat,
71
+ heartbeat: true,
72
+ });
73
+
74
+ sessionsState.transitionSession({
75
+ sessionId: alphaMainId,
76
+ branchName: 'main',
77
+ state: 'interrupted',
78
+ reason: 'branch_switch',
79
+ at: times.alphaSwitchAway,
80
+ });
81
+
82
+ const alphaFeature = sessionsState.activateSession({
83
+ agentName: 'alpha',
84
+ branchName: 'feature_task5a',
85
+ provider: 'claude',
86
+ reason: 'branch_activate',
87
+ at: times.alphaFeatureStart,
88
+ });
89
+ const alphaFeatureId = alphaFeature.session.session_id;
90
+
91
+ sessionsState.touchSession({
92
+ sessionId: alphaFeatureId,
93
+ branchName: 'feature_task5a',
94
+ at: times.alphaFeatureTouch,
95
+ });
96
+
97
+ const alphaInterrupted = sessionsState.transitionLatestSessionForAgent({
98
+ agentName: 'alpha',
99
+ branchName: 'feature_task5a',
100
+ state: 'interrupted',
101
+ reason: 'dead_agent_snapshot',
102
+ recoverySnapshotFile: 'recovery-alpha.json',
103
+ at: times.alphaDeadSnapshot,
104
+ });
105
+
106
+ const alphaResumed = sessionsState.activateSession({
107
+ agentName: 'alpha',
108
+ branchName: 'feature_task5a',
109
+ provider: 'claude',
110
+ reason: 'register',
111
+ at: times.alphaResume,
112
+ });
113
+
114
+ sessionsState.transitionSession({
115
+ sessionId: alphaResumed.session.session_id,
116
+ branchName: 'feature_task5a',
117
+ state: 'completed',
118
+ reason: 'graceful_exit',
119
+ recoverySnapshotFile: 'recovery-alpha.json',
120
+ at: times.alphaGracefulExit,
121
+ });
122
+
123
+ const alphaFresh = sessionsState.activateSession({
124
+ agentName: 'alpha',
125
+ branchName: 'feature_task5a',
126
+ provider: 'claude',
127
+ reason: 'register',
128
+ at: times.alphaFreshStart,
129
+ });
130
+
131
+ const betaStart = sessionsState.activateSession({
132
+ agentName: 'beta',
133
+ branchName: 'main',
134
+ provider: 'gemini',
135
+ reason: 'register',
136
+ at: times.betaStart,
137
+ });
138
+
139
+ const betaResume = sessionsState.activateSession({
140
+ agentName: 'beta',
141
+ branchName: 'main',
142
+ provider: 'gemini',
143
+ reason: 'register',
144
+ at: times.betaResume,
145
+ });
146
+
147
+ const alphaMainManifest = readJson(branchPaths.getBranchSessionFile(alphaMainId, 'main'));
148
+ const alphaFeatureManifest = readJson(branchPaths.getBranchSessionFile(alphaFeatureId, 'feature_task5a'));
149
+ const alphaFreshManifest = readJson(branchPaths.getBranchSessionFile(alphaFresh.session.session_id, 'feature_task5a'));
150
+ const betaManifest = readJson(branchPaths.getBranchSessionFile(betaStart.session.session_id, 'main'));
151
+ const indexFile = branchPaths.getSessionsIndexFile();
152
+ const index = readJson(indexFile);
153
+
154
+ assert(alphaMain.created && !alphaMain.resumed, 'Initial register should create a main-branch session for alpha.', problems);
155
+ assert(alphaMainManifest.state === 'interrupted', 'Switching branches should interrupt alpha\'s main-branch session.', problems);
156
+ assert(alphaMainManifest.transition_reason === 'branch_switch', 'Main-branch interruption should record the branch_switch reason.', problems);
157
+ assert(alphaMainManifest.last_heartbeat_at === times.alphaHeartbeat, 'Heartbeat touch should update the session heartbeat timestamp.', problems);
158
+
159
+ assert(alphaFeature.created && !alphaFeature.resumed, 'First feature-branch activation should create a fresh branch-local session.', problems);
160
+ assert(alphaInterrupted.session && alphaInterrupted.session.state === 'interrupted', 'Dead-agent snapshot should mark the active feature session interrupted.', problems);
161
+ assert(alphaInterrupted.session && alphaInterrupted.session.recovery_snapshot_file === 'recovery-alpha.json', 'Dead-agent interruption should retain the recovery snapshot reference.', problems);
162
+
163
+ assert(alphaResumed.resumed, 'Registering again on the same interrupted branch should resume the feature session.', problems);
164
+ assert(alphaResumed.session.session_id === alphaFeatureId, 'Branch-local resume should reuse the interrupted feature session id.', problems);
165
+ assert(alphaFeatureManifest.state === 'completed', 'Graceful exit should mark the resumed feature session completed.', problems);
166
+ assert(alphaFeatureManifest.resume_count === 1, 'Feature session should record one resume after interruption.', problems);
167
+ assert(alphaFeatureManifest.transition_reason === 'graceful_exit', 'Graceful completion should keep the graceful_exit reason.', problems);
168
+
169
+ assert(alphaFresh.created && !alphaFresh.resumed, 'Registering after a completed session should create a new feature session.', problems);
170
+ assert(alphaFresh.session.session_id !== alphaFeatureId, 'Completed sessions must not be resumed as the next live interval.', problems);
171
+ assert(alphaFreshManifest.state === 'active', 'Fresh post-completion session should be active.', problems);
172
+
173
+ assert(betaStart.created && !betaStart.resumed, 'Beta should start with a new main-branch session.', problems);
174
+ assert(betaResume.resumed, 'An orphaned active session should be reclaimed by interrupt+resume on register.', problems);
175
+ assert(betaResume.session.session_id === betaStart.session.session_id, 'Orphaned active recovery should reuse the same beta session id.', problems);
176
+ assert(betaManifest.resume_count === 1, 'Recovered orphaned beta session should record one resume.', problems);
177
+
178
+ assert(fs.existsSync(indexFile), 'Session discovery index should be written under runtime/projections.', problems);
179
+ assert(index.sessions[alphaMainId] && index.sessions[alphaFeatureId] && index.sessions[alphaFresh.session.session_id], 'Index should contain every session summary.', problems);
180
+ assert(index.by_agent.alpha && index.by_agent.alpha.active_session_id === alphaFresh.session.session_id, 'Index should point alpha to the latest active branch session.', problems);
181
+ assert(index.by_branch.feature_task5a && index.by_branch.feature_task5a.active_session_ids.includes(alphaFresh.session.session_id), 'Index should expose the active feature-branch session.', problems);
182
+
183
+ fs.unlinkSync(indexFile);
184
+ const rebuiltIndex = sessionsState.rebuildIndex({ at: times.rebuildIndex });
185
+ assert(rebuiltIndex.sessions[alphaFeatureId] && rebuiltIndex.sessions[alphaFeatureId].state === 'completed', 'Rebuilt index should recover completed feature-session state from manifests.', problems);
186
+ assert(rebuiltIndex.by_agent.alpha.active_session_id === alphaFresh.session.session_id, 'Rebuilt index should restore the latest active alpha session.', problems);
187
+
188
+ const alphaMainEvents = sessionTypesForAgent(eventLog, 'main', 'alpha');
189
+ const alphaFeatureEvents = sessionTypesForAgent(eventLog, 'feature_task5a', 'alpha');
190
+ const betaEvents = sessionTypesForAgent(eventLog, 'main', 'beta');
191
+
192
+ assert(JSON.stringify(alphaMainEvents) === JSON.stringify(['session.started', 'session.interrupted']), 'Main branch should log start+interrupt events for alpha.', problems);
193
+ assert(JSON.stringify(alphaFeatureEvents) === JSON.stringify(['session.started', 'session.interrupted', 'session.resumed', 'session.completed', 'session.started']), 'Feature branch should log the full alpha lifecycle sequence.', problems);
194
+ assert(JSON.stringify(betaEvents) === JSON.stringify(['session.started', 'session.interrupted', 'session.resumed']), 'Main branch should log orphaned-active recovery for beta as start+interrupt+resume.', problems);
195
+
196
+ if (problems.length > 0) {
197
+ fail(['Session lifecycle validation failed.', ...problems], 1);
198
+ }
199
+
200
+ console.log([
201
+ 'Session lifecycle validation passed.',
202
+ 'Validated branch-scoped session creation, heartbeat activity, branch interruption, dead-agent interruption, resume, graceful completion, orphaned-active recovery, and index rebuild.',
203
+ `Index file: .agent-bridge/${path.relative(dataDir, indexFile).split(path.sep).join('/')}`,
204
+ ].join('\n'));
205
+ } finally {
206
+ fs.rmSync(tempDir, { recursive: true, force: true });
207
+ }
208
+ }
209
+
210
+ main();
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+
3
+ const path = require('path');
4
+
5
+ const {
6
+ resolveDataDir,
7
+ resolveDefaultDataRoot,
8
+ } = require(path.resolve(__dirname, '..', 'data-dir.js'));
9
+ const { createCanonicalState } = require(path.resolve(__dirname, '..', 'state', 'canonical.js'));
10
+
11
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
12
+ const USAGE = [
13
+ 'Usage: node agent-bridge/scripts/export-markdown-workspace.js',
14
+ ' [--data-dir <path>] [--project-root <path>] [--output <path>] [--branch <branch> ...]',
15
+ ].join('\n');
16
+
17
+ function fail(message, exitCode = 1) {
18
+ process.stderr.write(`${message}\n`);
19
+ process.exit(exitCode);
20
+ }
21
+
22
+ function parseArgs(argv) {
23
+ const result = {
24
+ branches: [],
25
+ dataDir: null,
26
+ outputRoot: null,
27
+ projectRoot: null,
28
+ };
29
+
30
+ for (let index = 0; index < argv.length; index++) {
31
+ const arg = argv[index];
32
+ if (arg === '--help' || arg === '-h') {
33
+ fail(USAGE, 0);
34
+ }
35
+
36
+ if (!['--data-dir', '--output', '--project-root', '--branch'].includes(arg)) {
37
+ fail(USAGE, 2);
38
+ }
39
+
40
+ if (index + 1 >= argv.length) {
41
+ fail(USAGE, 2);
42
+ }
43
+
44
+ const value = argv[index + 1];
45
+ index += 1;
46
+
47
+ if (arg === '--data-dir') {
48
+ result.dataDir = path.resolve(value);
49
+ continue;
50
+ }
51
+
52
+ if (arg === '--output') {
53
+ result.outputRoot = path.resolve(value);
54
+ continue;
55
+ }
56
+
57
+ if (arg === '--project-root') {
58
+ result.projectRoot = path.resolve(value);
59
+ continue;
60
+ }
61
+
62
+ result.branches.push(value);
63
+ }
64
+
65
+ return result;
66
+ }
67
+
68
+ function main() {
69
+ const args = parseArgs(process.argv.slice(2));
70
+ const projectRoot = args.projectRoot
71
+ || (args.dataDir ? path.resolve(args.dataDir, '..') : resolveDefaultDataRoot({ cwd: process.cwd(), moduleDir: PACKAGE_ROOT }));
72
+ const dataDir = args.dataDir
73
+ || resolveDataDir({ cwd: projectRoot, moduleDir: PACKAGE_ROOT });
74
+ const canonicalState = createCanonicalState({ dataDir, processPid: process.pid });
75
+ const result = canonicalState.exportMarkdownWorkspace({
76
+ projectRoot,
77
+ outputRoot: args.outputRoot,
78
+ branches: args.branches.length > 0 ? args.branches : null,
79
+ });
80
+
81
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
82
+ }
83
+
84
+ main();
@@ -0,0 +1,2 @@
1
+ {"event_id":"evt-task3c-clean-1","stream":"branch","branch_id":"main","seq":1,"type":"message.sent","occurred_at":"2026-04-15T20:10:00.000Z","schema_version":1,"actor_agent":"fixture-alpha","session_id":"session_task3c_clean","command_id":"cmd_task3c_clean_1","causation_id":null,"correlation_id":"corr_task3c_clean","payload":{"message":{"id":"msg-task3c-clean-1","from":"fixture-alpha","to":"fixture-beta","content":"Fixture clean hello","timestamp":"2026-04-15T20:10:00.000Z","reply_to":null,"system":false}}}
2
+ {"event_id":"evt-task3c-clean-2","stream":"branch","branch_id":"main","seq":2,"type":"message.sent","occurred_at":"2026-04-15T20:10:05.000Z","schema_version":1,"actor_agent":"fixture-beta","session_id":"session_task3c_clean","command_id":"cmd_task3c_clean_2","causation_id":"evt-task3c-clean-1","correlation_id":"corr_task3c_clean","payload":{"message":{"id":"msg-task3c-clean-2","from":"fixture-beta","to":"fixture-alpha","content":"Fixture clean reply","timestamp":"2026-04-15T20:10:05.000Z","reply_to":"msg-task3c-clean-1","system":false}}}
@@ -0,0 +1 @@
1
+ {"event_id":"evt-task3c-corrupt-correction-payload-1","stream":"branch","branch_id":"main","seq":1,"type":"message.corrected","occurred_at":"2026-04-15T20:13:00.000Z","schema_version":1,"actor_agent":"fixture-alpha","session_id":"session_task3c_corrupt_correction_payload","command_id":"cmd_task3c_corrupt_correction_payload_1","causation_id":null,"correlation_id":"corr_task3c_corrupt_correction_payload","payload":{"message_id":"msg-task3c-corrupt-correction-payload-1","content":42}}
@@ -0,0 +1 @@
1
+ {"event_id":"evt-task3c-corrupt-jsonl-1","stream":"branch","branch_id":"main","seq":1,"type":"message.sent","occurred_at":"2026-04-15T20:11:00.000Z","schema_version":1,"actor_agent":"fixture-alpha","session_id":"session_task3c_corrupt_jsonl","command_id":"cmd_task3c_corrupt_jsonl_1","causation_id":null,"correlation_id":"corr_task3c_corrupt_jsonl","payload":{"message":{"id":"msg-task3c-corrupt-jsonl-1","from":"fixture-alpha","to":"fixture-beta","content":"This line is intentionally malformed","timestamp":"2026-04-15T20:11:00.000Z","reply_to":null,"system":false}}
@@ -0,0 +1 @@
1
+ {"event_id":"evt-task3c-corrupt-payload-1","stream":"branch","branch_id":"main","seq":1,"type":"message.sent","occurred_at":"2026-04-15T20:12:00.000Z","schema_version":1,"actor_agent":"fixture-alpha","session_id":"session_task3c_corrupt_payload","command_id":"cmd_task3c_corrupt_payload_1","causation_id":null,"correlation_id":"corr_task3c_corrupt_payload","payload":{"message":"not-an-object"}}
@@ -0,0 +1,2 @@
1
+ {"event_id":"evt-task3c-out-of-order-1","stream":"branch","branch_id":"main","seq":2,"type":"message.sent","occurred_at":"2026-04-15T20:13:05.000Z","schema_version":1,"actor_agent":"fixture-beta","session_id":"session_task3c_out_of_order","command_id":"cmd_task3c_out_of_order_2","causation_id":"evt-task3c-out-of-order-2","correlation_id":"corr_task3c_out_of_order","payload":{"message":{"id":"msg-task3c-out-of-order-2","from":"fixture-beta","to":"fixture-alpha","content":"Second event placed first on purpose","timestamp":"2026-04-15T20:13:05.000Z","reply_to":"msg-task3c-out-of-order-1","system":false}}}
2
+ {"event_id":"evt-task3c-out-of-order-2","stream":"branch","branch_id":"main","seq":1,"type":"message.sent","occurred_at":"2026-04-15T20:13:00.000Z","schema_version":1,"actor_agent":"fixture-alpha","session_id":"session_task3c_out_of_order","command_id":"cmd_task3c_out_of_order_1","causation_id":null,"correlation_id":"corr_task3c_out_of_order","payload":{"message":{"id":"msg-task3c-out-of-order-1","from":"fixture-alpha","to":"fixture-beta","content":"First event placed second on purpose","timestamp":"2026-04-15T20:13:00.000Z","reply_to":null,"system":false}}}
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+ // Migrate a pre-canonical .agent-bridge/ project to the canonical event-stream model.
3
+ //
4
+ // Old projects have legacy projection files (messages.jsonl, history.jsonl) under
5
+ // .agent-bridge/ but no canonical stream at .agent-bridge/runtime/branches/main/events.jsonl.
6
+ // The current canonical layer fail-closes any rebuild on legacy-only state to prevent
7
+ // silent data loss, which makes Clear Messages and several other dashboard actions error
8
+ // out on those projects.
9
+ //
10
+ // This script:
11
+ // 1. Detects the legacy-only condition.
12
+ // 2. Backs up the legacy projections to .agent-bridge/legacy-backup-<timestamp>/.
13
+ // 3. Removes the bare projections so the canonical layer is willing to write.
14
+ // 4. Replays each legacy message through canonicalState.appendMessage() so the canonical
15
+ // stream is created and projections are rewritten in lockstep.
16
+ //
17
+ // Usage:
18
+ // node agent-bridge/scripts/migrate-legacy-to-canonical.js [project-path]
19
+ // node agent-bridge/scripts/migrate-legacy-to-canonical.js --dry-run [project-path]
20
+ //
21
+ // project-path defaults to the current working directory.
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const { resolveDataDir } = require('../data-dir');
26
+ const { createCanonicalState } = require('../state/canonical');
27
+
28
+ function readJsonl(file) {
29
+ if (!fs.existsSync(file)) return [];
30
+ return fs
31
+ .readFileSync(file, 'utf8')
32
+ .split(/\r?\n/)
33
+ .filter((line) => line.trim())
34
+ .map((line) => {
35
+ try { return JSON.parse(line); } catch { return null; }
36
+ })
37
+ .filter(Boolean);
38
+ }
39
+
40
+ function loadLegacyMessages(dataDir) {
41
+ const history = readJsonl(path.join(dataDir, 'history.jsonl'));
42
+ if (history.length > 0) return history;
43
+ return readJsonl(path.join(dataDir, 'messages.jsonl'));
44
+ }
45
+
46
+ function backupLegacyProjections(dataDir) {
47
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
48
+ const backupDir = path.join(dataDir, 'legacy-backup-' + stamp);
49
+ fs.mkdirSync(backupDir, { recursive: true });
50
+ let copied = 0;
51
+ for (const fileName of ['messages.jsonl', 'history.jsonl']) {
52
+ const src = path.join(dataDir, fileName);
53
+ if (fs.existsSync(src)) {
54
+ fs.copyFileSync(src, path.join(backupDir, fileName));
55
+ copied++;
56
+ }
57
+ }
58
+ return { backupDir, copied };
59
+ }
60
+
61
+ function removeLegacyProjections(dataDir) {
62
+ for (const fileName of ['messages.jsonl', 'history.jsonl']) {
63
+ const src = path.join(dataDir, fileName);
64
+ if (fs.existsSync(src)) fs.unlinkSync(src);
65
+ }
66
+ }
67
+
68
+ function migrate(projectArg, opts) {
69
+ const dataDir = resolveDataDir({ cwd: projectArg });
70
+ const eventFile = path.join(dataDir, 'runtime', 'branches', 'main', 'events.jsonl');
71
+ const legacyMessages = path.join(dataDir, 'messages.jsonl');
72
+ const legacyHistory = path.join(dataDir, 'history.jsonl');
73
+
74
+ console.log('');
75
+ console.log(' Let Them Talk — Legacy Migration');
76
+ console.log(' ================================');
77
+ console.log(' Project data dir: ' + dataDir);
78
+ console.log('');
79
+
80
+ if (!fs.existsSync(dataDir)) {
81
+ console.log(' [info] No .agent-bridge/ directory at this path. Nothing to migrate.');
82
+ return { migrated: 0 };
83
+ }
84
+
85
+ const hasLegacy = fs.existsSync(legacyMessages) || fs.existsSync(legacyHistory);
86
+ const hasCanonical = fs.existsSync(eventFile);
87
+
88
+ if (!hasLegacy && !hasCanonical) {
89
+ console.log(' [info] No legacy projections and no canonical stream — nothing to do.');
90
+ return { migrated: 0 };
91
+ }
92
+
93
+ if (hasCanonical && hasLegacy) {
94
+ console.log(' [info] Both canonical stream and legacy projections exist.');
95
+ console.log(' Canonical stream wins; legacy projection files will be archived to be safe.');
96
+ if (opts.dryRun) {
97
+ console.log(' [dry-run] Would archive legacy projections to .agent-bridge/legacy-backup-<ts>/');
98
+ return { migrated: 0, archived: true };
99
+ }
100
+ const backup = backupLegacyProjections(dataDir);
101
+ if (backup.copied > 0) {
102
+ removeLegacyProjections(dataDir);
103
+ console.log(' [ok] Archived ' + backup.copied + ' legacy projection file(s) to ' + backup.backupDir);
104
+ }
105
+ return { migrated: 0, archived: true };
106
+ }
107
+
108
+ if (hasCanonical && !hasLegacy) {
109
+ console.log(' [info] Canonical event stream already exists and no legacy projections present.');
110
+ console.log(' Project is already on the canonical schema.');
111
+ return { migrated: 0 };
112
+ }
113
+
114
+ // Legacy-only — the case the canonical layer refuses to operate on.
115
+ const messages = loadLegacyMessages(dataDir);
116
+ console.log(' [info] Legacy-only project detected.');
117
+ console.log(' [info] Found ' + messages.length + ' legacy message(s) to replay onto branch "main".');
118
+
119
+ if (opts.dryRun) {
120
+ console.log('');
121
+ console.log(' [dry-run] Would back up legacy projections, then replay messages through the canonical layer.');
122
+ console.log(' [dry-run] Run again without --dry-run to perform the migration.');
123
+ return { migrated: 0, dryRun: true, candidate: messages.length };
124
+ }
125
+
126
+ const backup = backupLegacyProjections(dataDir);
127
+ if (backup.copied > 0) {
128
+ console.log(' [ok] Archived ' + backup.copied + ' legacy projection file(s) to ' + backup.backupDir);
129
+ }
130
+
131
+ // Remove legacy projections so the canonical layer is willing to (re)build them.
132
+ removeLegacyProjections(dataDir);
133
+
134
+ const canonicalState = createCanonicalState({ dataDir });
135
+
136
+ let migrated = 0;
137
+ let skipped = 0;
138
+ for (const m of messages) {
139
+ if (!m || typeof m !== 'object') { skipped++; continue; }
140
+ if (typeof m.id !== 'string' || !m.id) { skipped++; continue; }
141
+ try {
142
+ canonicalState.appendMessage(m, { branch: 'main', actorAgent: m.from || 'system' });
143
+ migrated++;
144
+ } catch (e) {
145
+ skipped++;
146
+ console.warn(' [warn] Skipped message ' + m.id + ': ' + (e && e.message ? e.message : e));
147
+ }
148
+ }
149
+
150
+ console.log('');
151
+ console.log(' [ok] Migrated ' + migrated + '/' + messages.length + ' message(s) onto the canonical event stream.');
152
+ if (skipped > 0) console.log(' [warn] Skipped ' + skipped + ' message(s) — see warnings above.');
153
+ console.log(' [ok] Canonical event log: ' + eventFile);
154
+ console.log(' [info] Original projections are preserved in: ' + backup.backupDir);
155
+ console.log('');
156
+ console.log(' You can now use Clear Messages and other dashboard actions on this project.');
157
+ console.log('');
158
+
159
+ return { migrated, skipped, backupDir: backup.backupDir };
160
+ }
161
+
162
+ function parseArgs(argv) {
163
+ const args = argv.slice(2);
164
+ const opts = { dryRun: false };
165
+ const positional = [];
166
+ for (const a of args) {
167
+ if (a === '--dry-run' || a === '-n') opts.dryRun = true;
168
+ else if (a === '--help' || a === '-h') opts.help = true;
169
+ else positional.push(a);
170
+ }
171
+ opts.project = positional[0] || process.cwd();
172
+ return opts;
173
+ }
174
+
175
+ function printUsage() {
176
+ console.log('');
177
+ console.log(' migrate-legacy-to-canonical');
178
+ console.log(' ===========================');
179
+ console.log(' Backfills the canonical event stream from legacy projection files so');
180
+ console.log(' pre-upgrade projects work with the new dashboard control plane.');
181
+ console.log('');
182
+ console.log(' Usage:');
183
+ console.log(' node agent-bridge/scripts/migrate-legacy-to-canonical.js [project-path]');
184
+ console.log(' node agent-bridge/scripts/migrate-legacy-to-canonical.js --dry-run [project-path]');
185
+ console.log('');
186
+ console.log(' project-path defaults to the current working directory.');
187
+ console.log('');
188
+ }
189
+
190
+ if (require.main === module) {
191
+ const opts = parseArgs(process.argv);
192
+ if (opts.help) { printUsage(); process.exit(0); }
193
+ try {
194
+ migrate(opts.project, opts);
195
+ } catch (e) {
196
+ console.error(' [error] ' + (e && e.stack ? e.stack : e));
197
+ process.exit(1);
198
+ }
199
+ }
200
+
201
+ module.exports = { migrate };