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,277 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const { ApiAgentEngine } = require(path.resolve(__dirname, '..', 'api-agents.js'));
8
+ const { createCanonicalState, createBranchPathResolvers } = require(path.resolve(__dirname, '..', 'state', 'canonical.js'));
9
+
10
+ function fail(lines, exitCode = 1) {
11
+ process.stderr.write(lines.join('\n') + '\n');
12
+ process.exit(exitCode);
13
+ }
14
+
15
+ function assert(condition, message, problems) {
16
+ if (!condition) problems.push(message);
17
+ }
18
+
19
+ function readJson(filePath, fallback) {
20
+ if (!fs.existsSync(filePath)) return fallback;
21
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
22
+ }
23
+
24
+ function readJsonl(filePath) {
25
+ if (!fs.existsSync(filePath)) return [];
26
+ const raw = fs.readFileSync(filePath, 'utf8').trim();
27
+ if (!raw) return [];
28
+ return raw.split(/\r?\n/).map((line) => {
29
+ try {
30
+ return JSON.parse(line);
31
+ } catch {
32
+ return null;
33
+ }
34
+ }).filter(Boolean);
35
+ }
36
+
37
+ function createFixtureDataDir() {
38
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ltt-api-agent-parity-'));
39
+ const dataDir = path.join(tempRoot, '.agent-bridge');
40
+ fs.mkdirSync(dataDir, { recursive: true });
41
+ return { tempRoot, dataDir };
42
+ }
43
+
44
+ function removeFixture(tempRoot) {
45
+ try {
46
+ fs.rmSync(tempRoot, { recursive: true, force: true });
47
+ } catch {}
48
+ }
49
+
50
+ function sleep(ms) {
51
+ return new Promise((resolve) => setTimeout(resolve, ms));
52
+ }
53
+
54
+ async function waitFor(predicate, timeoutMs = 2000, intervalMs = 20) {
55
+ const deadline = Date.now() + timeoutMs;
56
+ while (Date.now() < deadline) {
57
+ if (predicate()) return true;
58
+ await sleep(intervalMs);
59
+ }
60
+ return false;
61
+ }
62
+
63
+ async function main() {
64
+ const problems = [];
65
+ const fixture = createFixtureDataDir();
66
+ const featureBranch = 'feature_task10b';
67
+ const channelName = 'ops';
68
+ let engine = null;
69
+
70
+ try {
71
+ const canonicalState = createCanonicalState({ dataDir: fixture.dataDir, processPid: 7100 });
72
+ const branchPaths = createBranchPathResolvers(fixture.dataDir);
73
+ engine = new ApiAgentEngine(fixture.dataDir);
74
+
75
+ const createResult = engine.create('render_bot', 'zai', {
76
+ model: 'glm-image',
77
+ capabilities: ['image_generation'],
78
+ });
79
+ assert(createResult && createResult.ok, 'API parity fixture should create the render_bot adapter successfully.', problems);
80
+
81
+ engine.agents.render_bot.provider = {
82
+ async generate(prompt) {
83
+ return { type: 'text', data: `Rendered: ${prompt}` };
84
+ },
85
+ };
86
+
87
+ const requesterSession = canonicalState.ensureAgentSession({
88
+ agentName: 'artist_alpha',
89
+ branchName: featureBranch,
90
+ provider: 'claude',
91
+ reason: 'fixture_request',
92
+ });
93
+
94
+ fs.writeFileSync(branchPaths.getChannelsFile(featureBranch), JSON.stringify({
95
+ general: { description: 'General channel', members: ['*'] },
96
+ [channelName]: { description: 'Ops channel', members: ['*'] },
97
+ }, null, 2));
98
+
99
+ const startResult = engine.start('render_bot');
100
+ assert(startResult && startResult.ok, 'API parity fixture should start the render_bot adapter successfully.', problems);
101
+
102
+ const incomingMessage = {
103
+ id: 'msg_task10b_feature_request',
104
+ from: 'artist_alpha',
105
+ to: 'render_bot',
106
+ content: 'Generate: branch-aware adapter parity art',
107
+ timestamp: '2026-04-16T18:00:00.000Z',
108
+ channel: channelName,
109
+ session_id: requesterSession && requesterSession.session ? requesterSession.session.session_id : null,
110
+ command_id: 'cmd_task10b_feature_request',
111
+ correlation_id: 'corr_task10b_feature_request',
112
+ };
113
+
114
+ canonicalState.appendScopedMessage(incomingMessage, {
115
+ branch: featureBranch,
116
+ channel: channelName,
117
+ actorAgent: incomingMessage.from,
118
+ sessionId: incomingMessage.session_id,
119
+ commandId: incomingMessage.command_id,
120
+ correlationId: incomingMessage.correlation_id,
121
+ });
122
+
123
+ engine._pollMessages('render_bot');
124
+
125
+ const featureChannelHistoryFile = branchPaths.getChannelHistoryFile(channelName, featureBranch);
126
+ const featureChannelMessagesFile = branchPaths.getChannelMessagesFile(channelName, featureBranch);
127
+ const featureBranchHistoryFile = branchPaths.getHistoryFile(featureBranch);
128
+ const featureBranchMessagesFile = branchPaths.getMessagesFile(featureBranch);
129
+ const mainHistoryFile = branchPaths.getHistoryFile('main');
130
+ const mainMessagesFile = branchPaths.getMessagesFile('main');
131
+
132
+ const processed = await waitFor(() => readJsonl(featureChannelHistoryFile).filter((message) => message.from === 'render_bot').length >= 2);
133
+ assert(processed, 'API parity fixture should observe two render_bot replies in the feature branch channel history.', problems);
134
+
135
+ const featureChannelMessages = readJsonl(featureChannelMessagesFile);
136
+ const featureChannelHistory = readJsonl(featureChannelHistoryFile);
137
+ const featureBranchHistory = readJsonl(featureBranchHistoryFile);
138
+ const featureBranchMessages = readJsonl(featureBranchMessagesFile);
139
+ const mainHistory = readJsonl(mainHistoryFile);
140
+ const mainMessages = readJsonl(mainMessagesFile);
141
+ const agentReplies = featureChannelHistory.filter((message) => message.from === 'render_bot');
142
+ const liveAgentReplies = featureChannelMessages.filter((message) => message.from === 'render_bot');
143
+ const processingReply = agentReplies.find((message) => typeof message.content === 'string' && message.content.startsWith('Processing:'));
144
+ const finalReply = agentReplies.find((message) => message.content === 'Rendered: branch-aware adapter parity art');
145
+ const replySessionId = processingReply && processingReply.session_id ? processingReply.session_id : (finalReply && finalReply.session_id ? finalReply.session_id : null);
146
+ const requesterSessionId = requesterSession && requesterSession.session ? requesterSession.session.session_id : null;
147
+
148
+ assert(featureChannelMessages.some((message) => message.id === incomingMessage.id), 'Feature branch channel messages should include the original incoming request.', problems);
149
+ assert(featureChannelHistory.some((message) => message.id === incomingMessage.id), 'Feature branch channel history should include the original incoming request.', problems);
150
+ assert(!!featureChannelMessages.find((message) => typeof message.content === 'string' && message.content.startsWith('Processing:') && message.from === 'render_bot'), 'Feature branch channel messages should include the processing reply.', problems);
151
+ assert(!!featureChannelMessages.find((message) => message.content === 'Rendered: branch-aware adapter parity art' && message.from === 'render_bot'), 'Feature branch channel messages should include the final rendered reply.', problems);
152
+ assert(!!processingReply, 'Feature branch channel history should include the processing reply.', problems);
153
+ assert(!!finalReply, 'Feature branch channel history should include the final rendered reply.', problems);
154
+ assert(featureBranchHistory.length === 0, 'Non-general feature branch replies should not leak into the branch default history file.', problems);
155
+ assert(featureBranchMessages.length === 0, 'Non-general feature branch replies should not leak into the branch default messages file.', problems);
156
+ assert(mainHistory.length === 0, 'Feature branch adapter replies should not leak into main-branch history.', problems);
157
+ assert(mainMessages.length === 0, 'Feature branch adapter replies should not leak into main-branch live messages.', problems);
158
+ assert(sameReplySet(liveAgentReplies, agentReplies), 'Scoped live channel messages should mirror the scoped channel history for adapter replies.', problems);
159
+ assert(replySessionId && replySessionId !== requesterSessionId, 'API adapter replies should use an adapter-owned branch session instead of reusing the requester session.', problems);
160
+
161
+ for (const reply of agentReplies) {
162
+ assert(reply.channel === channelName, 'API adapter replies should preserve the original non-general channel.', problems);
163
+ assert(reply.reply_to === incomingMessage.id, 'API adapter replies should preserve reply_to against the triggering message.', problems);
164
+ assert(reply.thread_id === incomingMessage.id, 'API adapter replies should preserve thread_id for branch/channel conversations.', problems);
165
+ assert(reply.command_id === incomingMessage.command_id, 'API adapter replies should propagate command_id when present.', problems);
166
+ assert(reply.causation_id === incomingMessage.id, 'API adapter replies should set causation_id to the triggering message id.', problems);
167
+ assert(reply.correlation_id === incomingMessage.correlation_id, 'API adapter replies should preserve correlation_id when present.', problems);
168
+ assert(reply.session_id === replySessionId && !!reply.session_id, 'API adapter replies should use the adapter branch session id.', problems);
169
+ }
170
+
171
+ const activeRenderSession = canonicalState.listBranchSessions(featureBranch)
172
+ .filter((session) => session.agent_name === 'render_bot')
173
+ .pop();
174
+ assert(!!activeRenderSession, 'API parity fixture should create a branch-local session manifest for render_bot.', problems);
175
+ assert(activeRenderSession && activeRenderSession.state === 'active', 'render_bot session should be active while the adapter is running.', problems);
176
+ assert(activeRenderSession && activeRenderSession.provider === 'zai', 'render_bot branch session should retain the adapter provider id.', problems);
177
+ assert(activeRenderSession && activeRenderSession.session_id === replySessionId, 'Reply session metadata should match the canonical branch session manifest id.', problems);
178
+
179
+ const renderSessionManifest = replySessionId
180
+ ? readJson(branchPaths.getBranchSessionFile(replySessionId, featureBranch), null)
181
+ : null;
182
+ const sessionsIndex = readJson(branchPaths.getSessionsIndexFile(), null);
183
+ assert(!!renderSessionManifest, 'API parity fixture should persist the adapter branch session manifest file.', problems);
184
+ assert(renderSessionManifest && renderSessionManifest.agent_name === 'render_bot', 'Adapter branch session manifest should record the adapter agent name.', problems);
185
+ assert(renderSessionManifest && renderSessionManifest.branch_id === featureBranch, 'Adapter branch session manifest should stay scoped to the feature branch.', problems);
186
+ assert(renderSessionManifest && renderSessionManifest.provider === 'zai', 'Adapter branch session manifest should retain provider parity metadata.', problems);
187
+ assert(sessionsIndex && sessionsIndex.by_agent && sessionsIndex.by_agent.render_bot && sessionsIndex.by_agent.render_bot.active_session_id === replySessionId, 'Sessions index should expose the adapter active session id for render_bot.', problems);
188
+ assert(sessionsIndex && sessionsIndex.by_agent && sessionsIndex.by_agent.render_bot && sessionsIndex.by_agent.render_bot.active_branch_id === featureBranch, 'Sessions index should expose the adapter active branch for render_bot.', problems);
189
+ assert(sessionsIndex && sessionsIndex.by_branch && sessionsIndex.by_branch[featureBranch] && Array.isArray(sessionsIndex.by_branch[featureBranch].active_session_ids) && sessionsIndex.by_branch[featureBranch].active_session_ids.includes(replySessionId), 'Sessions index should include the adapter active session under the feature branch projection.', problems);
190
+
191
+ const agentsFile = path.join(fixture.dataDir, 'agents.json');
192
+ const agents = readJson(agentsFile, {});
193
+ assert(agents.render_bot && agents.render_bot.branch === featureBranch, 'API adapter agent row should track the latest active branch.', problems);
194
+ assert(agents.render_bot && agents.render_bot.runtime_type === 'api', 'API adapter agent row should retain explicit runtime_type metadata.', problems);
195
+ assert(agents.render_bot && agents.render_bot.provider_id === 'zai', 'API adapter agent row should retain explicit provider_id metadata.', problems);
196
+
197
+ engine.stop('render_bot');
198
+
199
+ const stoppedRenderSession = canonicalState.listBranchSessions(featureBranch)
200
+ .filter((session) => session.agent_name === 'render_bot')
201
+ .pop();
202
+ assert(stoppedRenderSession && stoppedRenderSession.state === 'interrupted', 'Stopping the API adapter should interrupt its active branch session.', problems);
203
+
204
+ const stoppedSessionsIndex = readJson(branchPaths.getSessionsIndexFile(), null);
205
+ assert(stoppedSessionsIndex && stoppedSessionsIndex.by_agent && stoppedSessionsIndex.by_agent.render_bot && stoppedSessionsIndex.by_agent.render_bot.active_session_id === null, 'Stopping the API adapter should clear the active session pointer in the sessions index.', problems);
206
+ assert(stoppedSessionsIndex && stoppedSessionsIndex.by_branch && stoppedSessionsIndex.by_branch[featureBranch] && Array.isArray(stoppedSessionsIndex.by_branch[featureBranch].active_session_ids) && !stoppedSessionsIndex.by_branch[featureBranch].active_session_ids.includes(replySessionId), 'Stopping the API adapter should remove the interrupted session from the branch active-session index.', problems);
207
+
208
+ const backlogRequest = {
209
+ id: 'msg_task10b_feature_restart_backlog',
210
+ from: 'artist_alpha',
211
+ to: 'render_bot',
212
+ content: 'Generate: restart backlog parity art',
213
+ timestamp: '2026-04-16T18:10:00.000Z',
214
+ channel: channelName,
215
+ session_id: requesterSession && requesterSession.session ? requesterSession.session.session_id : null,
216
+ command_id: 'cmd_task10b_feature_restart_backlog',
217
+ correlation_id: 'corr_task10b_feature_restart_backlog',
218
+ };
219
+ canonicalState.appendScopedMessage(backlogRequest, {
220
+ branch: featureBranch,
221
+ channel: channelName,
222
+ actorAgent: backlogRequest.from,
223
+ sessionId: backlogRequest.session_id,
224
+ commandId: backlogRequest.command_id,
225
+ correlationId: backlogRequest.correlation_id,
226
+ });
227
+
228
+ engine = new ApiAgentEngine(fixture.dataDir);
229
+ engine.agents.render_bot.provider = {
230
+ async generate(prompt) {
231
+ return { type: 'text', data: `Rendered: ${prompt}` };
232
+ },
233
+ };
234
+
235
+ const restartResult = engine.start('render_bot');
236
+ assert(restartResult && restartResult.ok, 'Restarted API parity fixture should start the render_bot adapter successfully.', problems);
237
+
238
+ engine._pollMessages('render_bot');
239
+
240
+ const backlogProcessed = await waitFor(() => readJsonl(featureChannelHistoryFile).some((message) => message.reply_to === backlogRequest.id && message.content === 'Rendered: restart backlog parity art'));
241
+ assert(backlogProcessed, 'Restarting the API adapter must preserve and process directed backlog that arrived while the adapter was stopped.', problems);
242
+
243
+ const consumedIds = readJson(branchPaths.getConsumedFile('render_bot', featureBranch), []);
244
+ assert(Array.isArray(consumedIds) && consumedIds.includes(backlogRequest.id), 'Processed restart backlog should be persisted in the branch-local consumed state instead of staying only in memory.', problems);
245
+ } finally {
246
+ try { engine && typeof engine.stopAll === 'function' && engine.stopAll(); } catch {}
247
+ removeFixture(fixture.tempRoot);
248
+ }
249
+
250
+ if (problems.length > 0) {
251
+ fail([
252
+ 'API agent parity validation failed.',
253
+ ...problems.map((problem) => `- ${problem}`),
254
+ ]);
255
+ }
256
+
257
+ console.log([
258
+ 'API agent parity validation passed.',
259
+ '- Non-CLI adapters poll branch-scoped conversation history instead of only main messages.jsonl.',
260
+ '- Replies preserve branch/channel/session/correlation metadata through canonical writes and scoped message/history files.',
261
+ '- API adapters now participate in branch-local session manifests, sessions-index projections, and branch tracking like first-class runtimes.',
262
+ '- Restarted API adapters preserve outstanding directed backlog by reusing persisted consumed-state instead of marking every existing message as seen.',
263
+ ].join('\n'));
264
+ }
265
+
266
+ function sameReplySet(left, right) {
267
+ const leftIds = left.map((message) => message.id).sort();
268
+ const rightIds = right.map((message) => message.id).sort();
269
+ return JSON.stringify(leftIds) === JSON.stringify(rightIds);
270
+ }
271
+
272
+ main().catch((error) => {
273
+ fail([
274
+ 'API agent parity validation crashed.',
275
+ `- ${error && error.stack ? error.stack : String(error)}`,
276
+ ]);
277
+ });
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const SERVER_FILE = path.resolve(__dirname, '..', 'server.js');
7
+ const {
8
+ evaluateAutonomyCandidate,
9
+ rankClaimableTasks,
10
+ resolveAgentDecisionContext,
11
+ selectAutonomyDecisionCandidate,
12
+ } = require(path.resolve(__dirname, '..', 'autonomy', 'decision-v2.js'));
13
+ const { resolveAgentContract } = require(path.resolve(__dirname, '..', 'agent-contracts.js'));
14
+
15
+ function fail(lines, exitCode = 1) {
16
+ process.stderr.write(lines.join('\n') + '\n');
17
+ process.exit(exitCode);
18
+ }
19
+
20
+ function assert(condition, message, problems) {
21
+ if (!condition) problems.push(message);
22
+ }
23
+
24
+ function extractBlock(source, startAnchor, endAnchor) {
25
+ const startIndex = source.indexOf(startAnchor);
26
+ if (startIndex === -1) return '';
27
+ const endIndex = endAnchor ? source.indexOf(endAnchor, startIndex + startAnchor.length) : source.length;
28
+ if (endIndex === -1) return source.slice(startIndex);
29
+ return source.slice(startIndex, endIndex);
30
+ }
31
+
32
+ function main() {
33
+ const problems = [];
34
+ const serverSource = fs.readFileSync(SERVER_FILE, 'utf8');
35
+ const getWorkBlock = extractBlock(serverSource, 'async function toolGetWork(params = {}) {', 'async function toolVerifyAndAdvance(params) {');
36
+
37
+ assert(serverSource.includes("require('./autonomy/decision-v2')"), 'server.js must load the shared autonomy-v2 decision helper module.', problems);
38
+ assert(getWorkBlock.includes('const decisionContext = buildAutonomyDecisionContext(contract, skills, agents);'), 'toolGetWork() must build an autonomy-v2 decision context before selecting work.', problems);
39
+ assert(getWorkBlock.includes('rankClaimableTasks('), 'toolGetWork() must rank self-claimable tasks through the autonomy-v2 decision helper.', problems);
40
+ assert(getWorkBlock.includes('selectAutonomyDecisionCandidate(prelistenCandidates, decisionContext)'), 'toolGetWork() must select pre-listen work from explicit ranked candidates.', problems);
41
+ assert(getWorkBlock.includes('selectAutonomyDecisionCandidate(postlistenCandidates, decisionContext)'), 'toolGetWork() must select post-listen work from explicit ranked candidates.', problems);
42
+ assert(getWorkBlock.includes('attachCapabilityAdvisory('), 'toolGetWork() must surface capability advisory metadata from the autonomy-v2 decision helper when relevant.', problems);
43
+ assert(!getWorkBlock.includes('const unassigned = findUnassignedTasks(skills);'), 'toolGetWork() must no longer fall back to the old direct findUnassignedTasks(skills) heuristic seam.', problems);
44
+
45
+ const reviewerContext = resolveAgentDecisionContext({
46
+ agentName: 'qa_agent',
47
+ branchId: 'main',
48
+ sessionSummary: { session_id: 'sess_qa', branch_id: 'main', state: 'active' },
49
+ contract: resolveAgentContract({ role: 'quality', archetype: 'reviewer', skills: ['testing'] }),
50
+ agentRecord: { runtime_type: 'cli' },
51
+ });
52
+
53
+ const assignedPrecedenceSelection = selectAutonomyDecisionCandidate([
54
+ {
55
+ id: 'task_followup',
56
+ order: 20,
57
+ target: {
58
+ work_type: 'task',
59
+ title: 'Review follow-up notes',
60
+ description: 'Write the release summary',
61
+ },
62
+ },
63
+ {
64
+ id: 'assigned_media_step',
65
+ order: 10,
66
+ target: {
67
+ work_type: 'workflow_step',
68
+ title: 'Produce the release video',
69
+ description: 'Assigned media step',
70
+ assigned: true,
71
+ assignment_priority: 'active',
72
+ required_capabilities: ['video_generation'],
73
+ session_summary: { session_id: 'sess_qa', branch_id: 'main', state: 'active' },
74
+ resume_context: { dependency_evidence: [{ evidence: { evidence_ref: { evidence_id: 'ev_dep' } } }] },
75
+ },
76
+ },
77
+ ], reviewerContext);
78
+ assert(!!assignedPrecedenceSelection, 'Assigned-precedence fixture must yield a selected candidate.', problems);
79
+ assert(assignedPrecedenceSelection && assignedPrecedenceSelection.id === 'assigned_media_step', 'Explicitly assigned workflow work must still outrank non-assigned work even when capability fit is weaker.', problems);
80
+ assert(assignedPrecedenceSelection && assignedPrecedenceSelection.evaluation.capability_advisory.status === 'mismatch', 'Assigned precedence fixture must expose capability mismatch advisory instead of silently overriding ownership.', problems);
81
+ assert(assignedPrecedenceSelection && assignedPrecedenceSelection.evaluation.capability_advisory.admissible === true, 'Assigned precedence fixture must keep assigned work admissible in the decision-only slice.', problems);
82
+
83
+ const imageCapableContext = resolveAgentDecisionContext({
84
+ agentName: 'media_agent',
85
+ branchId: 'main',
86
+ contract: resolveAgentContract({ archetype: 'implementer', skills: ['media'] }),
87
+ agentRecord: {
88
+ runtime_type: 'api',
89
+ provider_id: 'gemini',
90
+ model_id: 'gemini-image',
91
+ capabilities: ['image_generation'],
92
+ },
93
+ });
94
+ const chatOnlyContext = resolveAgentDecisionContext({
95
+ agentName: 'chat_agent',
96
+ branchId: 'main',
97
+ contract: resolveAgentContract({ archetype: 'implementer', skills: ['media'] }),
98
+ agentRecord: { runtime_type: 'cli' },
99
+ });
100
+ const capabilityCandidates = [
101
+ {
102
+ id: 'image_asset_task',
103
+ order: 10,
104
+ target: {
105
+ work_type: 'task',
106
+ title: 'Render release poster',
107
+ description: 'Create the poster asset for the launch',
108
+ required_capabilities: ['image_generation'],
109
+ },
110
+ },
111
+ {
112
+ id: 'text_release_task',
113
+ order: 20,
114
+ target: {
115
+ work_type: 'task',
116
+ title: 'Write release notes',
117
+ description: 'Document the launch details',
118
+ },
119
+ },
120
+ ];
121
+ const imageSelection = selectAutonomyDecisionCandidate(capabilityCandidates, imageCapableContext);
122
+ const chatSelection = selectAutonomyDecisionCandidate(capabilityCandidates, chatOnlyContext);
123
+ assert(imageSelection && imageSelection.id === 'image_asset_task', 'Capability-aware selection must prefer explicitly image-capable work when the runtime advertises the required capability.', problems);
124
+ assert(chatSelection && chatSelection.id === 'text_release_task', 'Capability-aware selection must skip inadmissible media work instead of silently assigning it to a runtime that lacks the required capability.', problems);
125
+
126
+ const reviewPreferredSelection = selectAutonomyDecisionCandidate([
127
+ {
128
+ id: 'implementation_task',
129
+ order: 20,
130
+ target: {
131
+ work_type: 'task',
132
+ title: 'Implement dashboard drag and drop',
133
+ description: 'Write the feature code and wire the client behavior',
134
+ },
135
+ },
136
+ {
137
+ id: 'review_request',
138
+ order: 10,
139
+ target: {
140
+ work_type: 'review',
141
+ title: 'Review API regression fix',
142
+ description: 'Verify tests and review the patch for regressions',
143
+ },
144
+ },
145
+ ], reviewerContext);
146
+ assert(reviewPreferredSelection && reviewPreferredSelection.id === 'review_request', 'Contract-aware selection must be able to rank review work above generic implementation work for reviewer-style agents.', problems);
147
+
148
+ const prepWithEvidence = evaluateAutonomyCandidate({
149
+ target: {
150
+ work_type: 'prep_work',
151
+ title: 'Prepare for downstream implementation',
152
+ description: 'Read the latest verified upstream context',
153
+ assigned: true,
154
+ assignment_priority: 'assigned',
155
+ session_summary: { session_id: 'sess_qa', branch_id: 'main', state: 'active' },
156
+ resume_context: {
157
+ dependency_evidence: [{ evidence: { evidence_ref: { evidence_id: 'ev_dep' } } }],
158
+ recent_evidence: [{ evidence: { evidence_ref: { evidence_id: 'ev_recent' } } }],
159
+ },
160
+ },
161
+ }, reviewerContext);
162
+ const prepWithoutEvidence = evaluateAutonomyCandidate({
163
+ target: {
164
+ work_type: 'prep_work',
165
+ title: 'Prepare for downstream implementation',
166
+ description: 'Read the latest verified upstream context',
167
+ assigned: true,
168
+ assignment_priority: 'assigned',
169
+ },
170
+ }, reviewerContext);
171
+ assert(prepWithEvidence.resume_signal_count > prepWithoutEvidence.resume_signal_count, 'Session/evidence-aware evaluation must count resume signals when authoritative resume context is present.', problems);
172
+ assert(prepWithEvidence.score > prepWithoutEvidence.score, 'Session/evidence-aware evaluation must boost ranked prep work when authoritative resume context is present.', problems);
173
+
174
+ const rankedTasks = rankClaimableTasks([
175
+ { id: 'docs_task', status: 'pending', title: 'Refresh docs', description: 'Documentation cleanup' },
176
+ { id: 'backend_task', status: 'pending', title: 'Implement backend route', description: 'Backend service endpoint for launch flows' },
177
+ ], resolveAgentDecisionContext({
178
+ agentName: 'builder',
179
+ branchId: 'main',
180
+ contract: resolveAgentContract({ archetype: 'implementer' }),
181
+ agentRecord: { runtime_type: 'cli' },
182
+ availableSkills: ['backend'],
183
+ }), {
184
+ allTasks: [
185
+ { id: 'done_backend', assignee: 'builder', status: 'done', title: 'Fix backend auth', description: 'Backend service token refresh path' },
186
+ { id: 'other_done', assignee: 'someone_else', status: 'done', title: 'Tidy styles', description: 'CSS cleanup' },
187
+ ],
188
+ availableSkills: ['backend'],
189
+ });
190
+ assert(rankedTasks.length === 2, 'rankClaimableTasks() must keep admissible pending tasks available for selection.', problems);
191
+ assert(rankedTasks[0] && rankedTasks[0].task.id === 'backend_task', 'rankClaimableTasks() must preserve the historical-affinity behavior for self-claimable tasks inside the autonomy-v2 selector.', problems);
192
+
193
+ if (problems.length > 0) {
194
+ fail(['Autonomy-v2 decision validation failed.', ...problems.map((problem) => `- ${problem}`)]);
195
+ }
196
+
197
+ console.log([
198
+ 'Autonomy-v2 decision validation passed.',
199
+ '- get_work now selects from explicit ranked candidates built on the shared autonomy-v2 helper.',
200
+ '- Assigned work stays admissible and takes precedence even when capability fit is weaker.',
201
+ '- Capability metadata gates self-claimable work without silently reassigning existing ownership.',
202
+ '- Contract fit can rerank non-assigned work, while session/evidence resume signals boost context-rich prep decisions.',
203
+ '- Historical self-claim task affinity remains preserved inside the new selector.',
204
+ ].join('\n'));
205
+ }
206
+
207
+ main();