let-them-talk 5.3.0 → 5.4.1

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 +346 -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 +864 -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 +141 -34
  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 +9577 -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,224 @@
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
+ classifyBlockedTaskPolicy,
9
+ classifyRetryPolicy,
10
+ classifyStalledWorkflowStepPolicy,
11
+ planWatchdogActions,
12
+ } = require(path.resolve(__dirname, '..', 'autonomy', 'watchdog-policy.js'));
13
+ const { resolveAgentDecisionContext } = require(path.resolve(__dirname, '..', 'autonomy', 'decision-v2.js'));
14
+ const { resolveAgentContract } = require(path.resolve(__dirname, '..', 'agent-contracts.js'));
15
+
16
+ function fail(lines, exitCode = 1) {
17
+ process.stderr.write(lines.join('\n') + '\n');
18
+ process.exit(exitCode);
19
+ }
20
+
21
+ function assert(condition, message, problems) {
22
+ if (!condition) problems.push(message);
23
+ }
24
+
25
+ function extractBlock(source, startAnchor, endAnchor) {
26
+ const startIndex = source.indexOf(startAnchor);
27
+ if (startIndex === -1) return '';
28
+ const endIndex = endAnchor ? source.indexOf(endAnchor, startIndex + startAnchor.length) : source.length;
29
+ if (endIndex === -1) return source.slice(startIndex);
30
+ return source.slice(startIndex, endIndex);
31
+ }
32
+
33
+ function isoFromMs(ms) {
34
+ return new Date(ms).toISOString();
35
+ }
36
+
37
+ function main() {
38
+ const problems = [];
39
+ const serverSource = fs.readFileSync(SERVER_FILE, 'utf8');
40
+ const watchdogBlock = extractBlock(serverSource, 'function watchdogCheck() {', '// --- Monitor Agent: system health check ---');
41
+ const updateTaskBlock = extractBlock(serverSource, 'function toolUpdateTask(taskId, status, notes = null, evidence = null) {', 'function toolListTasks(status = null, assignee = null) {');
42
+ const monitorBlock = extractBlock(serverSource, 'function monitorHealthCheck() {', '// --- Advisor Agent: strategic analysis ---');
43
+
44
+ assert(serverSource.includes("require('./autonomy/watchdog-policy')"), 'server.js must load the shared autonomy/watchdog policy helper.', problems);
45
+ assert(watchdogBlock.includes('const watchdogActions = planWatchdogActions({'), 'watchdogCheck() must derive actions from the shared watchdog policy planner.', problems);
46
+ assert(watchdogBlock.includes('canonicalState.setWorkflowStepPolicySignal({'), 'watchdogCheck() must persist workflow-step watchdog signals through canonical state helpers.', problems);
47
+ assert(watchdogBlock.includes("sourceTool: 'watchdog_policy'"), 'watchdogCheck() task transitions must identify the explicit watchdog policy source tool.', problems);
48
+ assert(!serverSource.includes('function reassignWorkFrom('), 'server.js must remove the old raw watchdog reassignment helper seam.', problems);
49
+ assert(!watchdogBlock.includes('step.assignee = replacement'), 'watchdogCheck() must no longer silently assign stalled workflow steps to a replacement agent.', problems);
50
+ assert(!watchdogBlock.includes('task.assignee = null; // Unassign so get_work can claim it'), 'watchdogCheck() must no longer mutate task claims through the old raw reassignment block.', problems);
51
+ assert(!monitorBlock.includes('task.assignee = freshAgents[0]'), 'monitorHealthCheck() must stop acting like a second scheduler by directly assigning fresh agents.', problems);
52
+ assert(updateTaskBlock.includes('const retryPolicy = status === \'pending\''), 'toolUpdateTask() must classify pending-task retry policy explicitly before mutating task state.', problems);
53
+ assert(updateTaskBlock.includes('classifyRetryPolicy({'), 'toolUpdateTask() must use the shared retry policy classifier.', problems);
54
+ assert(!updateTaskBlock.includes("task.status = 'blocked_permanent';"), 'toolUpdateTask() must not mutate blocked_permanent directly via the old raw circuit-breaker branch.', problems);
55
+
56
+ const interruptedReviewerContext = resolveAgentDecisionContext({
57
+ agentName: 'qa_agent',
58
+ branchId: 'main',
59
+ sessionSummary: { session_id: 'sess_qa', branch_id: 'main', state: 'interrupted', stale: true },
60
+ contract: resolveAgentContract({ role: 'quality', archetype: 'reviewer', skills: ['testing'] }),
61
+ agentRecord: { runtime_type: 'cli' },
62
+ });
63
+ const activeImageContext = resolveAgentDecisionContext({
64
+ agentName: 'media_bot',
65
+ branchId: 'main',
66
+ sessionSummary: { session_id: 'sess_media', branch_id: 'main', state: 'active', stale: false },
67
+ contract: resolveAgentContract({ archetype: 'implementer', skills: ['media'] }),
68
+ agentRecord: {
69
+ runtime_type: 'api',
70
+ provider_id: 'gemini',
71
+ model_id: 'gemini-image',
72
+ capabilities: ['image_generation'],
73
+ },
74
+ });
75
+
76
+ const retryPolicy = classifyRetryPolicy({
77
+ target: {
78
+ work_type: 'task',
79
+ title: 'Render launch poster',
80
+ description: 'Create the image asset for release day',
81
+ assigned: true,
82
+ required_capabilities: ['image_generation'],
83
+ },
84
+ context: interruptedReviewerContext,
85
+ attemptCount: 3,
86
+ ownerAlive: true,
87
+ idleMs: 0,
88
+ });
89
+ assert(retryPolicy.state === 'blocked_permanent', 'Retry policy must block permanently at the bounded attempt limit.', problems);
90
+ assert(retryPolicy.owner_state === 'session_interrupted', 'Retry policy must surface interrupted canonical session state explicitly.', problems);
91
+ assert(retryPolicy.capability_advisory && retryPolicy.capability_advisory.status === 'mismatch', 'Retry policy must keep provider/capability context visible instead of falling back to raw attempt counts only.', problems);
92
+
93
+ const blockedPolicy = classifyBlockedTaskPolicy({
94
+ target: {
95
+ work_type: 'task',
96
+ title: 'Recover blocked artifact pipeline',
97
+ description: 'Blocked build artifact repair task',
98
+ assigned: true,
99
+ },
100
+ context: interruptedReviewerContext,
101
+ attemptCount: 2,
102
+ ownerAlive: false,
103
+ idleMs: 0,
104
+ blockedAgeMs: 360000,
105
+ });
106
+ assert(blockedPolicy.signal === 'escalate', 'Blocked-task policy must produce an explicit escalation signal once the bounded window or retry threshold is crossed.', problems);
107
+ assert(blockedPolicy.classification === 'blocked_owner_unavailable', 'Blocked-task policy must classify unavailable owners explicitly from canonical session/aliveness context.', problems);
108
+
109
+ const checkinStepPolicy = classifyStalledWorkflowStepPolicy({
110
+ target: {
111
+ work_type: 'workflow_step',
112
+ title: 'Assemble release pack',
113
+ description: 'Launch workflow',
114
+ assigned: true,
115
+ required_capabilities: ['image_generation'],
116
+ },
117
+ context: activeImageContext,
118
+ ownerAlive: true,
119
+ idleMs: 0,
120
+ stepAgeMs: 960000,
121
+ resumeContext: {
122
+ dependency_evidence: [{ evidence: { evidence_ref: { evidence_id: 'ev_dep' } } }],
123
+ recent_evidence: [{ evidence: { evidence_ref: { evidence_id: 'ev_recent' } } }],
124
+ },
125
+ });
126
+ assert(checkinStepPolicy.signal === 'checkin', 'Stalled-step policy must request a bounded status check before a full escalation.', problems);
127
+ assert(checkinStepPolicy.dependency_evidence_count === 1 && checkinStepPolicy.recent_evidence_count === 1, 'Stalled-step policy must carry evidence context counts explicitly.', problems);
128
+
129
+ const unavailableStepPolicy = classifyStalledWorkflowStepPolicy({
130
+ target: {
131
+ work_type: 'workflow_step',
132
+ title: 'Finalize release',
133
+ description: 'Launch workflow',
134
+ assigned: true,
135
+ },
136
+ context: interruptedReviewerContext,
137
+ ownerAlive: false,
138
+ idleMs: 0,
139
+ stepAgeMs: 60000,
140
+ });
141
+ assert(unavailableStepPolicy.signal === 'escalate', 'Stalled-step policy must escalate immediately when the canonical owner is unavailable.', problems);
142
+ assert(unavailableStepPolicy.classification === 'step_owner_unavailable', 'Unavailable-step policy must be classified explicitly for follow-on 12C handling.', problems);
143
+
144
+ const nowMs = Date.UTC(2026, 3, 16, 12, 0, 0);
145
+ const watchdogActions = planWatchdogActions({
146
+ watchdogAgentName: 'alpha',
147
+ branchId: 'main',
148
+ nowMs,
149
+ agents: {
150
+ alpha: { branch: 'main', last_activity: isoFromMs(nowMs - 1000) },
151
+ bravo: { branch: 'main', last_activity: isoFromMs(nowMs - 360000), watchdog_nudged: nowMs - 240000 },
152
+ charlie: { branch: 'main', last_activity: isoFromMs(nowMs - 720000) },
153
+ },
154
+ tasks: [
155
+ {
156
+ id: 'task_blocked',
157
+ title: 'Blocked publish checklist',
158
+ description: 'Wait on blocked release dependency',
159
+ status: 'blocked',
160
+ assignee: 'bravo',
161
+ updated_at: isoFromMs(nowMs - 400000),
162
+ attempt_agents: ['alpha', 'bravo'],
163
+ },
164
+ {
165
+ id: 'task_claim',
166
+ title: 'Recover dead owner claim',
167
+ description: 'Resume from interrupted owner state',
168
+ status: 'in_progress',
169
+ assignee: 'charlie',
170
+ updated_at: isoFromMs(nowMs - 120000),
171
+ attempt_agents: ['charlie'],
172
+ },
173
+ ],
174
+ workflows: [{
175
+ id: 'wf_launch',
176
+ name: 'Launch workflow',
177
+ status: 'active',
178
+ steps: [{
179
+ id: 2,
180
+ description: 'Finalize release notes',
181
+ status: 'in_progress',
182
+ assignee: 'charlie',
183
+ started_at: isoFromMs(nowMs - 1900000),
184
+ }],
185
+ }],
186
+ resolveContext: (agentName) => {
187
+ if (agentName === 'bravo') return activeImageContext;
188
+ if (agentName === 'charlie') return interruptedReviewerContext;
189
+ return resolveAgentDecisionContext({
190
+ agentName,
191
+ branchId: 'main',
192
+ sessionSummary: { session_id: `sess_${agentName}`, branch_id: 'main', state: 'active', stale: false },
193
+ contract: resolveAgentContract({ archetype: 'generalist' }),
194
+ agentRecord: { runtime_type: 'cli' },
195
+ });
196
+ },
197
+ resolveStepResumeContext: () => ({
198
+ dependency_evidence: [{ evidence: { evidence_ref: { evidence_id: 'ev_dep' } } }],
199
+ recent_evidence: [],
200
+ }),
201
+ isAgentAlive: (agentName) => agentName !== 'charlie',
202
+ });
203
+
204
+ assert(watchdogActions.some((action) => action.kind === 'nudge_idle_hard' && action.agentName === 'bravo'), 'Watchdog planner must emit explicit hard-idle nudges deterministically.', problems);
205
+ assert(watchdogActions.some((action) => action.kind === 'escalate_blocked_task' && action.taskId === 'task_blocked'), 'Watchdog planner must escalate blocked tasks through explicit policy actions.', problems);
206
+ assert(watchdogActions.some((action) => action.kind === 'release_task_claim' && action.taskId === 'task_claim'), 'Watchdog planner must release interrupted/dead task claims without assigning a replacement.', problems);
207
+ assert(watchdogActions.some((action) => action.kind === 'signal_stalled_step' && action.workflowId === 'wf_launch' && action.signal === 'escalate'), 'Watchdog planner must surface stalled workflow steps as explicit escalation signals.', problems);
208
+ assert(!watchdogActions.some((action) => String(action.kind || '').includes('reassign')), 'Watchdog planner must not emit broad reassignment actions in the 12B policy-only slice.', problems);
209
+ assert(!watchdogActions.some((action) => String(action.kind || '').includes('assign')), 'Watchdog planner must not silently assign new owners in the 12B policy-only slice.', problems);
210
+
211
+ if (problems.length > 0) {
212
+ fail(['Autonomy-v2 watchdog policy validation failed.', ...problems.map((problem) => `- ${problem}`)]);
213
+ }
214
+
215
+ console.log([
216
+ 'Autonomy-v2 watchdog policy validation passed.',
217
+ '- Watchdog and retry policy now flow through a shared classifier/planner instead of raw server-side mutation seams.',
218
+ '- Retry, blocked-task, and stalled-step decisions all carry explicit session, contract, capability, and evidence context.',
219
+ '- Dead/interrupted ownership now releases task claims or emits escalation signals without broad silent reassignment.',
220
+ '- Monitor health reporting consumes the same policy planner and no longer acts like a second scheduler.',
221
+ ].join('\n'));
222
+ }
223
+
224
+ main();
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const { Client } = require('@modelcontextprotocol/sdk/client/index.js');
8
+ const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js');
9
+
10
+ const { createBranchPathResolvers } = require(path.resolve(__dirname, '..', 'state', 'canonical.js'));
11
+
12
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
13
+ const SERVER_FILE = path.resolve(PACKAGE_ROOT, 'server.js');
14
+
15
+ function fail(lines, exitCode = 1) {
16
+ fs.writeSync(2, lines.join('\n') + '\n');
17
+ process.exit(exitCode);
18
+ }
19
+
20
+ function stableSerialize(value) {
21
+ if (Array.isArray(value)) {
22
+ return `[${value.map((entry) => stableSerialize(entry)).join(',')}]`;
23
+ }
24
+
25
+ if (value && typeof value === 'object') {
26
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableSerialize(value[key])}`).join(',')}}`;
27
+ }
28
+
29
+ return JSON.stringify(value);
30
+ }
31
+
32
+ function deepEqual(actual, expected) {
33
+ return stableSerialize(actual) === stableSerialize(expected);
34
+ }
35
+
36
+ function toJsonl(rows) {
37
+ return rows.map((row) => JSON.stringify(row)).join('\n') + (rows.length ? '\n' : '');
38
+ }
39
+
40
+ function readJson(filePath, fallback) {
41
+ if (!fs.existsSync(filePath)) return fallback;
42
+ try {
43
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
44
+ } catch {
45
+ return fallback;
46
+ }
47
+ }
48
+
49
+ function readJsonl(filePath) {
50
+ if (!fs.existsSync(filePath)) return [];
51
+ const raw = fs.readFileSync(filePath, 'utf8').trim();
52
+ if (!raw) return [];
53
+ return raw.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
54
+ }
55
+
56
+ async function callTool(client, name, args = {}) {
57
+ const result = await client.callTool({ name, arguments: args });
58
+ const text = Array.isArray(result.content)
59
+ ? result.content.map((entry) => (entry && entry.type === 'text' ? entry.text : '')).join('')
60
+ : '';
61
+
62
+ if (result.isError) {
63
+ throw new Error(`${name} failed: ${text || 'unknown error'}`);
64
+ }
65
+
66
+ return text ? JSON.parse(text) : {};
67
+ }
68
+
69
+ function createFixture() {
70
+ const evidenceRef = {
71
+ evidence_id: 'evidence-task4f-main',
72
+ branch_id: 'main',
73
+ recorded_at: '2026-04-16T00:00:05.000Z',
74
+ recorded_by_session: 'session-alpha-main',
75
+ };
76
+
77
+ return {
78
+ message: {
79
+ id: 'msg-task4f-main-1',
80
+ from: 'alpha',
81
+ to: 'beta',
82
+ content: 'Main branch snapshot seed',
83
+ timestamp: '2026-04-16T00:00:10.000Z',
84
+ reply_to: null,
85
+ system: false,
86
+ },
87
+ task: {
88
+ id: 'task-task4f-main',
89
+ title: 'Main branch fork snapshot task',
90
+ description: 'Task fixture that must copy into the forked branch snapshot.',
91
+ status: 'done',
92
+ assignee: 'alpha',
93
+ created_by: 'alpha',
94
+ created_at: '2026-04-16T00:00:00.000Z',
95
+ updated_at: '2026-04-16T00:00:05.000Z',
96
+ evidence_ref: evidenceRef,
97
+ notes: [],
98
+ },
99
+ workflow: {
100
+ id: 'wf-task4f-main',
101
+ name: 'Main branch fork snapshot workflow',
102
+ branch_id: 'main',
103
+ status: 'active',
104
+ autonomous: false,
105
+ parallel: false,
106
+ created_by: 'alpha',
107
+ created_at: '2026-04-16T00:00:00.000Z',
108
+ updated_at: '2026-04-16T00:00:05.000Z',
109
+ steps: [
110
+ {
111
+ id: 1,
112
+ description: 'Seeded workflow step',
113
+ assignee: 'alpha',
114
+ depends_on: [],
115
+ status: 'done',
116
+ started_at: '2026-04-16T00:00:00.000Z',
117
+ completed_at: '2026-04-16T00:00:05.000Z',
118
+ notes: '',
119
+ evidence_ref: evidenceRef,
120
+ },
121
+ ],
122
+ },
123
+ evidenceStore: {
124
+ schema_version: 1,
125
+ updated_at: '2026-04-16T00:00:05.000Z',
126
+ records: [
127
+ {
128
+ evidence_id: 'evidence-task4f-main',
129
+ subject_kind: 'completion',
130
+ branch_id: 'main',
131
+ task_id: 'task-task4f-main',
132
+ workflow_id: 'wf-task4f-main',
133
+ step_id: 1,
134
+ notes: 'Seeded branch-local evidence',
135
+ summary: 'Validated seeded fork snapshot state.',
136
+ verification: 'Manual temp-runtime inspection',
137
+ files_changed: ['agent-bridge/server.js'],
138
+ confidence: 95,
139
+ learnings: 'Fork snapshots should preserve task/workflow evidence context.',
140
+ flagged: false,
141
+ flag_reason: null,
142
+ recorded_at: '2026-04-16T00:00:05.000Z',
143
+ recorded_by: 'alpha',
144
+ recorded_by_session: 'session-alpha-main',
145
+ source_tool: 'check-branch-fork-snapshot',
146
+ },
147
+ ],
148
+ },
149
+ workspaces: {
150
+ alpha: {
151
+ draft: { content: 'Main branch workspace note' },
152
+ retry_history: [],
153
+ },
154
+ },
155
+ };
156
+ }
157
+
158
+ async function runValidation() {
159
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'letthemtalk-fork-snapshot-'));
160
+ const dataDir = path.join(tempRoot, '.agent-bridge');
161
+ const branchPaths = createBranchPathResolvers(dataDir);
162
+ const fixture = createFixture();
163
+ const problems = [];
164
+ let transport = null;
165
+
166
+ try {
167
+ fs.mkdirSync(dataDir, { recursive: true });
168
+ fs.writeFileSync(branchPaths.getMessagesFile('main'), toJsonl([fixture.message]));
169
+ fs.writeFileSync(branchPaths.getHistoryFile('main'), toJsonl([fixture.message]));
170
+ fs.writeFileSync(branchPaths.getTasksFile('main'), JSON.stringify([fixture.task], null, 2));
171
+ fs.writeFileSync(branchPaths.getWorkflowsFile('main'), JSON.stringify([fixture.workflow], null, 2));
172
+ fs.writeFileSync(branchPaths.getEvidenceFile('main'), JSON.stringify(fixture.evidenceStore, null, 2));
173
+ fs.mkdirSync(branchPaths.getWorkspacesDir('main'), { recursive: true });
174
+ for (const [agentName, workspace] of Object.entries(fixture.workspaces)) {
175
+ fs.writeFileSync(branchPaths.getWorkspaceFile(agentName, 'main'), JSON.stringify(workspace, null, 2));
176
+ }
177
+
178
+ const client = new Client({ name: 'branch-fork-validator', version: '1.0.0' });
179
+ transport = new StdioClientTransport({
180
+ command: process.execPath,
181
+ args: [SERVER_FILE],
182
+ cwd: PACKAGE_ROOT,
183
+ env: {
184
+ AGENT_BRIDGE_DATA_DIR: dataDir,
185
+ },
186
+ stderr: 'pipe',
187
+ });
188
+
189
+ await client.connect(transport);
190
+ await client.listTools();
191
+
192
+ await callTool(client, 'register', { name: 'alpha', provider: 'Verifier' });
193
+ const forkResult = await callTool(client, 'fork_conversation', { branch_name: 'feature_task4f' });
194
+
195
+ if (!forkResult.success || forkResult.branch !== 'feature_task4f') {
196
+ problems.push('fork_conversation must create and switch into the target feature_task4f branch.');
197
+ }
198
+
199
+ const forkMessages = readJsonl(branchPaths.getMessagesFile('feature_task4f'));
200
+ const forkHistory = readJsonl(branchPaths.getHistoryFile('feature_task4f'));
201
+ const forkTasks = readJson(branchPaths.getTasksFile('feature_task4f'), []);
202
+ const forkWorkflows = readJson(branchPaths.getWorkflowsFile('feature_task4f'), []);
203
+ const forkEvidence = readJson(branchPaths.getEvidenceFile('feature_task4f'), null);
204
+ const forkWorkspace = readJson(branchPaths.getWorkspaceFile('alpha', 'feature_task4f'), {});
205
+
206
+ const expectedForkTask = {
207
+ ...fixture.task,
208
+ evidence_ref: {
209
+ ...fixture.task.evidence_ref,
210
+ branch_id: 'feature_task4f',
211
+ },
212
+ };
213
+
214
+ const expectedForkWorkflow = {
215
+ ...fixture.workflow,
216
+ branch_id: 'feature_task4f',
217
+ steps: fixture.workflow.steps.map((step) => ({
218
+ ...step,
219
+ evidence_ref: {
220
+ ...step.evidence_ref,
221
+ branch_id: 'feature_task4f',
222
+ },
223
+ })),
224
+ };
225
+
226
+ const expectedForkEvidence = {
227
+ ...fixture.evidenceStore,
228
+ records: fixture.evidenceStore.records.map((record) => ({
229
+ ...record,
230
+ branch_id: 'feature_task4f',
231
+ })),
232
+ };
233
+
234
+ if (forkMessages.length !== 0) {
235
+ problems.push('Forked branch messages projection must start empty after the snapshot copy.');
236
+ }
237
+
238
+ if (!deepEqual(forkHistory, [fixture.message])) {
239
+ problems.push('Forked branch history must preserve the visible main-branch conversation snapshot.');
240
+ }
241
+
242
+ if (!deepEqual(forkTasks, [expectedForkTask])) {
243
+ problems.push('Forked branch tasks must copy the source branch task snapshot and remap embedded branch references.');
244
+ }
245
+
246
+ if (!deepEqual(forkWorkflows, [expectedForkWorkflow])) {
247
+ problems.push('Forked branch workflows must copy the source branch workflow snapshot and remap branch_id fields to the fork.');
248
+ }
249
+
250
+ if (!deepEqual(forkEvidence, expectedForkEvidence)) {
251
+ problems.push('Forked branch evidence store must copy historical evidence context and remap branch_id fields to the fork.');
252
+ }
253
+
254
+ if (!deepEqual(forkWorkspace, fixture.workspaces.alpha)) {
255
+ problems.push('Forked branch workspaces must copy the source branch workspace snapshot.');
256
+ }
257
+
258
+ await callTool(client, 'create_task', {
259
+ title: 'Fork-only branch task',
260
+ description: 'Verifies post-fork task divergence stays branch-local.',
261
+ });
262
+ await callTool(client, 'create_workflow', {
263
+ name: 'Fork-only branch workflow',
264
+ steps: ['Fork-only step', 'Fork-only follow-up'],
265
+ autonomous: false,
266
+ parallel: false,
267
+ });
268
+ await callTool(client, 'workspace_write', {
269
+ key: 'fork_note',
270
+ content: 'Fork-only workspace note',
271
+ });
272
+
273
+ const mainTasksAfterForkWrites = readJson(branchPaths.getTasksFile('main'), []);
274
+ const mainWorkflowsAfterForkWrites = readJson(branchPaths.getWorkflowsFile('main'), []);
275
+ const forkTasksAfterWrites = readJson(branchPaths.getTasksFile('feature_task4f'), []);
276
+ const forkWorkflowsAfterWrites = readJson(branchPaths.getWorkflowsFile('feature_task4f'), []);
277
+ const mainWorkspaceAfterForkWrites = readJson(branchPaths.getWorkspaceFile('alpha', 'main'), {});
278
+ const forkWorkspaceAfterWrites = readJson(branchPaths.getWorkspaceFile('alpha', 'feature_task4f'), {});
279
+
280
+ if (!deepEqual(mainTasksAfterForkWrites, [fixture.task])) {
281
+ problems.push('Fork-local task writes must not mutate the source branch task snapshot.');
282
+ }
283
+
284
+ if (!deepEqual(mainWorkflowsAfterForkWrites, [fixture.workflow])) {
285
+ problems.push('Fork-local workflow writes must not mutate the source branch workflow snapshot.');
286
+ }
287
+
288
+ if (!(Array.isArray(forkTasksAfterWrites) && forkTasksAfterWrites.length === 2)) {
289
+ problems.push('Fork-local task writes must append only inside the forked branch task projection.');
290
+ }
291
+
292
+ if (!(Array.isArray(forkWorkflowsAfterWrites) && forkWorkflowsAfterWrites.length === 2)) {
293
+ problems.push('Fork-local workflow writes must append only inside the forked branch workflow projection.');
294
+ }
295
+
296
+ if (!deepEqual(mainWorkspaceAfterForkWrites, fixture.workspaces.alpha)) {
297
+ problems.push('Fork-local workspace writes must not mutate the source branch workspace snapshot.');
298
+ }
299
+
300
+ if (!(forkWorkspaceAfterWrites && forkWorkspaceAfterWrites.fork_note && forkWorkspaceAfterWrites.fork_note.content === 'Fork-only workspace note')) {
301
+ problems.push('Fork-local workspace writes must persist only inside the forked branch workspace projection.');
302
+ }
303
+ } finally {
304
+ if (transport) {
305
+ try {
306
+ await transport.close();
307
+ } catch {}
308
+ }
309
+ fs.rmSync(tempRoot, { recursive: true, force: true });
310
+ }
311
+
312
+ return problems;
313
+ }
314
+
315
+ async function main() {
316
+ const problems = await runValidation();
317
+
318
+ if (problems.length > 0) {
319
+ fail([
320
+ 'Branch fork snapshot validation failed.',
321
+ 'Violations:',
322
+ ...problems.map((problem) => `- ${problem}`),
323
+ ], 1);
324
+ }
325
+
326
+ console.log([
327
+ 'Branch fork snapshot validation passed.',
328
+ 'Validated fork-time history/message reset plus branch-local task/workflow/evidence/workspace snapshot copying and post-fork divergence.',
329
+ ].join('\n'));
330
+ }
331
+
332
+ main().catch((error) => {
333
+ fail([
334
+ 'Branch fork snapshot validation failed.',
335
+ `Unhandled error: ${error && error.message ? error.message : String(error)}`,
336
+ ], 1);
337
+ });